Share Replication API and Scheduler Support

This patch provides the scheduler support to filter share
backends matching replication capabilities reported by the
hosts and the replication_type extra_spec provided via the
share_type during share creation.

It also adds wsgi routes, API endpoints and driver entry
routines to support the actions: list, show, create, delete
and promote share replicas. It augments the ShareInstance
DB model with a 'replica_state' attribute and the Share DB
Model with 'replication_type' attribute to support these
workflows.

Replica states are periodically updated from the respective
backends that the replicas are created on.

APIImpact
Impact on existing APIs:
In Microversion 2.11, the /shares APIs return 2 additional
fields during index and show calls for each share: 'has_replicas'
and 'replication_type'. Similarly, the field 'replica_state' is
added to the API response for /share-instances.
Also, deletion of a share that has replicas is forbidden,
returning error code 403.

DocImpact

Co-Authored-By: Alex Meade <mr.alex.meade@gmail.com>

Implements: blueprint manila-share-replication

Change-Id: I10515d55b1291c34777a31d8c6a3a1954f551235
This commit is contained in:
Goutham Pacha Ravi 2015-10-13 08:41:26 -04:00
parent e5f4e981f5
commit f858e537dd
48 changed files with 4143 additions and 104 deletions

View File

@ -110,5 +110,11 @@
"cgsnapshot:update" : "rule:default", "cgsnapshot:update" : "rule:default",
"cgsnapshot:delete": "rule:default", "cgsnapshot:delete": "rule:default",
"cgsnapshot:get_cgsnapshot": "rule:default", "cgsnapshot:get_cgsnapshot": "rule:default",
"cgsnapshot:get_all": "rule:default" "cgsnapshot:get_all": "rule:default",
"share_replica:get_all": "rule:default",
"share_replica:show": "rule:default",
"share_replica:create" : "rule:default",
"share_replica:delete": "rule:default",
"share_replica:promote": "rule:default"
} }

View File

@ -57,6 +57,7 @@ REST_API_VERSION_HISTORY = """
* 2.9 - Add export locations API * 2.9 - Add export locations API
* 2.10 - Field 'access_rules_status' was added to shares and share * 2.10 - Field 'access_rules_status' was added to shares and share
instances. instances.
* 2.11 - Share Replication support
""" """
@ -64,7 +65,7 @@ REST_API_VERSION_HISTORY = """
# The default api version request is defined to be the # The default api version request is defined to be the
# the minimum version of the API supported. # the minimum version of the API supported.
_MIN_API_VERSION = "2.0" _MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.10" _MAX_API_VERSION = "2.11"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -78,3 +78,10 @@ user documentation.
2.10 2.10
---- ----
Field 'access_rules_status' was added to shares and share instances. Field 'access_rules_status' was added to shares and share instances.
2.11
----
Share Replication support added. All Share replication APIs are tagged
'Experimental'. Share APIs return two new attributes: 'has_replicas' and
'replication_type'. Share instance APIs return a new attribute,
'replica_state'.

View File

@ -93,6 +93,8 @@ class ShareMixin(object):
raise exc.HTTPNotFound() raise exc.HTTPNotFound()
except exception.InvalidShare as e: except exception.InvalidShare as e:
raise exc.HTTPForbidden(explanation=six.text_type(e)) raise exc.HTTPForbidden(explanation=six.text_type(e))
except exception.Conflict as e:
raise exc.HTTPConflict(explanation=six.text_type(e))
return webob.Response(status_int=202) return webob.Response(status_int=202)
@ -116,7 +118,12 @@ class ShareMixin(object):
except ValueError: except ValueError:
raise exc.HTTPBadRequest( raise exc.HTTPBadRequest(
explanation=_("Bad value for 'force_host_copy'")) explanation=_("Bad value for 'force_host_copy'"))
self.share_api.migrate_share(context, share, host, force_host_copy)
try:
self.share_api.migrate_share(context, share, host, force_host_copy)
except exception.Conflict as e:
raise exc.HTTPConflict(explanation=six.text_type(e))
return webob.Response(status_int=202) return webob.Response(status_int=202)
def index(self, req): def index(self, req):

View File

@ -41,6 +41,7 @@ from manila.api.v2 import services
from manila.api.v2 import share_export_locations from manila.api.v2 import share_export_locations
from manila.api.v2 import share_instance_export_locations from manila.api.v2 import share_instance_export_locations
from manila.api.v2 import share_instances from manila.api.v2 import share_instances
from manila.api.v2 import share_replicas
from manila.api.v2 import share_types from manila.api.v2 import share_types
from manila.api.v2 import shares from manila.api.v2 import shares
from manila.api import versions from manila.api import versions
@ -280,3 +281,9 @@ class APIRouter(manila.api.openstack.APIRouter):
controller=self.resources["cgsnapshots"], controller=self.resources["cgsnapshots"],
collection={"detail": "GET"}, collection={"detail": "GET"},
member={"members": "GET", "action": "POST"}) member={"members": "GET", "action": "POST"})
self.resources['share-replicas'] = share_replicas.create_resource()
mapper.resource("share-replica", "share-replicas",
controller=self.resources['share-replicas'],
collection={'detail': 'GET'},
member={'action': 'POST'})

View File

@ -0,0 +1,182 @@
# Copyright 2015 Goutham Pacha Ravi
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""The Share Replication API."""
from oslo_log import log
import six
import webob
from webob import exc
from manila.api import common
from manila.api.openstack import wsgi
from manila.api.views import share_replicas as replication_view
from manila.common import constants
from manila import db
from manila import exception
from manila.i18n import _
from manila import share
LOG = log.getLogger(__name__)
MIN_SUPPORTED_API_VERSION = '2.11'
class ShareReplicationController(wsgi.Controller):
"""The Share Replication API controller for the OpenStack API."""
resource_name = 'share_replica'
_view_builder_class = replication_view.ReplicationViewBuilder
def __init__(self):
super(ShareReplicationController, self).__init__()
self.share_api = share.API()
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True)
def index(self, req):
"""Return a summary list of replicas."""
return self._get_replicas(req)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True)
def detail(self, req):
"""Returns a detailed list of replicas."""
return self._get_replicas(req, is_detail=True)
@wsgi.Controller.authorize('get_all')
def _get_replicas(self, req, is_detail=False):
"""Returns list of replicas."""
context = req.environ['manila.context']
share_id = req.params.get('share_id')
if share_id:
try:
replicas = db.share_replicas_get_all_by_share(
context, share_id)
except exception.NotFound:
msg = _("Share with share ID %s not found.") % share_id
raise exc.HTTPNotFound(explanation=msg)
else:
replicas = db.share_replicas_get_all(context)
limited_list = common.limited(replicas, req)
if is_detail:
replicas = self._view_builder.detail_list(req, limited_list)
else:
replicas = self._view_builder.summary_list(req, limited_list)
return replicas
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True)
@wsgi.Controller.authorize
def show(self, req, id):
"""Return data about the given replica."""
context = req.environ['manila.context']
try:
replica = db.share_replica_get(context, id)
except exception.ShareReplicaNotFound:
msg = _("Replica %s not found.") % id
raise exc.HTTPNotFound(explanation=msg)
return self._view_builder.detail(req, replica)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True)
@wsgi.response(202)
@wsgi.Controller.authorize
def create(self, req, body):
"""Add a replica to an existing share."""
context = req.environ['manila.context']
if not self.is_valid_body(body, 'share_replica'):
msg = _("Body does not contain 'share_replica' information.")
raise exc.HTTPUnprocessableEntity(explanation=msg)
share_id = body.get('share_replica').get('share_id')
availability_zone = body.get('share_replica').get('availability_zone')
share_network_id = body.get('share_replica').get('share_network_id')
if not share_id:
msg = _("Must provide Share ID to add replica.")
raise exc.HTTPBadRequest(explanation=msg)
try:
share_ref = db.share_get(context, share_id)
except exception.NotFound:
msg = _("No share exists with ID %s.")
raise exc.HTTPNotFound(explanation=msg % share_id)
try:
new_replica = self.share_api.create_share_replica(
context, share_ref, availability_zone=availability_zone,
share_network_id=share_network_id)
except exception.AvailabilityZoneNotFound as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
except exception.ReplicationException as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
except exception.ShareBusyException as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
return self._view_builder.detail(req, new_replica)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True)
@wsgi.Controller.authorize
def delete(self, req, id):
"""Delete a replica."""
context = req.environ['manila.context']
try:
replica = db.share_replica_get(context, id)
except exception.ShareReplicaNotFound:
msg = _("No replica exists with ID %s.")
raise exc.HTTPNotFound(explanation=msg % id)
try:
self.share_api.delete_share_replica(context, replica)
except exception.ReplicationException as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
return webob.Response(status_int=202)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True)
@wsgi.action('promote')
@wsgi.response(202)
@wsgi.Controller.authorize
def promote(self, req, id, body):
"""Promote a replica to active state."""
context = req.environ['manila.context']
try:
replica = db.share_replica_get(context, id)
except exception.ShareReplicaNotFound:
msg = _("No replica exists with ID %s.")
raise exc.HTTPNotFound(explanation=msg % id)
replica_state = replica.get('replica_state')
if replica_state == constants.REPLICA_STATE_ACTIVE:
return webob.Response(status_int=200)
try:
replica = self.share_api.promote_share_replica(context, replica)
except exception.ReplicationException as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
except exception.AdminRequired as e:
raise exc.HTTPForbidden(explanation=six.text_type(e))
return self._view_builder.detail(req, replica)
def create_resource():
return wsgi.Resource(ShareReplicationController())

View File

@ -21,6 +21,7 @@ class ViewBuilder(common.ViewBuilder):
_detail_version_modifiers = [ _detail_version_modifiers = [
"remove_export_locations", "remove_export_locations",
"add_access_rules_status_field", "add_access_rules_status_field",
"add_replication_fields",
] ]
def detail_list(self, request, instances): def detail_list(self, request, instances):
@ -71,3 +72,7 @@ class ViewBuilder(common.ViewBuilder):
instance_dict['access_rules_status'] = ( instance_dict['access_rules_status'] = (
share_instance.get('access_rules_status') share_instance.get('access_rules_status')
) )
@common.ViewBuilder.versioned_method("2.11")
def add_replication_fields(self, instance_dict, share_instance):
instance_dict['replica_state'] = share_instance.get('replica_state')

View File

@ -0,0 +1,75 @@
# Copyright 2015 Goutham Pacha Ravi
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from manila.api import common
class ReplicationViewBuilder(common.ViewBuilder):
"""Model a server API response as a python dictionary."""
_collection_name = 'share_replicas'
_collection_links = 'share_replica_links'
def summary_list(self, request, replicas):
"""Summary view of a list of replicas."""
return self._list_view(self.summary, request, replicas)
def detail_list(self, request, replicas):
"""Detailed view of a list of replicas."""
return self._list_view(self.detail, request, replicas)
def summary(self, request, replica):
"""Generic, non-detailed view of a share replica."""
replica_dict = {
'id': replica.get('id'),
'share_id': replica.get('share_id'),
'status': replica.get('status'),
'replica_state': replica.get('replica_state'),
}
return {'share_replica': replica_dict}
def detail(self, request, replica):
"""Detailed view of a single replica."""
replica_dict = {
'id': replica.get('id'),
'share_id': replica.get('share_id'),
'availability_zone': replica.get('availability_zone'),
'created_at': replica.get('created_at'),
'host': replica.get('host'),
'status': replica.get('status'),
'share_network_id': replica.get('share_network_id'),
'share_server_id': replica.get('share_server_id'),
'replica_state': replica.get('replica_state')
}
return {'share_replica': replica_dict}
def _list_view(self, func, request, replicas):
"""Provide a view for a list of replicas."""
replicas_list = [func(request, replica)['share_replica']
for replica in replicas]
replica_links = self._get_collection_links(request,
replicas,
self._collection_name)
replicas_dict = {self._collection_name: replicas_list}
if replica_links:
replicas_dict[self._collection_links] = replica_links
return replicas_dict

View File

@ -27,6 +27,7 @@ class ViewBuilder(common.ViewBuilder):
"modify_share_type_field", "modify_share_type_field",
"remove_export_locations", "remove_export_locations",
"add_access_rules_status_field", "add_access_rules_status_field",
"add_replication_fields",
] ]
def summary_list(self, request, shares): def summary_list(self, request, shares):
@ -128,6 +129,11 @@ class ViewBuilder(common.ViewBuilder):
def add_access_rules_status_field(self, share_dict, share): def add_access_rules_status_field(self, share_dict, share):
share_dict['access_rules_status'] = share.get('access_rules_status') share_dict['access_rules_status'] = share.get('access_rules_status')
@common.ViewBuilder.versioned_method('2.11')
def add_replication_fields(self, share_dict, share):
share_dict['replication_type'] = share.get('replication_type')
share_dict['has_replicas'] = share['has_replicas']
def _list_view(self, func, request, shares): def _list_view(self, func, request, shares):
"""Provide a view for a list of shares.""" """Provide a view for a list of shares."""
shares_list = [func(request, share)['share'] for share in shares] shares_list = [func(request, share)['share'] for share in shares]

View File

@ -35,6 +35,7 @@ STATUS_SHRINKING_ERROR = 'shrinking_error'
STATUS_SHRINKING_POSSIBLE_DATA_LOSS_ERROR = ( STATUS_SHRINKING_POSSIBLE_DATA_LOSS_ERROR = (
'shrinking_possible_data_loss_error' 'shrinking_possible_data_loss_error'
) )
STATUS_REPLICATION_CHANGE = 'replication_change'
STATUS_TASK_STATE_MIGRATION_STARTING = 'migration_starting' STATUS_TASK_STATE_MIGRATION_STARTING = 'migration_starting'
STATUS_TASK_STATE_MIGRATION_IN_PROGRESS = 'migration_in_progress' STATUS_TASK_STATE_MIGRATION_IN_PROGRESS = 'migration_in_progress'
STATUS_TASK_STATE_MIGRATION_ERROR = 'migration_error' STATUS_TASK_STATE_MIGRATION_ERROR = 'migration_error'
@ -104,12 +105,17 @@ TASK_STATE_STATUSES = (
STATUS_TASK_STATE_MIGRATION_IN_PROGRESS, STATUS_TASK_STATE_MIGRATION_IN_PROGRESS,
) )
REPLICA_STATE_ACTIVE = 'active'
REPLICA_STATE_IN_SYNC = 'in_sync'
REPLICA_STATE_OUT_OF_SYNC = 'out_of_sync'
class ExtraSpecs(object): class ExtraSpecs(object):
# Extra specs key names # Extra specs key names
DRIVER_HANDLES_SHARE_SERVERS = "driver_handles_share_servers" DRIVER_HANDLES_SHARE_SERVERS = "driver_handles_share_servers"
SNAPSHOT_SUPPORT = "snapshot_support" SNAPSHOT_SUPPORT = "snapshot_support"
REPLICATION_TYPE_SPEC = "replication_type"
# Extra specs containers # Extra specs containers
REQUIRED = ( REQUIRED = (
@ -120,9 +126,8 @@ class ExtraSpecs(object):
SNAPSHOT_SUPPORT, SNAPSHOT_SUPPORT,
) )
# NOTE(cknight): Some extra specs are necessary parts of the Manila API and # NOTE(cknight): Some extra specs are necessary parts of the Manila API and
# should be visible to non-admin users. This list matches the UNDELETABLE # should be visible to non-admin users. UNDELETABLE specs are user-visible.
# list today, but that may not always remain true. TENANT_VISIBLE = UNDELETABLE + (REPLICATION_TYPE_SPEC, )
TENANT_VISIBLE = UNDELETABLE
BOOLEAN = ( BOOLEAN = (
DRIVER_HANDLES_SHARE_SERVERS, DRIVER_HANDLES_SHARE_SERVERS,
SNAPSHOT_SUPPORT, SNAPSHOT_SUPPORT,

View File

@ -43,7 +43,6 @@ these objects be simple dictionaries.
from oslo_config import cfg from oslo_config import cfg
from oslo_db import api as db_api from oslo_db import api as db_api
db_opts = [ db_opts = [
cfg.StrOpt('db_backend', cfg.StrOpt('db_backend',
default='sqlalchemy', default='sqlalchemy',
@ -79,6 +78,8 @@ def authorize_quota_class_context(context, class_name):
################### ###################
def service_destroy(context, service_id): def service_destroy(context, service_id):
"""Destroy the service or raise if it does not exist.""" """Destroy the service or raise if it does not exist."""
return IMPL.service_destroy(context, service_id) return IMPL.service_destroy(context, service_id)
@ -415,6 +416,15 @@ def share_access_create(context, values):
return IMPL.share_access_create(context, values) return IMPL.share_access_create(context, values)
def share_instance_access_copy(context, share_id, instance_id):
"""Maps the existing access rules for the share to the instance in the DB.
Adds the instance mapping to the share's access rules and
returns the share's access rules.
"""
return IMPL.share_instance_access_copy(context, share_id, instance_id)
def share_access_get(context, access_id): def share_access_get(context, access_id):
"""Get share access rule.""" """Get share access rule."""
return IMPL.share_access_get(context, access_id) return IMPL.share_access_get(context, access_id)
@ -1028,3 +1038,59 @@ def cgsnapshot_member_update(context, member_id, values):
Raises NotFound if cgsnapshot member does not exist. Raises NotFound if cgsnapshot member does not exist.
""" """
return IMPL.cgsnapshot_member_update(context, member_id, values) return IMPL.cgsnapshot_member_update(context, member_id, values)
####################
def share_replicas_get_all(context, with_share_server=False,
with_share_data=False):
"""Returns all share replicas regardless of share."""
return IMPL.share_replicas_get_all(
context, with_share_server=with_share_server,
with_share_data=with_share_data)
def share_replicas_get_all_by_share(context, share_id, with_share_server=False,
with_share_data=False):
"""Returns all share replicas for a given share."""
return IMPL.share_replicas_get_all_by_share(
context, share_id, with_share_server=with_share_server,
with_share_data=with_share_data)
def share_replicas_get_available_active_replica(context, share_id,
with_share_server=False,
with_share_data=False):
"""Returns an active replica for a given share."""
return IMPL.share_replicas_get_available_active_replica(
context, share_id, with_share_server=with_share_server,
with_share_data=with_share_data)
def share_replicas_get_active_replicas_by_share(context, share_id,
with_share_server=False,
with_share_data=False):
"""Returns all active replicas for a given share."""
return IMPL.share_replicas_get_active_replicas_by_share(
context, share_id, with_share_server=with_share_server,
with_share_data=with_share_data)
def share_replica_get(context, replica_id, with_share_server=False,
with_share_data=False):
"""Get share replica by id."""
return IMPL.share_replica_get(
context, replica_id, with_share_server=with_share_server,
with_share_data=with_share_data)
def share_replica_update(context, share_replica_id, values,
with_share_data=False):
"""Updates a share replica with given values."""
return IMPL.share_replica_update(context, share_replica_id, values,
with_share_data=with_share_data)
def share_replica_delete(context, share_replica_id):
"""Deletes a share replica."""
return IMPL.share_replica_delete(context, share_replica_id)

View File

@ -0,0 +1,41 @@
# Copyright 2015 Goutham Pacha Ravi.
# All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Add replication attributes to Share and ShareInstance models.
Revision ID: 293fac1130ca
Revises: 344c1ac4747f
Create Date: 2015-09-10 15:45:07.273043
"""
# revision identifiers, used by Alembic.
revision = '293fac1130ca'
down_revision = '344c1ac4747f'
from alembic import op
import sqlalchemy as sa
def upgrade():
"""Add replication attributes to Shares and ShareInstances."""
op.add_column('shares', sa.Column('replication_type', sa.String(255)))
op.add_column('share_instances',
sa.Column('replica_state', sa.String(255)))
def downgrade():
"""Remove replication attributes from Shares and ShareInstances."""
op.drop_column('shares', 'replication_type')
op.drop_column('share_instances', 'replica_state')

View File

@ -1292,6 +1292,162 @@ def share_instances_get_all_by_consistency_group_id(context, cg_id):
return instances return instances
################
def _share_replica_get_with_filters(context, share_id=None, replica_id=None,
replica_state=None, status=None,
with_share_server=True, session=None):
query = model_query(context, models.ShareInstance, session=session,
read_deleted="no")
if share_id is not None:
query = query.filter(models.ShareInstance.share_id == share_id)
if replica_id is not None:
query = query.filter(models.ShareInstance.id == replica_id)
if replica_state is not None:
query = query.filter(
models.ShareInstance.replica_state == replica_state)
else:
query = query.filter(models.ShareInstance.replica_state.isnot(None))
if status is not None:
query = query.filter(models.ShareInstance.status == status)
if with_share_server:
query = query.options(joinedload('share_server'))
return query
def _set_replica_share_data(context, replicas, session):
if replicas and not isinstance(replicas, list):
replicas = [replicas]
for replica in replicas:
parent_share = share_get(context, replica['share_id'], session=session)
replica.set_share_data(parent_share)
return replicas
@require_context
def share_replicas_get_all(context, with_share_data=False,
with_share_server=True, session=None):
"""Returns replica instances for all available replicated shares."""
session = session or get_session()
result = _share_replica_get_with_filters(
context, with_share_server=with_share_server, session=session).all()
if with_share_data:
result = _set_replica_share_data(context, result, session)
return result
@require_context
def share_replicas_get_all_by_share(context, share_id,
with_share_data=False,
with_share_server=False, session=None):
"""Returns replica instances for a given share."""
session = session or get_session()
result = _share_replica_get_with_filters(
context, with_share_server=with_share_server,
share_id=share_id, session=session).all()
if with_share_data:
result = _set_replica_share_data(context, result, session)
return result
@require_context
def share_replicas_get_available_active_replica(context, share_id,
with_share_data=False,
with_share_server=False,
session=None):
"""Returns an 'active' replica instance that is 'available'."""
session = session or get_session()
result = _share_replica_get_with_filters(
context, with_share_server=with_share_server, share_id=share_id,
replica_state=constants.REPLICA_STATE_ACTIVE,
status=constants.STATUS_AVAILABLE, session=session).first()
if result and with_share_data:
result = _set_replica_share_data(context, result, session)[0]
return result
@require_context
def share_replicas_get_active_replicas_by_share(context, share_id,
with_share_data=False,
with_share_server=False,
session=None):
"""Returns all active replicas for a given share."""
session = session or get_session()
result = _share_replica_get_with_filters(
context, with_share_server=with_share_server, share_id=share_id,
replica_state=constants.REPLICA_STATE_ACTIVE, session=session).all()
if with_share_data:
result = _set_replica_share_data(context, result, session)
return result
@require_context
def share_replica_get(context, replica_id, with_share_data=False,
with_share_server=False, session=None):
"""Returns summary of requested replica if available."""
session = session or get_session()
result = _share_replica_get_with_filters(
context, with_share_server=with_share_server,
replica_id=replica_id, session=session).first()
if result is None:
raise exception.ShareReplicaNotFound(replica_id=replica_id)
if with_share_data:
result = _set_replica_share_data(context, result, session)[0]
return result
@require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def share_replica_update(context, share_replica_id, values,
with_share_data=False, session=None):
"""Updates a share replica with specified values."""
session = session or get_session()
with session.begin():
ensure_availability_zone_exists(context, values, session, strict=False)
updated_share_replica = _share_instance_update(
context, share_replica_id, values, session=session)
if with_share_data:
updated_share_replica = _set_replica_share_data(
context, updated_share_replica, session)[0]
return updated_share_replica
@require_context
def share_replica_delete(context, share_replica_id, session=None):
"""Deletes a share replica."""
session = session or get_session()
share_instance_delete(context, share_replica_id, session=session)
################ ################
@ -1378,8 +1534,10 @@ def share_update(context, share_id, values):
@require_context @require_context
def share_get(context, share_id, session=None): def share_get(context, share_id, session=None):
result = _share_get_query(context, session).filter_by(id=share_id).first() result = _share_get_query(context, session).filter_by(id=share_id).first()
if result is None: if result is None:
raise exception.NotFound() raise exception.NotFound()
return result return result
@ -1574,6 +1732,23 @@ def share_access_create(context, values):
return share_access_get(context, access_ref['id']) return share_access_get(context, access_ref['id'])
def share_instance_access_copy(context, share_id, instance_id, session=None):
"""Copy access rules from share to share instance."""
session = session or get_session()
share_access_rules = share_access_get_all_for_share(
context, share_id, session=session)
for access_rule in share_access_rules:
values = {
'share_instance_id': instance_id,
'access_id': access_rule['id'],
}
_share_instance_access_create(values, session)
return share_access_rules
def _share_instance_access_create(values, session): def _share_instance_access_create(values, session):
access_ref = models.ShareInstanceAccessMapping() access_ref = models.ShareInstanceAccessMapping()
access_ref.update(ensure_model_dict_has_id(values)) access_ref.update(ensure_model_dict_has_id(values))
@ -1608,8 +1783,8 @@ def share_instance_access_get(context, access_id, instance_id):
@require_context @require_context
def share_access_get_all_for_share(context, share_id): def share_access_get_all_for_share(context, share_id, session=None):
session = get_session() session = session or get_session()
return _share_access_get_query(context, session, return _share_access_get_query(context, session,
{'share_id': share_id}).all() {'share_id': share_id}).all()

View File

@ -209,10 +209,16 @@ class Share(BASE, ManilaBase):
@property @property
def export_locations(self): def export_locations(self):
# TODO(u_glide): Return a map with lists of locations per AZ when # TODO(gouthamr): Return AZ specific export locations for replicated
# replication functionality will be implemented. # shares.
# NOTE(gouthamr): For a replicated share, export locations of the
# 'active' instances are chosen, if 'available'.
all_export_locations = [] all_export_locations = []
for instance in self.instances: select_instances = list(filter(
lambda x: x['replica_state'] == constants.REPLICA_STATE_ACTIVE,
self.instances)) or self.instances
for instance in select_instances:
if instance['status'] == constants.STATUS_AVAILABLE: if instance['status'] == constants.STATUS_AVAILABLE:
for export_location in instance.export_locations: for export_location in instance.export_locations:
all_export_locations.append(export_location['path']) all_export_locations.append(export_location['path'])
@ -238,19 +244,46 @@ class Share(BASE, ManilaBase):
def share_server_id(self): def share_server_id(self):
return self.__getattr__('share_server_id') return self.__getattr__('share_server_id')
@property
def has_replicas(self):
if len(self.instances) > 1:
# NOTE(gouthamr): The 'primary' instance of a replicated share
# has a 'replica_state' set to 'active'. Only the secondary replica
# instances need to be regarded as true 'replicas' by users.
replicas = (list(filter(lambda x: x['replica_state'] is not None,
self.instances)))
return len(replicas) > 1
return False
@property @property
def instance(self): def instance(self):
# NOTE(ganso): We prefer instances with AVAILABLE status, # NOTE(gouthamr): The order of preference: status 'replication_change',
# and we also prefer to show any status other than TRANSITIONAL ones. # followed by 'available' and 'error'. If replicated share and
# not undergoing a 'replication_change', only 'active' instances are
# preferred.
result = None result = None
if len(self.instances) > 0: if len(self.instances) > 0:
for instance in self.instances: order = (constants.STATUS_REPLICATION_CHANGE,
if instance.status == constants.STATUS_AVAILABLE: constants.STATUS_AVAILABLE, constants.STATUS_ERROR)
return instance other_statuses = (
elif instance.status not in constants.TRANSITIONAL_STATUSES: [x['status'] for x in self.instances if
result = instance x['status'] not in order and
if result is None: x['status'] not in constants.TRANSITIONAL_STATUSES]
result = self.instances[0] )
order = (order + tuple(other_statuses) +
constants.TRANSITIONAL_STATUSES)
sorted_instances = sorted(
self.instances, key=lambda x: order.index(x['status']))
select_instances = sorted_instances
if (select_instances[0]['status'] !=
constants.STATUS_REPLICATION_CHANGE):
select_instances = (
list(filter(lambda x: x['replica_state'] ==
constants.REPLICA_STATE_ACTIVE,
sorted_instances)) or sorted_instances
)
result = select_instances[0]
return result return result
@property @property
@ -267,6 +300,7 @@ class Share(BASE, ManilaBase):
display_description = Column(String(255)) display_description = Column(String(255))
snapshot_id = Column(String(36)) snapshot_id = Column(String(36))
snapshot_support = Column(Boolean, default=True) snapshot_support = Column(Boolean, default=True)
replication_type = Column(String(255), nullable=True)
share_proto = Column(String(255)) share_proto = Column(String(255))
share_type_id = Column(String(36), ForeignKey('share_types.id'), share_type_id = Column(String(36), ForeignKey('share_types.id'),
nullable=True) nullable=True)
@ -300,7 +334,8 @@ class Share(BASE, ManilaBase):
class ShareInstance(BASE, ManilaBase): class ShareInstance(BASE, ManilaBase):
__tablename__ = 'share_instances' __tablename__ = 'share_instances'
_extra_keys = ['name', 'export_location', 'availability_zone'] _extra_keys = ['name', 'export_location', 'availability_zone',
'replica_state']
_proxified_properties = ('user_id', 'project_id', 'size', _proxified_properties = ('user_id', 'project_id', 'size',
'display_name', 'display_description', 'display_name', 'display_description',
'snapshot_id', 'share_proto', 'share_type_id', 'snapshot_id', 'share_proto', 'share_type_id',
@ -345,6 +380,7 @@ class ShareInstance(BASE, ManilaBase):
scheduled_at = Column(DateTime) scheduled_at = Column(DateTime)
launched_at = Column(DateTime) launched_at = Column(DateTime)
terminated_at = Column(DateTime) terminated_at = Column(DateTime)
replica_state = Column(String(255), nullable=True)
availability_zone_id = Column(String(36), availability_zone_id = Column(String(36),
ForeignKey('availability_zones.id'), ForeignKey('availability_zones.id'),

View File

@ -718,3 +718,12 @@ class ShareMountException(ManilaException):
class ShareCopyDataException(ManilaException): class ShareCopyDataException(ManilaException):
message = _("Failed to copy data: %(reason)s") message = _("Failed to copy data: %(reason)s")
# Replication
class ReplicationException(ManilaException):
message = _("Unable to perform a replication action: %(reason)s.")
class ShareReplicaNotFound(NotFound):
message = _("Share Replica %(replica_id)s could not be found.")

View File

@ -51,6 +51,16 @@ def share_update_db(context, share_id, host):
return db.share_update(context, share_id, values) return db.share_update(context, share_id, values)
def share_replica_update_db(context, share_replica_id, host):
"""Set the host and the scheduled_at field of a share replica.
:returns: A Share Replica with the updated fields set.
"""
now = timeutils.utcnow()
values = {'host': host, 'scheduled_at': now}
return db.share_replica_update(context, share_replica_id, values)
def cg_update_db(context, cg_id, host): def cg_update_db(context, cg_id, host):
'''Set the host and set the updated_at field of a consistency group. '''Set the host and set the updated_at field of a consistency group.
@ -114,3 +124,8 @@ class Scheduler(object):
filter_properties): filter_properties):
"""Must override schedule method for migration to work.""" """Must override schedule method for migration to work."""
raise NotImplementedError(_("Must implement host_passes_filters")) raise NotImplementedError(_("Must implement host_passes_filters"))
def schedule_create_replica(self, context, request_spec,
filter_properties):
"""Must override schedule method for create replica to work."""
raise NotImplementedError(_("Must implement schedule_create_replica"))

View File

@ -105,6 +105,32 @@ class FilterScheduler(base.Scheduler):
snapshot_id=snapshot_id snapshot_id=snapshot_id
) )
def schedule_create_replica(self, context, request_spec,
filter_properties):
share_replica_id = request_spec['share_instance_properties'].get('id')
weighed_host = self._schedule_share(
context, request_spec, filter_properties)
if not weighed_host:
msg = _('Failed to find a weighted host for scheduling share '
'replica %s.')
raise exception.NoValidHost(reason=msg % share_replica_id)
host = weighed_host.obj.host
updated_share_replica = base.share_replica_update_db(
context, share_replica_id, host)
self._post_select_populate_filter_properties(filter_properties,
weighed_host.obj)
# context is not serializable
filter_properties.pop('context', None)
self.share_rpcapi.create_share_replica(
context, updated_share_replica, host, request_spec=request_spec,
filter_properties=filter_properties)
def _format_filter_properties(self, context, filter_properties, def _format_filter_properties(self, context, filter_properties,
request_spec): request_spec):

View File

@ -127,6 +127,7 @@ class HostState(object):
self.consistency_group_support = False self.consistency_group_support = False
self.dedupe = False self.dedupe = False
self.compression = False self.compression = False
self.replication_type = None
# PoolState for all pools # PoolState for all pools
self.pools = {} self.pools = {}
@ -292,6 +293,9 @@ class HostState(object):
if 'compression' not in pool_cap: if 'compression' not in pool_cap:
pool_cap['compression'] = self.compression pool_cap['compression'] = self.compression
if not pool_cap.get('replication_type'):
pool_cap['replication_type'] = self.replication_type
def update_backend(self, capability): def update_backend(self, capability):
self.share_backend_name = capability.get('share_backend_name') self.share_backend_name = capability.get('share_backend_name')
self.vendor_name = capability.get('vendor_name') self.vendor_name = capability.get('vendor_name')
@ -303,6 +307,7 @@ class HostState(object):
self.consistency_group_support = capability.get( self.consistency_group_support = capability.get(
'consistency_group_support', False) 'consistency_group_support', False)
self.updated = capability['timestamp'] self.updated = capability['timestamp']
self.replication_type = capability.get('replication_type')
def consume_from_share(self, share): def consume_from_share(self, share):
"""Incrementally update host state from an share.""" """Incrementally update host state from an share."""
@ -365,6 +370,8 @@ class PoolState(HostState):
'dedupe', False) 'dedupe', False)
self.compression = capability.get( self.compression = capability.get(
'compression', False) 'compression', False)
self.replication_type = capability.get(
'replication_type', self.replication_type)
def update_pools(self, capability): def update_pools(self, capability):
# Do nothing, since we don't have pools within pool, yet # Do nothing, since we don't have pools within pool, yet

View File

@ -59,7 +59,7 @@ MAPPING = {
class SchedulerManager(manager.Manager): class SchedulerManager(manager.Manager):
"""Chooses a host to create shares.""" """Chooses a host to create shares."""
RPC_API_VERSION = '1.4' RPC_API_VERSION = '1.5'
def __init__(self, scheduler_driver=None, service_name=None, def __init__(self, scheduler_driver=None, service_name=None,
*args, **kwargs): *args, **kwargs):
@ -206,3 +206,21 @@ class SchedulerManager(manager.Manager):
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
self._set_cg_error_state('create_consistency_group', self._set_cg_error_state('create_consistency_group',
context, ex, request_spec) context, ex, request_spec)
def create_share_replica(self, context, request_spec=None,
filter_properties=None):
try:
self.driver.schedule_create_replica(context, request_spec,
filter_properties)
except Exception as ex:
with excutils.save_and_reraise_exception():
msg = _LW("Failed to schedule the new share replica: %s")
LOG.warning(msg % ex)
db.share_replica_update(
context,
request_spec.get('share_instance_properties').get('id'),
{'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})

View File

@ -36,15 +36,16 @@ class SchedulerAPI(object):
Replace create_share() - > create_share_instance() Replace create_share() - > create_share_instance()
1.3 - Add create_consistency_group method 1.3 - Add create_consistency_group method
1.4 - Add migrate_share_to_host method 1.4 - Add migrate_share_to_host method
1.5 - Add create_share_replica
""" """
RPC_API_VERSION = '1.4' RPC_API_VERSION = '1.5'
def __init__(self): def __init__(self):
super(SchedulerAPI, self).__init__() super(SchedulerAPI, self).__init__()
target = messaging.Target(topic=CONF.scheduler_topic, target = messaging.Target(topic=CONF.scheduler_topic,
version=self.RPC_API_VERSION) version=self.RPC_API_VERSION)
self.client = rpc.get_client(target, version_cap='1.4') self.client = rpc.get_client(target, version_cap='1.5')
def create_share_instance(self, ctxt, request_spec=None, def create_share_instance(self, ctxt, request_spec=None,
filter_properties=None): filter_properties=None):
@ -98,3 +99,14 @@ class SchedulerAPI(object):
force_host_copy=force_host_copy, force_host_copy=force_host_copy,
request_spec=request_spec_p, request_spec=request_spec_p,
filter_properties=filter_properties) filter_properties=filter_properties)
def create_share_replica(self, ctxt, request_spec=None,
filter_properties=None):
request_spec_p = jsonutils.to_primitive(request_spec)
cctxt = self.client.prepare(version='1.5')
return cctxt.cast(
ctxt,
'create_share_replica',
request_spec=request_spec_p,
filter_properties=filter_properties,
)

View File

@ -170,6 +170,8 @@ class API(base.Base):
share_type.get('extra_specs', {}).get( share_type.get('extra_specs', {}).get(
'snapshot_support', True) if share_type else True, 'snapshot_support', True) if share_type else True,
strict=True) strict=True)
replication_type = share_type.get('extra_specs', {}).get(
'replication_type') if share_type else None
except ValueError as e: except ValueError as e:
raise exception.InvalidParameterValue(six.text_type(e)) raise exception.InvalidParameterValue(six.text_type(e))
@ -221,6 +223,7 @@ class API(base.Base):
'project_id': context.project_id, 'project_id': context.project_id,
'snapshot_id': snapshot_id, 'snapshot_id': snapshot_id,
'snapshot_support': snapshot_support, 'snapshot_support': snapshot_support,
'replication_type': replication_type,
'metadata': metadata, 'metadata': metadata,
'display_name': name, 'display_name': name,
'display_description': description, 'display_description': description,
@ -264,25 +267,11 @@ class API(base.Base):
consistency_group=None, cgsnapshot_member=None): consistency_group=None, cgsnapshot_member=None):
policy.check_policy(context, 'share', 'create') policy.check_policy(context, 'share', 'create')
availability_zone_id = None request_spec, share_instance = (
if availability_zone: self._create_share_instance_and_get_request_spec(
availability_zone_id = self.db.availability_zone_get( context, share, availability_zone=availability_zone,
context, availability_zone).id consistency_group=consistency_group, host=host,
share_network_id=share_network_id))
# TODO(u_glide): Add here validation that provided share network
# doesn't conflict with provided availability_zone when Neutron
# will have AZ support.
share_instance = self.db.share_instance_create(
context, share['id'],
{
'share_network_id': share_network_id,
'status': constants.STATUS_CREATING,
'scheduled_at': timeutils.utcnow(),
'host': host if host else '',
'availability_zone_id': availability_zone_id,
}
)
if cgsnapshot_member: if cgsnapshot_member:
host = cgsnapshot_member['share']['host'] host = cgsnapshot_member['share']['host']
@ -292,45 +281,6 @@ class API(base.Base):
# NOTE(ameade): Do not cast to driver if creating from cgsnapshot # NOTE(ameade): Do not cast to driver if creating from cgsnapshot
return return
share_properties = {
'size': share['size'],
'user_id': share['user_id'],
'project_id': share['project_id'],
'metadata': self.db.share_metadata_get(context, share['id']),
'share_server_id': share['share_server_id'],
'snapshot_support': share['snapshot_support'],
'share_proto': share['share_proto'],
'share_type_id': share['share_type_id'],
'is_public': share['is_public'],
'consistency_group_id': share['consistency_group_id'],
'source_cgsnapshot_member_id': share[
'source_cgsnapshot_member_id'],
'snapshot_id': share['snapshot_id'],
}
share_instance_properties = {
'availability_zone_id': share_instance['availability_zone_id'],
'share_network_id': share_instance['share_network_id'],
'share_server_id': share_instance['share_server_id'],
'share_id': share_instance['share_id'],
'host': share_instance['host'],
'status': share_instance['status'],
}
share_type = None
if share['share_type_id']:
share_type = self.db.share_type_get(
context, share['share_type_id'])
request_spec = {
'share_properties': share_properties,
'share_instance_properties': share_instance_properties,
'share_proto': share['share_proto'],
'share_id': share['id'],
'snapshot_id': share['snapshot_id'],
'share_type': share_type,
'consistency_group': consistency_group,
}
if host: if host:
self.share_rpcapi.create_share_instance( self.share_rpcapi.create_share_instance(
context, context,
@ -348,6 +298,168 @@ class API(base.Base):
return share_instance return share_instance
def _create_share_instance_and_get_request_spec(
self, context, share, availability_zone=None,
consistency_group=None, host=None, share_network_id=None):
availability_zone_id = None
if availability_zone:
availability_zone_id = self.db.availability_zone_get(
context, availability_zone).id
# TODO(u_glide): Add here validation that provided share network
# doesn't conflict with provided availability_zone when Neutron
# will have AZ support.
share_instance = self.db.share_instance_create(
context, share['id'],
{
'share_network_id': share_network_id,
'status': constants.STATUS_CREATING,
'scheduled_at': timeutils.utcnow(),
'host': host if host else '',
'availability_zone_id': availability_zone_id,
}
)
share_properties = {
'id': share['id'],
'size': share['size'],
'user_id': share['user_id'],
'project_id': share['project_id'],
'metadata': self.db.share_metadata_get(context, share['id']),
'share_server_id': share['share_server_id'],
'snapshot_support': share['snapshot_support'],
'share_proto': share['share_proto'],
'share_type_id': share['share_type_id'],
'is_public': share['is_public'],
'consistency_group_id': share['consistency_group_id'],
'source_cgsnapshot_member_id': share[
'source_cgsnapshot_member_id'],
'snapshot_id': share['snapshot_id'],
'replication_type': share['replication_type'],
}
share_instance_properties = {
'id': share_instance['id'],
'availability_zone_id': share_instance['availability_zone_id'],
'share_network_id': share_instance['share_network_id'],
'share_server_id': share_instance['share_server_id'],
'share_id': share_instance['share_id'],
'host': share_instance['host'],
'status': share_instance['status'],
'replica_state': share_instance['replica_state'],
}
share_type = None
if share['share_type_id']:
share_type = self.db.share_type_get(
context, share['share_type_id'])
request_spec = {
'share_properties': share_properties,
'share_instance_properties': share_instance_properties,
'share_proto': share['share_proto'],
'share_id': share['id'],
'snapshot_id': share['snapshot_id'],
'share_type': share_type,
'consistency_group': consistency_group,
'availability_zone_id': availability_zone_id,
}
return request_spec, share_instance
def create_share_replica(self, context, share, availability_zone=None,
share_network_id=None):
if not share.get('replication_type'):
msg = _("Replication not supported for share %s.")
raise exception.InvalidShare(message=msg % share['id'])
self._check_is_share_busy(share)
if not self.db.share_replicas_get_available_active_replica(
context, share['id']):
msg = _("Share %s does not have any active replica in available "
"state.")
raise exception.ReplicationException(reason=msg % share['id'])
request_spec, share_replica = (
self._create_share_instance_and_get_request_spec(
context, share, availability_zone=availability_zone,
share_network_id=share_network_id))
self.db.share_replica_update(
context, share_replica['id'],
{'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC})
self.scheduler_rpcapi.create_share_replica(
context, request_spec=request_spec, filter_properties={})
return share_replica
def delete_share_replica(self, context, share_replica, force=False):
# Disallow deletion of ONLY active replica
replicas = self.db.share_replicas_get_all_by_share(
context, share_replica['share_id'])
active_replicas = list(filter(
lambda x: x['replica_state'] == constants.REPLICA_STATE_ACTIVE,
replicas))
if (share_replica.get('replica_state') ==
constants.REPLICA_STATE_ACTIVE and len(active_replicas) == 1):
msg = _("Cannot delete last active replica.")
raise exception.ReplicationException(reason=msg)
LOG.info(_LI("Deleting replica %s."), id)
if not share_replica['host']:
self.db.share_replica_update(context, share_replica['id'],
{'terminated_at': timeutils.utcnow()})
self.db.share_replica_delete(context, share_replica['id'])
else:
host = share_utils.extract_host(share_replica['host'])
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_DELETING,
'terminated_at': timeutils.utcnow()}
)
self.share_rpcapi.delete_share_replica(
context,
share_replica['id'],
host,
share_id=share_replica['share_id'],
force=force)
def promote_share_replica(self, context, share_replica):
if share_replica.get('status') != constants.STATUS_AVAILABLE:
msg = _("Replica %(replica_id)s must be in %(status)s state to be "
"promoted.")
raise exception.ReplicationException(
reason=msg % {'replica_id': share_replica['id'],
'status': constants.STATUS_AVAILABLE})
replica_state = share_replica['replica_state']
if (replica_state in (constants.REPLICA_STATE_OUT_OF_SYNC,
constants.STATUS_ERROR)
and not context.is_admin):
msg = _("Promoting a replica with 'replica_state': %s requires "
"administrator privileges.")
raise exception.AdminRequired(
message=msg % replica_state)
host = share_utils.extract_host(share_replica['host'])
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_REPLICATION_CHANGE})
self.share_rpcapi.promote_share_replica(
context, share_replica['id'], host,
share_id=share_replica['share_id'])
return self.db.share_replica_get(context, share_replica['id'])
def manage(self, context, share_data, driver_options): def manage(self, context, share_data, driver_options):
policy.check_policy(context, 'share', 'manage') policy.check_policy(context, 'share', 'manage')
@ -419,6 +531,13 @@ class API(base.Base):
"statuses": statuses} "statuses": statuses}
raise exception.InvalidShare(reason=msg) raise exception.InvalidShare(reason=msg)
# NOTE(gouthamr): If the share has more than one replica,
# it can't be deleted until the additional replicas are removed.
if share.has_replicas:
msg = _("Share %s has replicas. Remove the replicas before "
"deleting the share.") % share_id
raise exception.Conflict(err=msg)
snapshots = self.db.share_snapshot_get_all_for_share(context, share_id) snapshots = self.db.share_snapshot_get_all_for_share(context, share_id)
if len(snapshots): if len(snapshots):
msg = _("Share still has %d dependent snapshots") % len(snapshots) msg = _("Share still has %d dependent snapshots") % len(snapshots)
@ -580,6 +699,14 @@ class API(base.Base):
share_instance = share.instance share_instance = share.instance
# NOTE(gouthamr): Ensure share does not have replicas.
# Currently share migrations are disallowed for replicated shares.
if share.has_replicas:
msg = _('Share %s has replicas. Remove the replicas before '
'attempting to migrate the share.') % share['id']
LOG.error(msg)
raise exception.Conflict(err=msg)
# We only handle "available" share for now # We only handle "available" share for now
if share_instance['status'] != constants.STATUS_AVAILABLE: if share_instance['status'] != constants.STATUS_AVAILABLE:
msg = _('Share instance %(instance_id)s status must be available, ' msg = _('Share instance %(instance_id)s status must be available, '

View File

@ -618,6 +618,20 @@ class ShareDriver(object):
should be added/deleted. Driver can ignore rules in 'access_rules' and should be added/deleted. Driver can ignore rules in 'access_rules' and
apply only rules from 'add_rules' and 'delete_rules'. apply only rules from 'add_rules' and 'delete_rules'.
Drivers must be mindful of this call for share replicas. When
'update_access' is called on one of the replicas, the call is likely
propagated to all replicas belonging to the share, especially when
individual rules are added or removed. If a particular access rule
does not make sense to the driver in the context of a given replica,
the driver should be careful to report a correct behavior, and take
meaningful action. For example, if R/W access is requested on a
replica that is part of a "readable" type replication; R/O access
may be added by the driver instead of R/W. Note that raising an
exception *will* result in the access_rules_status on the replica,
and the share itself being "out_of_sync". Drivers can sync on the
valid access rules that are provided on the create_replica and
promote_replica calls.
:param context: Current context :param context: Current context
:param share: Share model with share data. :param share: Share model with share data.
:param access_rules: All access rules for given share :param access_rules: All access rules for given share
@ -1096,3 +1110,290 @@ class ShareDriver(object):
:return: list of share instances. :return: list of share instances.
""" """
return share_instances return share_instances
def create_replica(self, context, active_replica, new_replica,
access_rules, share_server=None):
"""Replicate the active replica to a new replica on this backend.
:param context: Current context
:param active_replica: A current active replica instance dictionary.
EXAMPLE:
.. code::
{
'id': 'd487b88d-e428-4230-a465-a800c2cce5f8',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'deleted': False,
'host': 'openstack2@cmodeSSVMNFS1',
'status': 'available',
'scheduled_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'launched_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'terminated_at': None,
'replica_state': 'active',
'availability_zone_id': 'e2c2db5c-cb2f-4697-9966-c06fb200cb80',
'export_locations': [
<models.ShareInstanceExportLocations>,
],
'access_rules_status': 'in_sync',
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'share_server_id': '4ce78e7b-0ef6-4730-ac2a-fd2defefbd05',
'share_server': <models.ShareServer> or None,
}
:param new_replica: The share replica dictionary.
EXAMPLE:
.. code::
{
'id': 'e82ff8b6-65f0-11e5-9d70-feff819cdc9f',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'deleted': False,
'host': 'openstack2@cmodeSSVMNFS2',
'status': 'available',
'scheduled_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'launched_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'terminated_at': None,
'replica_state': 'out_of_sync',
'availability_zone_id': 'f6e146d0-65f0-11e5-9d70-feff819cdc9f',
'export_locations': [
models.ShareInstanceExportLocations,
],
'access_rules_status': 'out_of_sync',
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'share_server_id': 'e6155221-ea00-49ef-abf9-9f89b7dd900a',
'share_server': <models.ShareServer> or None,
}
:param access_rules: A list of access rules that other instances of
the share already obey. Drivers are expected to apply access rules
to the new replica or disregard access rules that don't apply.
EXAMPLE:
.. code::
[ {
'id': 'f0875f6f-766b-4865-8b41-cccb4cdf1676',
'deleted' = False,
'share_id' = 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'access_type' = 'ip',
'access_to' = '172.16.20.1',
'access_level' = 'rw',
}]
:param share_server: <models.ShareServer> or None,
Share server of the replica being created.
:return: None or a dictionary containing export_locations,
replica_state and access_rules_status. export_locations is a list of
paths and replica_state is one of active, in_sync, out_of_sync or
error. A backend supporting 'writable' type replication should return
'active' as the replica_state. Export locations should be in the
same format as returned during the create_share call.
EXAMPLE:
.. code::
{
'export_locations': [
{
'path': '172.16.20.22/sample/export/path',
'is_admin_only': False,
'metadata': {'some_key': 'some_value'},
},
],
'replica_state': 'in_sync',
'access_rules_status': 'in_sync',
}
"""
raise NotImplementedError()
def delete_replica(self, context, active_replica, replica,
share_server=None):
"""Delete a replica. This is called on the destination backend.
:param context: Current context
:param active_replica: A current active replica instance dictionary.
EXAMPLE:
.. code::
{
'id': 'd487b88d-e428-4230-a465-a800c2cce5f8',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'deleted': False,
'host': 'openstack2@cmodeSSVMNFS1',
'status': 'available',
'scheduled_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'launched_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'terminated_at': None,
'replica_state': 'active',
'availability_zone_id': 'e2c2db5c-cb2f-4697-9966-c06fb200cb80',
'export_locations': [
models.ShareInstanceExportLocations,
],
'access_rules_status': 'in_sync',
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'share_server_id': '4ce78e7b-0ef6-4730-ac2a-fd2defefbd05',
'share_server': <models.ShareServer> or None,
}
:param replica: Dictionary of the share replica being deleted.
EXAMPLE:
.. code::
{
'id': 'e82ff8b6-65f0-11e5-9d70-feff819cdc9f',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'deleted': False,
'host': 'openstack2@cmodeSSVMNFS2',
'status': 'available',
'scheduled_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'launched_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'terminated_at': None,
'replica_state': 'in_sync',
'availability_zone_id': 'f6e146d0-65f0-11e5-9d70-feff819cdc9f',
'export_locations': [
models.ShareInstanceExportLocations
],
'access_rules_status': 'out_of_sync',
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'share_server_id': '53099868-65f1-11e5-9d70-feff819cdc9f',
'share_server': <models.ShareServer> or None,
}
:param share_server: <models.ShareServer> or None,
Share server of the replica to be deleted.
:return: None.
"""
raise NotImplementedError()
def promote_replica(self, context, replica_list, replica, access_rules,
share_server=None):
"""Promote a replica to 'active' replica state.
:param context: Current context
:param replica_list: List of all replicas for a particular share.
This list also contains the replica to be promoted. The 'active'
replica will have its 'replica_state' attr set to 'active'.
EXAMPLE:
.. code::
[
{
'id': 'd487b88d-e428-4230-a465-a800c2cce5f8',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'replica_state': 'in_sync',
...
'share_server_id': '4ce78e7b-0ef6-4730-ac2a-fd2defefbd05',
'share_server': <models.ShareServer> or None,
},
{
'id': '10e49c3e-aca9-483b-8c2d-1c337b38d6af',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'replica_state': 'active',
...
'share_server_id': 'f63629b3-e126-4448-bec2-03f788f76094',
'share_server': <models.ShareServer> or None,
},
{
'id': 'e82ff8b6-65f0-11e5-9d70-feff819cdc9f',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'replica_state': 'in_sync',
...
'share_server_id': '07574742-67ea-4dfd-9844-9fbd8ada3d87',
'share_server': <models.ShareServer> or None,
},
...
]
:param replica: Dictionary of the replica to be promoted.
EXAMPLE:
.. code::
{
'id': 'e82ff8b6-65f0-11e5-9d70-feff819cdc9f',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'deleted': False,
'host': 'openstack2@cmodeSSVMNFS2',
'status': 'available',
'scheduled_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'launched_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'terminated_at': None,
'replica_state': 'in_sync',
'availability_zone_id': 'f6e146d0-65f0-11e5-9d70-feff819cdc9f',
'export_locations': [
models.ShareInstanceExportLocations
],
'access_rules_status': 'in_sync',
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'share_server_id': '07574742-67ea-4dfd-9844-9fbd8ada3d87',
'share_server': <models.ShareServer> or None,
}
:param access_rules: A list of access rules that other instances of
the share already obey.
EXAMPLE:
.. code::
[ {
'id': 'f0875f6f-766b-4865-8b41-cccb4cdf1676',
'deleted' = False,
'share_id' = 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'access_type' = 'ip',
'access_to' = '172.16.20.1',
'access_level' = 'rw',
}]
:param share_server: <models.ShareServer> or None,
Share server of the replica to be promoted.
:return: updated_replica_list or None
The driver can return the updated list as in the request
parameter. Changes that will be updated to the Database are:
'export_locations', 'access_rules_status' and 'replica_state'.
:raises Exception
This can be any exception derived from BaseException. This is
re-raised by the manager after some necessary cleanup. If the
driver raises an exception during promotion, it is assumed
that all of the replicas of the share are in an inconsistent
state. Recovery is only possible through the periodic update
call and/or administrator intervention to correct the 'status'
of the affected replicas if they become healthy again.
"""
raise NotImplementedError()
def update_replica_state(self, context, replica,
access_rules, share_server=None):
"""Update the replica_state of a replica.
Drivers should fix replication relationships that were broken if
possible inside this method.
:param context: Current context
:param replica: Dictionary of the replica being updated.
EXAMPLE:
.. code::
{
'id': 'd487b88d-e428-4230-a465-a800c2cce5f8',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'deleted': False,
'host': 'openstack2@cmodeSSVMNFS1',
'status': 'available',
'scheduled_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'launched_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'terminated_at': None,
'replica_state': 'active',
'availability_zone_id': 'e2c2db5c-cb2f-4697-9966-c06fb200cb80',
'export_locations': [
models.ShareInstanceExportLocations,
],
'access_rules_status': 'in_sync',
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'share_server_id': '4ce78e7b-0ef6-4730-ac2a-fd2defefbd05',
}
:param access_rules: A list of access rules that other replicas of
the share already obey. The driver could attempt to sync on any
un-applied access_rules.
EXAMPLE:
.. code::
[ {
'id': 'f0875f6f-766b-4865-8b41-cccb4cdf1676',
'deleted' = False,
'share_id' = 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'access_type' = 'ip',
'access_to' = '172.16.20.1',
'access_level' = 'rw',
}]
:param share_server: <models.ShareServer> or None
:return: replica_state
replica_state - a str value denoting the replica_state that the
replica can have. Valid values are 'in_sync' and 'out_of_sync'
or None (to leave the current replica_state unchanged).
"""
raise NotImplementedError()

View File

@ -89,6 +89,11 @@ share_manager_opts = [
'will wait for a share server to go unutilized before ' 'will wait for a share server to go unutilized before '
'deleting it.', 'deleting it.',
deprecated_group='DEFAULT'), deprecated_group='DEFAULT'),
cfg.IntOpt('replica_state_update_interval',
default=300,
help='This value, specified in seconds, determines how often '
'the share manager will poll for the health '
'(replica_state) of each replica instance.'),
] ]
CONF = cfg.CONF CONF = cfg.CONF
@ -107,6 +112,28 @@ MAPPING = {
QUOTAS = quota.QUOTAS QUOTAS = quota.QUOTAS
def locked_share_replica_operation(operation):
"""Lock decorator for share replica operations.
Takes a named lock prior to executing the operation. The lock is named with
the id of the share to which the replica belongs.
Intended use:
If a replica operation uses this decorator, it will block actions on
all share replicas of the share until the named lock is free. This is
used to protect concurrent operations on replicas of the same share e.g.
promote ReplicaA while deleting ReplicaB, both belonging to the same share.
"""
def wrapped(instance, context, share_replica_id, share_id=None, **kwargs):
@utils.synchronized("%s" % share_id, external=True)
def locked_operation(*_args, **_kwargs):
return operation(*_args, **_kwargs)
return locked_operation(instance, context, share_replica_id,
share_id=share_id, **kwargs)
return wrapped
def add_hooks(f): def add_hooks(f):
def wrapped(self, *args, **kwargs): def wrapped(self, *args, **kwargs):
@ -137,7 +164,7 @@ def add_hooks(f):
class ShareManager(manager.SchedulerDependentManager): class ShareManager(manager.SchedulerDependentManager):
"""Manages NAS storages.""" """Manages NAS storages."""
RPC_API_VERSION = '1.7' RPC_API_VERSION = '1.8'
def __init__(self, share_driver=None, service_name=None, *args, **kwargs): def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
"""Load the driver from args, or from flags.""" """Load the driver from args, or from flags."""
@ -812,9 +839,376 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_instance_update( self.db.share_instance_update(
context, share_instance_id, context, share_instance_id,
{'status': constants.STATUS_AVAILABLE, {'status': constants.STATUS_AVAILABLE,
'launched_at': timeutils.utcnow()} 'launched_at': timeutils.utcnow()})
share = self.db.share_get(context, share_instance['share_id'])
if share.get('replication_type'):
self.db.share_replica_update(
context, share_instance_id,
{'replica_state': constants.REPLICA_STATE_ACTIVE})
def _update_share_replica_access_rules_state(self, context,
share_replica_id, state):
"""Update the access_rules_status for the share replica."""
self.db.share_instance_update_access_status(
context, share_replica_id, state)
@add_hooks
@utils.require_driver_initialized
@locked_share_replica_operation
def create_share_replica(self, context, share_replica_id, share_id=None,
request_spec=None, filter_properties=None):
"""Create a share replica."""
context = context.elevated()
share_replica = self.db.share_replica_get(
context, share_replica_id, with_share_data=True,
with_share_server=True)
if not share_replica['availability_zone']:
share_replica = self.db.share_replica_update(
context, share_replica['id'],
{'availability_zone': CONF.storage_availability_zone},
with_share_data=True
) )
current_active_replica = (
self.db.share_replicas_get_available_active_replica(
context, share_replica['share_id'], with_share_data=True,
with_share_server=True))
if not current_active_replica:
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})
msg = _("An active instance with 'available' status does "
"not exist to add replica to share %s.")
raise exception.ReplicationException(
reason=msg % share_replica['share_id'])
# We need the share_network_id in case of
# driver_handles_share_server=True
share_network_id = share_replica.get('share_network_id', None)
if (share_network_id and
not self.driver.driver_handles_share_servers):
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})
raise exception.InvalidDriverMode(
"Driver does not expect share-network to be provided "
"with current configuration.")
if share_network_id:
try:
share_server, share_replica = (
self._provide_share_server_for_share(
context, share_network_id, share_replica)
)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to get share server "
"for share replica creation."))
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})
else:
share_server = None
# Map the existing access rules for the share to
# the replica in the DB.
share_access_rules = self.db.share_instance_access_copy(
context, share_replica['share_id'], share_replica['id'])
current_active_replica = self._get_share_replica_dict(
context, current_active_replica)
share_replica = self._get_share_replica_dict(context, share_replica)
try:
replica_ref = self.driver.create_replica(
context, current_active_replica, share_replica,
share_access_rules, share_server=share_server)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Share replica %s failed on creation."),
share_replica['id'])
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})
self._update_share_replica_access_rules_state(
context, share_replica['id'], constants.STATUS_ERROR)
if replica_ref.get('export_locations'):
if isinstance(replica_ref.get('export_locations'), list):
self.db.share_export_locations_update(
context, share_replica['id'],
replica_ref.get('export_locations'))
else:
msg = _LW('Invalid export locations passed to the share '
'manager.')
LOG.warning(msg)
if replica_ref.get('replica_state'):
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_AVAILABLE,
'replica_state': replica_ref.get('replica_state')})
if replica_ref.get('access_rules_status'):
self._update_share_replica_access_rules_state(
context, share_replica['id'],
replica_ref.get('access_rules_status'))
else:
self._update_share_replica_access_rules_state(
context, share_replica['id'], constants.STATUS_ACTIVE)
LOG.info(_LI("Share replica %s created successfully."),
share_replica['id'])
@add_hooks
@utils.require_driver_initialized
@locked_share_replica_operation
def delete_share_replica(self, context, share_replica_id, share_id=None,
force=False):
"""Delete a share replica."""
context = context.elevated()
share_replica = self.db.share_replica_get(
context, share_replica_id, with_share_data=True,
with_share_server=True)
# Get the active replica
current_active_replica = (
self.db.share_replicas_get_available_active_replica(
context, share_replica['share_id'],
with_share_data=True, with_share_server=True)
)
share_server = self._get_share_server(context, share_replica)
current_active_replica = self._get_share_replica_dict(
context, current_active_replica)
share_replica = self._get_share_replica_dict(context, share_replica)
try:
self.access_helper.update_access_rules(
context,
share_replica_id,
delete_rules="all",
share_server=share_server
)
except Exception:
with excutils.save_and_reraise_exception() as exc_context:
# Set status to 'error' from 'deleting' since
# access_rules_status has been set to 'error'.
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_ERROR})
if force:
msg = _("The driver was unable to delete access rules "
"for the replica: %s. Will attempt to delete the "
"replica anyway.")
LOG.error(msg % share_replica['id'])
exc_context.reraise = False
try:
self.driver.delete_replica(
context, current_active_replica, share_replica,
share_server=share_server)
except Exception:
with excutils.save_and_reraise_exception() as exc_context:
if force:
msg = _("The driver was unable to delete the share "
"replica: %s on the backend. Since this "
"operation is forced, the replica will be "
"deleted from Manila's database. A cleanup on "
"the backend may be necessary.")
LOG.error(msg % share_replica['id'])
exc_context.reraise = False
else:
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_ERROR_DELETING,
'replica_state': constants.STATUS_ERROR})
self.db.share_replica_delete(context, share_replica['id'])
LOG.info(_LI("Share replica %s deleted successfully."),
share_replica['id'])
@add_hooks
@utils.require_driver_initialized
@locked_share_replica_operation
def promote_share_replica(self, context, share_replica_id, share_id=None):
"""Promote a share replica to active state."""
context = context.elevated()
share_replica = self.db.share_replica_get(
context, share_replica_id, with_share_data=True,
with_share_server=True)
share_server = self._get_share_server(context, share_replica)
# Get list of all replicas for share
replica_list = (
self.db.share_replicas_get_all_by_share(
context, share_replica['share_id'],
with_share_data=True, with_share_server=True)
)
try:
old_active_replica = list(filter(
lambda r: (
r['replica_state'] == constants.REPLICA_STATE_ACTIVE),
replica_list))[0]
except IndexError:
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_AVAILABLE})
msg = _("Share %(share)s has no replica with 'replica_state' "
"set to %(state)s. Promoting %(replica)s is not "
"possible.")
raise exception.ReplicationException(
reason=msg % {'share': share_replica['share_id'],
'state': constants.REPLICA_STATE_ACTIVE,
'replica': share_replica['id']})
access_rules = self.db.share_access_get_all_for_share(
context, share_replica['share_id'])
replica_list = [self._get_share_replica_dict(context, r)
for r in replica_list]
share_replica = self._get_share_replica_dict(context, share_replica)
try:
updated_replica_list = (
self.driver.promote_replica(
context, replica_list, share_replica, access_rules,
share_server=share_server)
)
except Exception:
with excutils.save_and_reraise_exception():
# (NOTE) gouthamr: If the driver throws an exception at
# this stage, there is a good chance that the replicas are
# somehow altered on the backend. We loop through the
# replicas and set their 'status's to 'error' and
# leave the 'replica_state' unchanged. This also changes the
# 'status' of the replica that failed to promote to 'error' as
# before this operation. The backend may choose to update
# the actual replica_state during the replica_monitoring
# stage.
updates = {'status': constants.STATUS_ERROR}
for replica_ref in replica_list:
self.db.share_replica_update(
context, replica_ref['id'], updates)
if not updated_replica_list:
self.db.share_replica_update(
context, share_replica['id'],
{'status': constants.STATUS_AVAILABLE,
'replica_state': constants.REPLICA_STATE_ACTIVE})
self.db.share_replica_update(
context, old_active_replica['id'],
{'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC})
else:
for updated_replica in updated_replica_list:
updated_export_locs = updated_replica.get(
'export_locations')
if(updated_export_locs and
isinstance(updated_export_locs, list)):
self.db.share_export_locations_update(
context, updated_replica['id'],
updated_export_locs)
updated_replica_state = updated_replica.get(
'replica_state')
updates = {'replica_state': updated_replica_state}
# Change the promoted replica's status from 'available' to
# 'replication_change'.
if updated_replica['id'] == share_replica['id']:
updates['status'] = constants.STATUS_AVAILABLE
if updated_replica_state == constants.STATUS_ERROR:
updates['status'] = constants.STATUS_ERROR
self.db.share_replica_update(
context, updated_replica['id'], updates)
if updated_replica.get('access_rules_status'):
self._update_share_replica_access_rules_state(
context, share_replica['id'],
updated_replica.get('access_rules_status'))
LOG.info(_LI("Share replica %s: promoted to active state "
"successfully."), share_replica['id'])
@periodic_task.periodic_task(spacing=CONF.replica_state_update_interval)
@utils.require_driver_initialized
def periodic_share_replica_update(self, context):
LOG.debug("Updating status of share replica instances.")
replicas = self.db.share_replicas_get_all(context,
with_share_data=True)
# Filter only non-active replicas belonging to this backend
def qualified_replica(r):
return (share_utils.extract_host(r['host']) ==
share_utils.extract_host(self.host))
replicas = list(filter(lambda x: qualified_replica(x), replicas))
for replica in replicas:
self._share_replica_update(
context, replica, share_id=replica['share_id'])
@locked_share_replica_operation
def _share_replica_update(self, context, share_replica, share_id=None):
share_server = self._get_share_server(context, share_replica)
replica_state = None
# Re-grab the replica:
share_replica = self.db.share_replica_get(
context, share_replica['id'], with_share_data=True,
with_share_server=True)
# We don't poll for replicas that are busy in some operation,
# or if they are the 'active' instance.
if (share_replica['status'] in constants.TRANSITIONAL_STATUSES
or share_replica['replica_state'] ==
constants.REPLICA_STATE_ACTIVE):
return
access_rules = self.db.share_access_get_all_for_share(
context, share_replica['share_id'])
LOG.debug("Updating status of share share_replica %s: ",
share_replica['id'])
share_replica = self._get_share_replica_dict(context, share_replica)
try:
replica_state = self.driver.update_replica_state(
context, share_replica, access_rules, share_server)
except Exception:
# If the replica_state was previously in 'error', it is
# possible that the driver throws an exception during its
# update. This exception can be ignored.
with excutils.save_and_reraise_exception() as exc_context:
if (share_replica.get('replica_state') ==
constants.STATUS_ERROR):
exc_context.reraise = False
if replica_state in (constants.REPLICA_STATE_IN_SYNC,
constants.REPLICA_STATE_OUT_OF_SYNC,
constants.STATUS_ERROR):
self.db.share_replica_update(context, share_replica['id'],
{'replica_state': replica_state})
elif replica_state:
msg = (_LW("Replica %(id)s cannot be set to %(state)s "
"through update call.") %
{'id': share_replica['id'], 'state': replica_state})
LOG.warning(msg)
@add_hooks @add_hooks
@utils.require_driver_initialized @utils.require_driver_initialized
def manage_share(self, context, share_id, driver_options): def manage_share(self, context, share_id, driver_options):
@ -1711,3 +2105,38 @@ class ShareManager(manager.SchedulerDependentManager):
LOG.info(_LI("Consistency group snapshot %s: deleted successfully"), LOG.info(_LI("Consistency group snapshot %s: deleted successfully"),
cgsnapshot_id) cgsnapshot_id)
def _get_share_replica_dict(self, context, share_replica):
# TODO(gouthamr): remove method when the db layer returns primitives
share_replica_ref = {
'id': share_replica.get('id'),
'share_id': share_replica.get('share_id'),
'host': share_replica.get('host'),
'status': share_replica.get('status'),
'replica_state': share_replica.get('replica_state'),
'availability_zone_id': share_replica.get('availability_zone_id'),
'export_locations': share_replica.get('export_locations'),
'share_network_id': share_replica.get('share_network_id'),
'share_server_id': share_replica.get('share_server_id'),
'deleted': share_replica.get('deleted'),
'terminated_at': share_replica.get('terminated_at'),
'launched_at': share_replica.get('launched_at'),
'scheduled_at': share_replica.get('scheduled_at'),
'share_server': self._get_share_server(context, share_replica),
'access_rules_status': share_replica.get('access_rules_status'),
# Share details
'user_id': share_replica.get('user_id'),
'project_id': share_replica.get('project_id'),
'size': share_replica.get('size'),
'display_name': share_replica.get('display_name'),
'display_description': share_replica.get('display_description'),
'snapshot_id': share_replica.get('snapshot_id'),
'share_proto': share_replica.get('share_proto'),
'share_type_id': share_replica.get('share_type_id'),
'is_public': share_replica.get('is_public'),
'consistency_group_id': share_replica.get('consistency_group_id'),
'source_cgsnapshot_member_id': share_replica.get(
'source_cgsnapshot_member_id'),
}
return share_replica_ref

View File

@ -46,6 +46,10 @@ class ShareAPI(object):
get_migration_info() get_migration_info()
get_driver_migration_info() get_driver_migration_info()
1.7 - Update target call API in allow/deny access methods 1.7 - Update target call API in allow/deny access methods
1.8 - Introduce Share Replication:
create_share_replica()
delete_share_replica()
promote_share_replica()
""" """
BASE_RPC_API_VERSION = '1.0' BASE_RPC_API_VERSION = '1.0'
@ -54,7 +58,7 @@ class ShareAPI(object):
super(ShareAPI, self).__init__() super(ShareAPI, self).__init__()
target = messaging.Target(topic=CONF.share_topic, target = messaging.Target(topic=CONF.share_topic,
version=self.BASE_RPC_API_VERSION) version=self.BASE_RPC_API_VERSION)
self.client = rpc.get_client(target, version_cap='1.7') self.client = rpc.get_client(target, version_cap='1.8')
def create_share_instance(self, ctxt, share_instance, host, def create_share_instance(self, ctxt, share_instance, host,
request_spec, filter_properties, request_spec, filter_properties,
@ -200,3 +204,40 @@ class ShareAPI(object):
ctxt, ctxt,
'delete_cgsnapshot', 'delete_cgsnapshot',
cgsnapshot_id=cgsnapshot['id']) cgsnapshot_id=cgsnapshot['id'])
def create_share_replica(self, ctxt, share_replica, host,
request_spec, filter_properties):
new_host = utils.extract_host(host)
cctxt = self.client.prepare(server=new_host, version='1.8')
request_spec_p = jsonutils.to_primitive(request_spec)
cctxt.cast(
ctxt,
'create_share_replica',
share_replica_id=share_replica['id'],
request_spec=request_spec_p,
filter_properties=filter_properties,
share_id=share_replica['share_id'],
)
def delete_share_replica(self, ctxt, share_replica_id, host,
share_id=None, force=False):
new_host = utils.extract_host(host)
cctxt = self.client.prepare(server=new_host, version='1.8')
cctxt.cast(
ctxt,
'delete_share_replica',
share_replica_id=share_replica_id,
share_id=share_id,
force=force,
)
def promote_share_replica(self, ctxt, share_replica_id, host,
share_id=None):
new_host = utils.extract_host(host)
cctxt = self.client.prepare(server=new_host, version='1.8')
cctxt.cast(
ctxt,
'promote_share_replica',
share_replica_id=share_replica_id,
share_id=share_id,
)

View File

@ -43,13 +43,20 @@ def stub_share(id, **kwargs):
'share_server_id': 'fake_share_server_id', 'share_server_id': 'fake_share_server_id',
'is_public': False, 'is_public': False,
'snapshot_support': True, 'snapshot_support': True,
'replication_type': None,
'has_replicas': False,
} }
share.update(kwargs) share.update(kwargs)
# NOTE(ameade): We must wrap the dictionary in an class in order to stub # NOTE(ameade): We must wrap the dictionary in an class in order to stub
# object attributes. # object attributes.
class wrapper(dict): class wrapper(dict):
pass def __getattr__(self, item):
try:
return self[item]
except KeyError:
raise AttributeError()
fake_share = wrapper() fake_share = wrapper()
fake_share.instance = {'id': "fake_instance_id"} fake_share.instance = {'id': "fake_instance_id"}
fake_share.update(share) fake_share.update(share)

View File

@ -121,7 +121,19 @@ class ShareInstancesAPITest(test.TestCase):
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
self.admin_context, self.resource_name, 'show') self.admin_context, self.resource_name, 'show')
@ddt.data("2.3", "2.8", "2.9") def test_show_with_replica_state(self):
test_instance = db_utils.create_share(size=1).instance
req = self._get_request('fake', version="2.11")
id = test_instance['id']
actual_result = self.controller.show(req, id)
self.assertEqual(id, actual_result['share_instance']['id'])
self.assertIn("replica_state", actual_result['share_instance'])
self.mock_policy_check.assert_called_once_with(
self.admin_context, self.resource_name, 'show')
@ddt.data("2.3", "2.8", "2.9", "2.11")
def test_get_share_instances(self, version): def test_get_share_instances(self, version):
test_share = db_utils.create_share(size=1) test_share = db_utils.create_share(size=1)
id = test_share['id'] id = test_share['id']
@ -147,6 +159,9 @@ class ShareInstancesAPITest(test.TestCase):
assert_method = self.assertIn assert_method = self.assertIn
assert_method("export_location", instance) assert_method("export_location", instance)
assert_method("export_locations", instance) assert_method("export_locations", instance)
if (api_version_request.APIVersionRequest(version) >
api_version_request.APIVersionRequest("2.10")):
self.assertIn("replica_state", instance)
self.mock_policy_check.assert_has_calls([ self.mock_policy_check.assert_has_calls([
get_instances_policy_check_call, share_policy_check_call]) get_instances_policy_check_call, share_policy_check_call])

View File

@ -0,0 +1,514 @@
# Copyright 2015 Goutham Pacha Ravi
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ddt
import mock
from oslo_config import cfg
import six
from webob import exc
from manila.api.v2 import share_replicas
from manila.common import constants
from manila import context
from manila import exception
from manila import policy
from manila import share
from manila import test
from manila.tests.api import fakes
from manila.tests import fake_share
CONF = cfg.CONF
@ddt.ddt
class ShareReplicasApiTest(test.TestCase):
"""Share Replicas API Test Cases."""
def setUp(self):
super(ShareReplicasApiTest, self).setUp()
self.controller = share_replicas.ShareReplicationController()
self.resource_name = self.controller.resource_name
self.api_version = share_replicas.MIN_SUPPORTED_API_VERSION
self.replicas_req = fakes.HTTPRequest.blank(
'/share-replicas', version=self.api_version,
experimental=True)
self.context = context.RequestContext('user', 'fake', False)
self.replicas_req.environ['manila.context'] = self.context
self.admin_context = context.RequestContext('admin', 'fake', True)
self.mock_policy_check = self.mock_object(policy, 'check_policy')
def _get_fake_replica(self, summary=False, **values):
replica = fake_share.fake_replica(**values)
expected_keys = {'id', 'share_id', 'status', 'replica_state'}
expected_replica = {key: replica[key] for key in replica if key
in expected_keys}
if not summary:
expected_replica.update({
'host': replica['host'],
'availability_zone': None,
'created_at': None,
'share_server_id': replica['share_server_id'],
'share_network_id': replica['share_network_id'],
})
return replica, expected_replica
def test_list_replicas_summary(self):
fake_replica, expected_replica = self._get_fake_replica(summary=True)
self.mock_object(share_replicas.db, 'share_replicas_get_all',
mock.Mock(return_value=[fake_replica]))
res_dict = self.controller.index(self.replicas_req)
self.assertEqual([expected_replica], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'get_all')
def test_list_share_replicas_summary(self):
fake_replica, expected_replica = self._get_fake_replica(summary=True)
self.mock_object(share_replicas.db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=[fake_replica]))
req = fakes.HTTPRequest.blank(
'/share-replicas?share_id=FAKE_SHARE_ID',
version=self.api_version, experimental=True)
req_context = req.environ['manila.context']
res_dict = self.controller.index(req)
self.assertEqual([expected_replica], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'get_all')
def test_list_replicas_detail(self):
fake_replica, expected_replica = self._get_fake_replica()
self.mock_object(share_replicas.db, 'share_replicas_get_all',
mock.Mock(return_value=[fake_replica]))
res_dict = self.controller.detail(self.replicas_req)
self.assertEqual([expected_replica], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'get_all')
def test_list_replicas_detail_with_limit(self):
fake_replica_1, expected_replica_1 = self._get_fake_replica()
fake_replica_2, expected_replica_2 = self._get_fake_replica(
id="fake_id2")
self.mock_object(
share_replicas.db, 'share_replicas_get_all',
mock.Mock(return_value=[fake_replica_1, fake_replica_2]))
req = fakes.HTTPRequest.blank('/share-replicas?limit=1',
version=self.api_version,
experimental=True)
req_context = req.environ['manila.context']
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['share_replicas']))
self.assertEqual([expected_replica_1], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'get_all')
def test_list_replicas_detail_with_limit_and_offset(self):
fake_replica_1, expected_replica_1 = self._get_fake_replica()
fake_replica_2, expected_replica_2 = self._get_fake_replica(
id="fake_id2")
self.mock_object(
share_replicas.db, 'share_replicas_get_all',
mock.Mock(return_value=[fake_replica_1, fake_replica_2]))
req = fakes.HTTPRequest.blank(
'/share-replicas/detail?limit=1&offset=1',
version=self.api_version, experimental=True)
req_context = req.environ['manila.context']
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['share_replicas']))
self.assertEqual([expected_replica_2], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'get_all')
def test_list_share_replicas_detail_invalid_share(self):
self.mock_object(share_replicas.db, 'share_replicas_get_all_by_share',
mock.Mock(side_effect=exception.NotFound))
mock__view_builder_call = self.mock_object(
share_replicas.replication_view.ReplicationViewBuilder,
'detail_list')
req = self.replicas_req
req.GET['share_id'] = 'FAKE_SHARE_ID'
self.assertRaises(exc.HTTPNotFound,
self.controller.detail, req)
self.assertFalse(mock__view_builder_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'get_all')
def test_list_share_replicas_detail(self):
fake_replica, expected_replica = self._get_fake_replica()
self.mock_object(share_replicas.db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=[fake_replica]))
req = fakes.HTTPRequest.blank(
'/share-replicas?share_id=FAKE_SHARE_ID',
version=self.api_version, experimental=True)
req_context = req.environ['manila.context']
res_dict = self.controller.detail(req)
self.assertEqual([expected_replica], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'get_all')
def test_list_share_replicas_with_limit(self):
fake_replica_1, expected_replica_1 = self._get_fake_replica()
fake_replica_2, expected_replica_2 = self._get_fake_replica(
id="fake_id2")
self.mock_object(
share_replicas.db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=[fake_replica_1, fake_replica_2]))
req = fakes.HTTPRequest.blank(
'/share-replicas?share_id=FAKE_SHARE_ID&limit=1',
version=self.api_version, experimental=True)
req_context = req.environ['manila.context']
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['share_replicas']))
self.assertEqual([expected_replica_1], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'get_all')
def test_list_share_replicas_with_limit_and_offset(self):
fake_replica_1, expected_replica_1 = self._get_fake_replica()
fake_replica_2, expected_replica_2 = self._get_fake_replica(
id="fake_id2")
self.mock_object(
share_replicas.db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=[fake_replica_1, fake_replica_2]))
req = fakes.HTTPRequest.blank(
'/share-replicas?share_id=FAKE_SHARE_ID&limit=1&offset=1',
version=self.api_version, experimental=True)
req_context = req.environ['manila.context']
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['share_replicas']))
self.assertEqual([expected_replica_2], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'get_all')
def test_show(self):
fake_replica, expected_replica = self._get_fake_replica()
self.mock_object(
share_replicas.db, 'share_replica_get',
mock.Mock(return_value=fake_replica))
res_dict = self.controller.show(
self.replicas_req, fake_replica.get('id'))
self.assertEqual(expected_replica, res_dict['share_replica'])
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'show')
def test_show_no_replica(self):
mock__view_builder_call = self.mock_object(
share_replicas.replication_view.ReplicationViewBuilder, 'detail')
fake_exception = exception.ShareReplicaNotFound(
replica_id='FAKE_REPLICA_ID')
self.mock_object(share_replicas.db, 'share_replica_get', mock.Mock(
side_effect=fake_exception))
self.assertRaises(exc.HTTPNotFound,
self.controller.show,
self.replicas_req,
'FAKE_REPLICA_ID')
self.assertFalse(mock__view_builder_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'show')
def test_create_invalid_body(self):
body = {}
mock__view_builder_call = self.mock_object(
share_replicas.replication_view.ReplicationViewBuilder,
'detail_list')
self.assertRaises(exc.HTTPUnprocessableEntity,
self.controller.create,
self.replicas_req, body)
self.assertEqual(0, mock__view_builder_call.call_count)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'create')
def test_create_no_share_id(self):
body = {
'share_replica': {
'share_id': None,
'availability_zone': None,
}
}
mock__view_builder_call = self.mock_object(
share_replicas.replication_view.ReplicationViewBuilder,
'detail_list')
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.replicas_req, body)
self.assertFalse(mock__view_builder_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'create')
def test_create_invalid_share_id(self):
body = {
'share_replica': {
'share_id': 'FAKE_SHAREID',
'availability_zone': 'FAKE_AZ'
}
}
mock__view_builder_call = self.mock_object(
share_replicas.replication_view.ReplicationViewBuilder,
'detail_list')
self.mock_object(share_replicas.db, 'share_get',
mock.Mock(side_effect=exception.NotFound))
self.assertRaises(exc.HTTPNotFound,
self.controller.create,
self.replicas_req, body)
self.assertFalse(mock__view_builder_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'create')
@ddt.data(exception.AvailabilityZoneNotFound,
exception.ReplicationException, exception.ShareBusyException)
def test_create_exception_path(self, exception_type):
fake_replica, _ = self._get_fake_replica(
replication_type='writable')
mock__view_builder_call = self.mock_object(
share_replicas.replication_view.ReplicationViewBuilder,
'detail_list')
body = {
'share_replica': {
'share_id': 'FAKE_SHAREID',
'availability_zone': 'FAKE_AZ'
}
}
exc_args = {'id': 'xyz', 'reason': 'abc'}
self.mock_object(share_replicas.db, 'share_get',
mock.Mock(return_value=fake_replica))
self.mock_object(share.API, 'create_share_replica',
mock.Mock(side_effect=exception_type(**exc_args)))
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.replicas_req, body)
self.assertFalse(mock__view_builder_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'create')
def test_create(self):
fake_replica, expected_replica = self._get_fake_replica(
replication_type='writable')
body = {
'share_replica': {
'share_id': 'FAKE_SHAREID',
'availability_zone': 'FAKE_AZ'
}
}
self.mock_object(share_replicas.db, 'share_get',
mock.Mock(return_value=fake_replica))
self.mock_object(share.API, 'create_share_replica',
mock.Mock(return_value=fake_replica))
self.mock_object(share_replicas.db,
'share_replicas_get_available_active_replica',
mock.Mock(return_value=[{'id': 'active1'}]))
res_dict = self.controller.create(self.replicas_req, body)
self.assertEqual(expected_replica, res_dict['share_replica'])
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'create')
def test_delete_invalid_replica(self):
fake_exception = exception.ShareReplicaNotFound(
replica_id='FAKE_REPLICA_ID')
self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(side_effect=fake_exception))
mock_delete_replica_call = self.mock_object(
share.API, 'delete_share_replica')
self.assertRaises(
exc.HTTPNotFound, self.controller.delete,
self.replicas_req, 'FAKE_REPLICA_ID')
self.assertFalse(mock_delete_replica_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'delete')
def test_delete_exception(self):
fake_replica_1 = self._get_fake_replica(
share_id='FAKE_SHARE_ID',
replica_state=constants.REPLICA_STATE_ACTIVE)[0]
fake_replica_2 = self._get_fake_replica(
share_id='FAKE_SHARE_ID',
replica_state=constants.REPLICA_STATE_ACTIVE)[0]
exception_type = exception.ReplicationException(reason='xyz')
self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(return_value=fake_replica_1))
self.mock_object(
share_replicas.db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=[fake_replica_1, fake_replica_2]))
self.mock_object(share.API, 'delete_share_replica',
mock.Mock(side_effect=exception_type))
self.assertRaises(exc.HTTPBadRequest, self.controller.delete,
self.replicas_req, 'FAKE_REPLICA_ID')
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'delete')
def test_delete(self):
fake_replica = self._get_fake_replica(
share_id='FAKE_SHARE_ID',
replica_state=constants.REPLICA_STATE_ACTIVE)[0]
self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(return_value=fake_replica))
self.mock_object(share.API, 'delete_share_replica')
resp = self.controller.delete(
self.replicas_req, 'FAKE_REPLICA_ID')
self.assertEqual(202, resp.status_code)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'delete')
def test_promote_invalid_replica_id(self):
body = {'promote': None}
fake_exception = exception.ShareReplicaNotFound(
replica_id='FAKE_REPLICA_ID')
self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(side_effect=fake_exception))
self.assertRaises(exc.HTTPNotFound,
self.controller.promote,
self.replicas_req,
'FAKE_REPLICA_ID', body)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'promote')
def test_promote_already_active(self):
body = {'promote': None}
replica, expected_replica = self._get_fake_replica(
replica_state=constants.REPLICA_STATE_ACTIVE)
self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(return_value=replica))
mock_api_promote_replica_call = self.mock_object(
share.API, 'promote_share_replica')
resp = self.controller.promote(self.replicas_req, replica['id'], body)
self.assertEqual(200, resp.status_code)
self.assertFalse(mock_api_promote_replica_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'promote')
def test_promote_replication_exception(self):
body = {'promote': None}
replica, expected_replica = self._get_fake_replica(
replica_state=constants.REPLICA_STATE_IN_SYNC)
exception_type = exception.ReplicationException(reason='xyz')
self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(return_value=replica))
mock_api_promote_replica_call = self.mock_object(
share.API, 'promote_share_replica',
mock.Mock(side_effect=exception_type))
self.assertRaises(exc.HTTPBadRequest,
self.controller.promote,
self.replicas_req,
replica['id'],
body)
self.assertTrue(mock_api_promote_replica_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'promote')
def test_promote_admin_required_exception(self):
body = {'promote': None}
replica, expected_replica = self._get_fake_replica(
replica_state=constants.REPLICA_STATE_IN_SYNC)
self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(return_value=replica))
mock_api_promote_replica_call = self.mock_object(
share.API, 'promote_share_replica',
mock.Mock(side_effect=exception.AdminRequired))
self.assertRaises(exc.HTTPForbidden,
self.controller.promote,
self.replicas_req,
replica['id'],
body)
self.assertTrue(mock_api_promote_replica_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'promote')
def test_promote(self):
body = {'promote': None}
replica, expected_replica = self._get_fake_replica(
replica_state=constants.REPLICA_STATE_IN_SYNC)
self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(return_value=replica))
mock_api_promote_replica_call = self.mock_object(
share.API, 'promote_share_replica',
mock.Mock(return_value=replica))
resp = self.controller.promote(self.replicas_req, replica['id'], body)
self.assertEqual(expected_replica, resp['share_replica'])
self.assertTrue(mock_api_promote_replica_call.called)
self.mock_policy_check.assert_called_once_with(
self.context, self.resource_name, 'promote')
@ddt.data('index', 'detail', 'show', 'create', 'delete', 'promote')
def test_policy_not_authorized(self, method_name):
method = getattr(self.controller, method_name)
arguments = {
'id': 'FAKE_REPLICA_ID',
'body': {'FAKE_KEY': 'FAKE_VAL'},
}
if method_name in ('index', 'detail'):
arguments.clear()
noauthexc = exception.PolicyNotAuthorized(action=six.text_type(method))
with mock.patch.object(
policy, 'check_policy', mock.Mock(side_effect=noauthexc)):
self.assertRaises(
exc.HTTPForbidden, method, self.replicas_req, **arguments)
@ddt.data('index', 'detail', 'show', 'create', 'delete', 'promote')
def test_upsupported_microversion(self, method_name):
unsupported_microversions = ('1.0', '2.2', '2.8')
method = getattr(self.controller, method_name)
arguments = {
'id': 'FAKE_REPLICA_ID',
'body': {'FAKE_KEY': 'FAKE_VAL'},
}
if method_name in ('index', 'detail'):
arguments.clear()
for microversion in unsupported_microversions:
req = fakes.HTTPRequest.blank(
'/share-replicas', version=microversion,
experimental=True)
self.assertRaises(exception.VersionNotFoundForAPIMethod,
method, req, **arguments)

View File

@ -25,6 +25,7 @@ import webob
from manila.api import common from manila.api import common
from manila.api.openstack import api_version_request as api_version from manila.api.openstack import api_version_request as api_version
from manila.api.v2 import share_replicas
from manila.api.v2 import shares from manila.api.v2 import shares
from manila.common import constants from manila.common import constants
from manila import context from manila import context
@ -203,6 +204,29 @@ class ShareAPITest(test.TestCase):
self.controller.create, req, {'share': self.share}) self.controller.create, req, {'share': self.share})
share_types.get_default_share_type.assert_called_once_with() share_types.get_default_share_type.assert_called_once_with()
def test_share_create_with_replication(self):
self.mock_object(share_api.API, 'create', self.create_mock)
body = {"share": copy.deepcopy(self.share)}
req = fakes.HTTPRequest.blank(
'/shares', version=share_replicas.MIN_SUPPORTED_API_VERSION)
res_dict = self.controller.create(req, body)
expected = self._get_expected_share_detailed_response(self.share)
expected['share']['task_state'] = None
expected['share']['consistency_group_id'] = None
expected['share']['source_cgsnapshot_member_id'] = None
expected['share']['replication_type'] = None
expected['share']['share_type_name'] = None
expected['share']['has_replicas'] = False
expected['share']['access_rules_status'] = 'active'
expected['share'].pop('export_location')
expected['share'].pop('export_locations')
self.assertEqual(expected, res_dict)
def test_share_create_with_share_net(self): def test_share_create_with_share_net(self):
shr = { shr = {
"size": 100, "size": 100,
@ -250,6 +274,22 @@ class ShareAPITest(test.TestCase):
self.mock_object(share_api.API, 'migrate_share') self.mock_object(share_api.API, 'migrate_share')
getattr(self.controller, method)(req, share['id'], body) getattr(self.controller, method)(req, share['id'], body)
def test_migrate_share_has_replicas(self):
share = db_utils.create_share()
req = fakes.HTTPRequest.blank('/shares/%s/action' % share['id'],
use_admin_context=True)
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.api_version_request = api_version.APIVersionRequest('2.10')
req.api_version_request.experimental = True
body = {'migrate_share': {'host': 'fake_host'}}
self.mock_object(share_api.API, 'migrate_share',
mock.Mock(side_effect=exception.Conflict(err='err')))
self.assertRaises(webob.exc.HTTPConflict,
self.controller.migrate_share,
req, share['id'], body)
@ddt.data('2.5', '2.6', '2.7') @ddt.data('2.5', '2.6', '2.7')
def test_migrate_share_no_share_id(self, version): def test_migrate_share_no_share_id(self, version):
req = fakes.HTTPRequest.blank('/shares/%s/action' % 'fake_id', req = fakes.HTTPRequest.blank('/shares/%s/action' % 'fake_id',
@ -500,11 +540,40 @@ class ShareAPITest(test.TestCase):
self.controller.show, self.controller.show,
req, '1') req, '1')
def test_share_show_with_replication_type(self):
req = fakes.HTTPRequest.blank(
'/shares/1', version=share_replicas.MIN_SUPPORTED_API_VERSION)
res_dict = self.controller.show(req, '1')
expected = self._get_expected_share_detailed_response()
expected['share']['task_state'] = None
expected['share']['consistency_group_id'] = None
expected['share']['source_cgsnapshot_member_id'] = None
expected['share']['access_rules_status'] = 'active'
expected['share']['share_type_name'] = None
expected['share']['replication_type'] = None
expected['share']['has_replicas'] = False
expected['share'].pop('export_location')
expected['share'].pop('export_locations')
self.assertEqual(expected, res_dict)
def test_share_delete(self): def test_share_delete(self):
req = fakes.HTTPRequest.blank('/shares/1') req = fakes.HTTPRequest.blank('/shares/1')
resp = self.controller.delete(req, 1) resp = self.controller.delete(req, 1)
self.assertEqual(202, resp.status_int) self.assertEqual(202, resp.status_int)
def test_share_delete_has_replicas(self):
req = fakes.HTTPRequest.blank('/shares/1')
self.mock_object(share_api.API, 'get',
mock.Mock(return_value=self.share))
self.mock_object(share_api.API, 'delete',
mock.Mock(side_effect=exception.Conflict(err='err')))
self.assertRaises(
webob.exc.HTTPConflict, self.controller.delete, req, 1)
def test_share_delete_in_consistency_group_param_not_provided(self): def test_share_delete_in_consistency_group_param_not_provided(self):
fake_share = stubs.stub_share('fake_share', fake_share = stubs.stub_share('fake_share',
consistency_group_id='fake_cg_id') consistency_group_id='fake_cg_id')
@ -827,6 +896,58 @@ class ShareAPITest(test.TestCase):
expected['shares'][0].pop('export_locations') expected['shares'][0].pop('export_locations')
self._list_detail_test_common(req, expected) self._list_detail_test_common(req, expected)
def test_share_list_detail_with_replication_type(self):
self.mock_object(share_api.API, 'get_all',
stubs.stub_share_get_all_by_project)
env = {'QUERY_STRING': 'name=Share+Test+Name'}
req = fakes.HTTPRequest.blank(
'/shares/detail', environ=env,
version=share_replicas.MIN_SUPPORTED_API_VERSION)
res_dict = self.controller.detail(req)
expected = {
'shares': [
{
'status': 'fakestatus',
'description': 'displaydesc',
'availability_zone': 'fakeaz',
'name': 'displayname',
'share_proto': 'FAKEPROTO',
'metadata': {},
'project_id': 'fakeproject',
'access_rules_status': 'active',
'host': 'fakehost',
'id': '1',
'snapshot_id': '2',
'share_network_id': None,
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'size': 1,
'share_type_name': None,
'share_type': '1',
'volume_type': '1',
'is_public': False,
'consistency_group_id': None,
'source_cgsnapshot_member_id': None,
'snapshot_support': True,
'has_replicas': False,
'replication_type': None,
'task_state': None,
'links': [
{
'href': 'http://localhost/v1/fake/shares/1',
'rel': 'self'
},
{
'href': 'http://localhost/fake/shares/1',
'rel': 'bookmark'
}
],
}
]
}
self.assertEqual(expected, res_dict)
self.assertEqual(res_dict['shares'][0]['volume_type'],
res_dict['shares'][0]['share_type'])
def test_remove_invalid_options(self): def test_remove_invalid_options(self):
ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False) ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False)
search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'} search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'}

View File

@ -36,6 +36,12 @@ class FakeModel(object):
def __repr__(self): def __repr__(self):
return '<FakeModel: %s>' % self.values return '<FakeModel: %s>' % self.values
def get(self, key, default=None):
return self.__getattr__(key) or default
def __contains__(self, key):
return self._getattr__(key)
def stub_out(stubs, funcs): def stub_out(stubs, funcs):
"""Set the stubs in mapping in the db api.""" """Set the stubs in mapping in the db api."""

View File

@ -34,6 +34,7 @@ See BaseMigrationChecks class for more information.
""" """
import abc import abc
import datetime
from oslo_utils import uuidutils from oslo_utils import uuidutils
import six import six
@ -351,3 +352,121 @@ class AccessRulesStatusMigrationChecks(BaseMigrationChecks):
for rule in engine.execute(share_instances_rules_table.select()): for rule in engine.execute(share_instances_rules_table.select()):
valid_state = valid_statuses[rule['share_instance_id']] valid_state = valid_statuses[rule['share_instance_id']]
self.test_case.assertEqual(valid_state, rule['state']) self.test_case.assertEqual(valid_state, rule['state'])
@map_to_migration('293fac1130ca')
class ShareReplicationMigrationChecks(BaseMigrationChecks):
valid_share_display_names = ('FAKE_SHARE_1', 'FAKE_SHARE_2',
'FAKE_SHARE_3')
valid_share_ids = []
valid_replication_types = ('writable', 'readable', 'dr')
def _load_tables_and_get_data(self, engine):
share_table = utils.load_table('shares', engine)
share_instances_table = utils.load_table('share_instances', engine)
shares = engine.execute(
share_table.select().where(share_table.c.id.in_(
self.valid_share_ids))
).fetchall()
share_instances = engine.execute(share_instances_table.select().where(
share_instances_table.c.share_id.in_(self.valid_share_ids))
).fetchall()
return shares, share_instances
def _new_share(self, **kwargs):
share = {
'id': uuidutils.generate_uuid(),
'display_name': 'fake_share',
'size': '1',
'deleted': 'False',
'share_proto': 'fake_proto',
'user_id': 'fake_user_id',
'project_id': 'fake_project_uuid',
'snapshot_support': '1',
'task_state': None,
}
share.update(kwargs)
return share
def _new_instance(self, share_id=None, **kwargs):
instance = {
'id': uuidutils.generate_uuid(),
'share_id': share_id or uuidutils.generate_uuid(),
'deleted': 'False',
'host': 'openstack@BackendZ#PoolA',
'status': 'available',
'scheduled_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'launched_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'terminated_at': None,
'access_rules_status': 'active',
}
instance.update(kwargs)
return instance
def setup_upgrade_data(self, engine):
shares_data = []
instances_data = []
self.valid_share_ids = []
for share_display_name in self.valid_share_display_names:
share_ref = self._new_share(display_name=share_display_name)
shares_data.append(share_ref)
instances_data.append(self._new_instance(share_id=share_ref['id']))
shares_table = utils.load_table('shares', engine)
for share in shares_data:
self.valid_share_ids.append(share['id'])
engine.execute(shares_table.insert(share))
shares_instances_table = utils.load_table('share_instances', engine)
for share_instance in instances_data:
engine.execute(shares_instances_table.insert(share_instance))
def check_upgrade(self, engine, _):
shares, share_instances = self._load_tables_and_get_data(engine)
share_ids = [share['id'] for share in shares]
share_instance_share_ids = [share_instance['share_id'] for
share_instance in share_instances]
# Assert no data is lost
for sid in self.valid_share_ids:
self.test_case.assertIn(sid, share_ids)
self.test_case.assertIn(sid, share_instance_share_ids)
for share in shares:
self.test_case.assertIn(share['display_name'],
self.valid_share_display_names)
self.test_case.assertEqual('False', share.deleted)
self.test_case.assertTrue(hasattr(share, 'replication_type'))
for share_instance in share_instances:
self.test_case.assertTrue(hasattr(share_instance, 'replica_state'))
def check_downgrade(self, engine):
shares, share_instances = self._load_tables_and_get_data(engine)
share_ids = [share['id'] for share in shares]
share_instance_share_ids = [share_instance['share_id'] for
share_instance in share_instances]
# Assert no data is lost
for sid in self.valid_share_ids:
self.test_case.assertIn(sid, share_ids)
self.test_case.assertIn(sid, share_instance_share_ids)
for share in shares:
self.test_case.assertEqual('False', share.deleted)
self.test_case.assertIn(share.display_name,
self.valid_share_display_names)
self.test_case.assertFalse(hasattr(share, 'replication_type'))
for share_instance in share_instances:
self.test_case.assertEqual('False', share_instance.deleted)
self.test_case.assertIn(share_instance.share_id,
self.valid_share_ids)
self.test_case.assertFalse(
hasattr(share_instance, 'replica_state'))

View File

@ -217,6 +217,344 @@ class ShareDatabaseAPITestCase(test.TestCase):
self.assertEqual(2, len(actual_result)) self.assertEqual(2, len(actual_result))
self.assertEqual(shares[0]['id'], actual_result[1]['id']) self.assertEqual(shares[0]['id'], actual_result[1]['id'])
@ddt.data(None, 'writable')
def test_share_get_has_replicas_field(self, replication_type):
share = db_utils.create_share(replication_type=replication_type)
db_share = db_api.share_get(self.ctxt, share['id'])
self.assertTrue('has_replicas' in db_share)
@ddt.data({'with_share_data': False, 'with_share_server': False},
{'with_share_data': False, 'with_share_server': True},
{'with_share_data': True, 'with_share_server': False},
{'with_share_data': True, 'with_share_server': True})
@ddt.unpack
def test_share_replicas_get_all(self, with_share_data,
with_share_server):
share_server = db_utils.create_share_server()
share_1 = db_utils.create_share()
share_2 = db_utils.create_share()
db_utils.create_share_replica(
replica_state=constants.REPLICA_STATE_ACTIVE,
share_id=share_1['id'],
share_server_id=share_server['id'])
db_utils.create_share_replica(
replica_state=constants.REPLICA_STATE_IN_SYNC,
share_id=share_1['id'],
share_server_id=share_server['id'])
db_utils.create_share_replica(
replica_state=constants.REPLICA_STATE_OUT_OF_SYNC,
share_id=share_2['id'],
share_server_id=share_server['id'])
db_utils.create_share_replica(share_id=share_2['id'])
expected_ss_keys = {
'backend_details', 'host', 'id',
'share_network_id', 'status',
}
expected_share_keys = {
'project_id', 'share_type_id', 'display_name',
'name', 'share_proto', 'is_public',
'source_cgsnapshot_member_id',
}
session = db_api.get_session()
with session.begin():
share_replicas = db_api.share_replicas_get_all(
self.ctxt, with_share_server=with_share_server,
with_share_data=with_share_data, session=session)
self.assertEqual(3, len(share_replicas))
for replica in share_replicas:
if with_share_server:
self.assertTrue(expected_ss_keys.issubset(
replica['share_server'].keys()))
else:
self.assertFalse('share_server' in replica.keys())
self.assertEqual(
with_share_data,
expected_share_keys.issubset(replica.keys()))
@ddt.data({'with_share_data': False, 'with_share_server': False},
{'with_share_data': False, 'with_share_server': True},
{'with_share_data': True, 'with_share_server': False},
{'with_share_data': True, 'with_share_server': True})
@ddt.unpack
def test_share_replicas_get_all_by_share(self, with_share_data,
with_share_server):
share_server = db_utils.create_share_server()
share = db_utils.create_share()
db_utils.create_share_replica(
replica_state=constants.REPLICA_STATE_ACTIVE,
share_id=share['id'],
share_server_id=share_server['id'])
db_utils.create_share_replica(
replica_state=constants.REPLICA_STATE_IN_SYNC,
share_id=share['id'],
share_server_id=share_server['id'])
db_utils.create_share_replica(
replica_state=constants.REPLICA_STATE_OUT_OF_SYNC,
share_id=share['id'],
share_server_id=share_server['id'])
expected_ss_keys = {
'backend_details', 'host', 'id',
'share_network_id', 'status',
}
expected_share_keys = {
'project_id', 'share_type_id', 'display_name',
'name', 'share_proto', 'is_public',
'source_cgsnapshot_member_id',
}
session = db_api.get_session()
with session.begin():
share_replicas = db_api.share_replicas_get_all_by_share(
self.ctxt, share['id'],
with_share_server=with_share_server,
with_share_data=with_share_data, session=session)
self.assertEqual(3, len(share_replicas))
for replica in share_replicas:
if with_share_server:
self.assertTrue(expected_ss_keys.issubset(
replica['share_server'].keys()))
else:
self.assertFalse('share_server' in replica.keys())
self.assertEqual(with_share_data,
expected_share_keys.issubset(replica.keys()))
def test_share_replicas_get_available_active_replica(self):
share_server = db_utils.create_share_server()
share_1 = db_utils.create_share()
share_2 = db_utils.create_share()
share_3 = db_utils.create_share()
db_utils.create_share_replica(
id='Replica1',
share_id=share_1['id'],
status=constants.STATUS_AVAILABLE,
replica_state=constants.REPLICA_STATE_ACTIVE,
share_server_id=share_server['id'])
db_utils.create_share_replica(
id='Replica2',
status=constants.STATUS_AVAILABLE,
share_id=share_1['id'],
replica_state=constants.REPLICA_STATE_ACTIVE,
share_server_id=share_server['id'])
db_utils.create_share_replica(
id='Replica3',
status=constants.STATUS_AVAILABLE,
share_id=share_2['id'],
replica_state=constants.REPLICA_STATE_ACTIVE)
db_utils.create_share_replica(
id='Replica4',
status=constants.STATUS_ERROR,
share_id=share_2['id'],
replica_state=constants.REPLICA_STATE_ACTIVE)
db_utils.create_share_replica(
id='Replica5',
status=constants.STATUS_AVAILABLE,
share_id=share_2['id'],
replica_state=constants.REPLICA_STATE_IN_SYNC)
db_utils.create_share_replica(
id='Replica6',
share_id=share_3['id'],
status=constants.STATUS_AVAILABLE,
replica_state=constants.REPLICA_STATE_IN_SYNC)
session = db_api.get_session()
expected_ss_keys = {
'backend_details', 'host', 'id',
'share_network_id', 'status',
}
expected_share_keys = {
'project_id', 'share_type_id', 'display_name',
'name', 'share_proto', 'is_public',
'source_cgsnapshot_member_id',
}
with session.begin():
replica_share_1 = (
db_api.share_replicas_get_available_active_replica(
self.ctxt, share_1['id'], with_share_server=True,
session=session)
)
replica_share_2 = (
db_api.share_replicas_get_available_active_replica(
self.ctxt, share_2['id'], with_share_data=True,
session=session)
)
replica_share_3 = (
db_api.share_replicas_get_available_active_replica(
self.ctxt, share_3['id'], session=session)
)
self.assertIn(replica_share_1.get('id'), ['Replica1', 'Replica2'])
self.assertTrue(expected_ss_keys.issubset(
replica_share_1['share_server'].keys()))
self.assertFalse(
expected_share_keys.issubset(replica_share_1.keys()))
self.assertEqual(replica_share_2.get('id'), 'Replica3')
self.assertFalse(replica_share_2['share_server'])
self.assertTrue(
expected_share_keys.issubset(replica_share_2.keys()))
self.assertIsNone(replica_share_3)
def test_share_replicas_get_active_replicas_by_share(self):
db_utils.create_share_replica(
id='Replica1',
share_id='FAKE_SHARE_ID1',
status=constants.STATUS_AVAILABLE,
replica_state=constants.REPLICA_STATE_ACTIVE)
db_utils.create_share_replica(
id='Replica2',
status=constants.STATUS_AVAILABLE,
share_id='FAKE_SHARE_ID1',
replica_state=constants.REPLICA_STATE_ACTIVE)
db_utils.create_share_replica(
id='Replica3',
status=constants.STATUS_AVAILABLE,
share_id='FAKE_SHARE_ID2',
replica_state=constants.REPLICA_STATE_ACTIVE)
db_utils.create_share_replica(
id='Replica4',
status=constants.STATUS_ERROR,
share_id='FAKE_SHARE_ID2',
replica_state=constants.REPLICA_STATE_ACTIVE)
db_utils.create_share_replica(
id='Replica5',
status=constants.STATUS_AVAILABLE,
share_id='FAKE_SHARE_ID2',
replica_state=constants.REPLICA_STATE_IN_SYNC)
db_utils.create_share_replica(
id='Replica6',
share_id='FAKE_SHARE_ID3',
status=constants.STATUS_AVAILABLE,
replica_state=constants.REPLICA_STATE_IN_SYNC)
def get_active_replica_ids(share_id):
active_replicas = (
db_api.share_replicas_get_active_replicas_by_share(
self.ctxt, share_id)
)
return [r['id'] for r in active_replicas]
active_ids_shr1 = get_active_replica_ids('FAKE_SHARE_ID1')
active_ids_shr2 = get_active_replica_ids('FAKE_SHARE_ID2')
active_ids_shr3 = get_active_replica_ids('FAKE_SHARE_ID3')
self.assertEqual(active_ids_shr1, ['Replica1', 'Replica2'])
self.assertEqual(active_ids_shr2, ['Replica3', 'Replica4'])
self.assertEqual([], active_ids_shr3)
def test_share_replica_get_exception(self):
replica = db_utils.create_share_replica(share_id='FAKE_SHARE_ID')
self.assertRaises(exception.ShareReplicaNotFound,
db_api.share_replica_get,
self.ctxt, replica['id'])
def test_share_replica_get_without_share_data(self):
share = db_utils.create_share()
replica = db_utils.create_share_replica(
share_id=share['id'],
replica_state=constants.REPLICA_STATE_ACTIVE)
expected_extra_keys = {
'project_id', 'share_type_id', 'display_name',
'name', 'share_proto', 'is_public',
'source_cgsnapshot_member_id',
}
share_replica = db_api.share_replica_get(self.ctxt, replica['id'])
self.assertIsNotNone(share_replica['replica_state'])
self.assertEqual(share['id'], share_replica['share_id'])
self.assertFalse(expected_extra_keys.issubset(share_replica.keys()))
def test_share_replica_get_with_share_data(self):
share = db_utils.create_share()
replica = db_utils.create_share_replica(
share_id=share['id'],
replica_state=constants.REPLICA_STATE_ACTIVE)
expected_extra_keys = {
'project_id', 'share_type_id', 'display_name',
'name', 'share_proto', 'is_public',
'source_cgsnapshot_member_id',
}
share_replica = db_api.share_replica_get(
self.ctxt, replica['id'], with_share_data=True)
self.assertIsNotNone(share_replica['replica_state'])
self.assertEqual(share['id'], share_replica['share_id'])
self.assertTrue(expected_extra_keys.issubset(share_replica.keys()))
def test_share_replica_get_with_share_server(self):
session = db_api.get_session()
share_server = db_utils.create_share_server()
share = db_utils.create_share()
replica = db_utils.create_share_replica(
share_id=share['id'],
replica_state=constants.REPLICA_STATE_ACTIVE,
share_server_id=share_server['id']
)
expected_extra_keys = {
'backend_details', 'host', 'id',
'share_network_id', 'status',
}
with session.begin():
share_replica = db_api.share_replica_get(
self.ctxt, replica['id'], with_share_server=True,
session=session)
self.assertIsNotNone(share_replica['replica_state'])
self.assertEqual(
share_server['id'], share_replica['share_server_id'])
self.assertTrue(expected_extra_keys.issubset(
share_replica['share_server'].keys()))
def test_share_replica_update(self):
share = db_utils.create_share()
replica = db_utils.create_share_replica(
share_id=share['id'], replica_state=constants.REPLICA_STATE_ACTIVE)
updated_replica = db_api.share_replica_update(
self.ctxt, replica['id'],
{'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC})
self.assertEqual(constants.REPLICA_STATE_OUT_OF_SYNC,
updated_replica['replica_state'])
def test_share_replica_delete(self):
share = db_utils.create_share()
share = db_api.share_get(self.ctxt, share['id'])
replica = db_utils.create_share_replica(
share_id=share['id'], replica_state=constants.REPLICA_STATE_ACTIVE)
self.assertEqual(1, len(
db_api.share_replicas_get_all_by_share(self.ctxt, share['id'])))
db_api.share_replica_delete(self.ctxt, replica['id'])
self.assertEqual(
[], db_api.share_replicas_get_all_by_share(self.ctxt, share['id']))
def test_share_instance_access_copy(self):
share = db_utils.create_share()
rules = []
for i in range(0, 5):
rules.append(db_utils.create_access(share_id=share['id']))
instance = db_utils.create_share_instance(share_id=share['id'])
share_access_rules = db_api.share_instance_access_copy(
self.ctxt, share['id'], instance['id'])
share_access_rule_ids = [a['id'] for a in share_access_rules]
self.assertEqual(5, len(share_access_rules))
for rule_id in share_access_rule_ids:
self.assertIsNotNone(
db_api.share_instance_access_get(
self.ctxt, rule_id, instance['id']))
@ddt.ddt @ddt.ddt
class ConsistencyGroupDatabaseAPITestCase(test.TestCase): class ConsistencyGroupDatabaseAPITestCase(test.TestCase):

View File

@ -76,6 +76,55 @@ class ShareTestCase(test.TestCase):
self.assertEqual(constants.STATUS_CREATING, share.instance['status']) self.assertEqual(constants.STATUS_CREATING, share.instance['status'])
@ddt.data(constants.STATUS_AVAILABLE, constants.STATUS_ERROR,
constants.STATUS_CREATING)
def test_share_instance_replication_change(self, status):
instance_list = [
db_utils.create_share_instance(
status=constants.STATUS_REPLICATION_CHANGE,
share_id='fake_id'),
db_utils.create_share_instance(
status=status, share_id='fake_id'),
db_utils.create_share_instance(
status=constants.STATUS_ERROR_DELETING, share_id='fake_id')
]
share1 = db_utils.create_share(instances=instance_list)
share2 = db_utils.create_share(instances=list(reversed(instance_list)))
self.assertEqual(
constants.STATUS_REPLICATION_CHANGE, share1.instance['status'])
self.assertEqual(
constants.STATUS_REPLICATION_CHANGE, share2.instance['status'])
def test_share_instance_prefer_active_instance(self):
instance_list = [
db_utils.create_share_instance(
status=constants.STATUS_AVAILABLE,
share_id='fake_id',
replica_state=constants.REPLICA_STATE_IN_SYNC),
db_utils.create_share_instance(
status=constants.STATUS_CREATING,
share_id='fake_id',
replica_state=constants.REPLICA_STATE_OUT_OF_SYNC),
db_utils.create_share_instance(
status=constants.STATUS_ERROR, share_id='fake_id',
replica_state=constants.REPLICA_STATE_ACTIVE),
db_utils.create_share_instance(
status=constants.STATUS_MANAGING, share_id='fake_id',
replica_state=constants.REPLICA_STATE_ACTIVE),
]
share1 = db_utils.create_share(instances=instance_list)
share2 = db_utils.create_share(instances=list(reversed(instance_list)))
self.assertEqual(
constants.STATUS_ERROR, share1.instance['status'])
self.assertEqual(
constants.STATUS_ERROR, share2.instance['status'])
def test_access_rules_status_no_instances(self): def test_access_rules_status_no_instances(self):
share = db_utils.create_share(instances=[]) share = db_utils.create_share(instances=[])

View File

@ -98,6 +98,18 @@ def create_share_instance(**kwargs):
kwargs.pop('share_id'), kwargs) kwargs.pop('share_id'), kwargs)
def create_share_replica(**kwargs):
"""Create a share replica object."""
replica = {
'host': 'fake',
'status': constants.STATUS_CREATING,
}
replica.update(kwargs)
return db.share_instance_create(context.get_admin_context(),
kwargs.pop('share_id'), kwargs)
def create_snapshot(**kwargs): def create_snapshot(**kwargs):
"""Create a snapshot object.""" """Create a snapshot object."""
with_share = kwargs.pop('with_share', False) with_share = kwargs.pop('with_share', False)

View File

@ -13,6 +13,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import datetime
import uuid
from manila.tests.db import fakes as db_fakes from manila.tests.db import fakes as db_fakes
@ -29,6 +31,8 @@ def fake_share(**kwargs):
'project_id': 'fake_project_uuid', 'project_id': 'fake_project_uuid',
'availability_zone': 'fake_az', 'availability_zone': 'fake_az',
'snapshot_support': 'True', 'snapshot_support': 'True',
'replication_type': None,
'is_busy': False,
} }
share.update(kwargs) share.update(kwargs)
return db_fakes.FakeModel(share) return db_fakes.FakeModel(share)
@ -58,3 +62,61 @@ def fake_access(**kwargs):
} }
access.update(kwargs) access.update(kwargs)
return db_fakes.FakeModel(access) return db_fakes.FakeModel(access)
def fake_replica(id=None, as_primitive=True, for_manager=False, **kwargs):
replica = {
'id': id or str(uuid.uuid4()),
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'deleted': False,
'host': 'openstack@BackendZ#PoolA',
'status': 'available',
'scheduled_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'launched_at': datetime.datetime(2015, 8, 10, 0, 5, 58),
'terminated_at': None,
'replica_state': None,
'availability_zone_id': 'f6e146d0-65f0-11e5-9d70-feff819cdc9f',
'export_locations': [{'path': 'path1'}, {'path': 'path2'}],
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'share_server_id': '53099868-65f1-11e5-9d70-feff819cdc9f',
'access_rules_status': 'out_of_sync',
}
if for_manager:
replica.update({
'user_id': None,
'project_id': None,
'share_type_id': None,
'size': None,
'display_name': None,
'display_description': None,
'snapshot_id': None,
'share_proto': None,
'is_public': None,
'consistency_group_id': None,
'source_cgsnapshot_member_id': None,
'availability_zone': 'fake_az',
})
replica.update(kwargs)
if as_primitive:
return replica
else:
return db_fakes.FakeModel(replica)
def fake_replica_request_spec(as_primitive=True, **kwargs):
request_spec = {
'share_properties': fake_share(
id='f0e4bb5e-65f0-11e5-9d70-feff819cdc9f'),
'share_instance_properties': fake_replica(
id='9c0db763-a109-4862-b010-10f2bd395295'),
'share_proto': 'nfs',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'snapshot_id': None,
'share_type': 'fake_share_type',
'consistency_group': None,
}
request_spec.update(kwargs)
if as_primitive:
return request_spec
else:
return db_fakes.FakeModel(request_spec)

View File

@ -17,6 +17,7 @@
import re import re
from eventlet import greenthread from eventlet import greenthread
import mock
from oslo_log import log from oslo_log import log
import six import six
@ -109,3 +110,10 @@ def stub_out_utils_execute(testcase):
fake_execute_set_repliers([]) fake_execute_set_repliers([])
fake_execute_clear_log() fake_execute_clear_log()
testcase.mock_object(utils, 'execute', fake_execute) testcase.mock_object(utils, 'execute', fake_execute)
def get_fake_lock_context():
context_manager_mock = mock.Mock()
setattr(context_manager_mock, '__enter__', mock.Mock())
setattr(context_manager_mock, '__exit__', mock.Mock())
return context_manager_mock

View File

@ -30,6 +30,7 @@ from manila.tests.scheduler.drivers import test_base
from manila.tests.scheduler import fakes from manila.tests.scheduler import fakes
SNAPSHOT_SUPPORT = constants.ExtraSpecs.SNAPSHOT_SUPPORT SNAPSHOT_SUPPORT = constants.ExtraSpecs.SNAPSHOT_SUPPORT
REPLICATION_TYPE_SPEC = constants.ExtraSpecs.REPLICATION_TYPE_SPEC
@ddt.ddt @ddt.ddt
@ -133,6 +134,59 @@ class FilterSchedulerTestCase(test_base.SchedulerTestCase):
self.assertIsNone(weighed_host) self.assertIsNone(weighed_host)
self.assertTrue(_mock_service_get_all_by_topic.called) self.assertTrue(_mock_service_get_all_by_topic.called)
@ddt.data(
*[{'name': 'foo', 'extra_specs': {
SNAPSHOT_SUPPORT: 'True', REPLICATION_TYPE_SPEC: v
}} for v in ('writable', 'readable', 'dr')]
)
@mock.patch('manila.db.service_get_all_by_topic')
def test__schedule_share_with_valid_replication_spec(
self, share_type, _mock_service_get_all_by_topic):
sched = fakes.FakeFilterScheduler()
sched.host_manager = fakes.FakeHostManager()
fake_context = context.RequestContext('user', 'project',
is_admin=True)
fakes.mock_host_manager_db_calls(_mock_service_get_all_by_topic)
request_spec = {
'share_type': share_type,
'share_properties': {'project_id': 1, 'size': 1},
'share_instance_properties': {'project_id': 1, 'size': 1},
}
weighed_host = sched._schedule_share(fake_context, request_spec, {})
self.assertIsNotNone(weighed_host)
self.assertIsNotNone(weighed_host.obj)
self.assertTrue(hasattr(weighed_host.obj, REPLICATION_TYPE_SPEC))
expected_replication_type_support = (
share_type.get('extra_specs', {}).get(REPLICATION_TYPE_SPEC))
self.assertEqual(
expected_replication_type_support,
getattr(weighed_host.obj, REPLICATION_TYPE_SPEC))
self.assertTrue(_mock_service_get_all_by_topic.called)
@ddt.data(
*[{'name': 'foo', 'extra_specs': {
SNAPSHOT_SUPPORT: 'True', REPLICATION_TYPE_SPEC: v
}} for v in ('None', 'readwrite', 'activesync')]
)
@mock.patch('manila.db.service_get_all_by_topic')
def test__schedule_share_with_invalid_replication_type_spec(
self, share_type, _mock_service_get_all_by_topic):
sched = fakes.FakeFilterScheduler()
sched.host_manager = fakes.FakeHostManager()
fake_context = context.RequestContext('user', 'project',
is_admin=True)
fakes.mock_host_manager_db_calls(_mock_service_get_all_by_topic)
request_spec = {
'share_type': share_type,
'share_properties': {'project_id': 1, 'size': 1},
'share_instance_properties': {'project_id': 1, 'size': 1},
}
weighed_host = sched._schedule_share(fake_context, request_spec, {})
self.assertIsNone(weighed_host)
self.assertTrue(_mock_service_get_all_by_topic.called)
@mock.patch('manila.db.service_get_all_by_topic') @mock.patch('manila.db.service_get_all_by_topic')
def test_schedule_share_with_cg_pool_support( def test_schedule_share_with_cg_pool_support(
self, _mock_service_get_all_by_topic): self, _mock_service_get_all_by_topic):
@ -450,3 +504,38 @@ class FilterSchedulerTestCase(test_base.SchedulerTestCase):
sched.host_passes_filters, sched.host_passes_filters,
ctx, 'host3#_pool0', request_spec, {}) ctx, 'host3#_pool0', request_spec, {})
self.assertTrue(_mock_service_get_topic.called) self.assertTrue(_mock_service_get_topic.called)
def test_schedule_create_replica_no_host(self):
sched = fakes.FakeFilterScheduler()
request_spec = fakes.fake_replica_request_spec()
self.mock_object(self.driver_cls, '_schedule_share',
mock.Mock(return_value=None))
self.assertRaises(exception.NoValidHost,
sched.schedule_create_replica,
self.context, request_spec, {})
def test_schedule_create_replica(self):
sched = fakes.FakeFilterScheduler()
request_spec = fakes.fake_replica_request_spec()
host = 'fake_host'
replica_id = request_spec['share_instance_properties']['id']
mock_update_db_call = self.mock_object(
base, 'share_replica_update_db',
mock.Mock(return_value='replica'))
mock_share_rpcapi_call = self.mock_object(
sched.share_rpcapi, 'create_share_replica')
self.mock_object(
self.driver_cls, '_schedule_share',
mock.Mock(return_value=fakes.get_fake_host(host_name=host)))
retval = sched.schedule_create_replica(
self.context, fakes.fake_replica_request_spec(), {})
self.assertIsNone(retval)
mock_update_db_call.assert_called_once_with(
self.context, replica_id, host)
mock_share_rpcapi_call.assert_called_once_with(
self.context, 'replica', host, request_spec=request_spec,
filter_properties={})

View File

@ -79,6 +79,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
timestamp=None, reserved_percentage=0, timestamp=None, reserved_percentage=0,
driver_handles_share_servers=False, driver_handles_share_servers=False,
snapshot_support=True, snapshot_support=True,
replication_type=None,
pools=[dict(pool_name='pool1', pools=[dict(pool_name='pool1',
total_capacity_gb=51, total_capacity_gb=51,
free_capacity_gb=41, free_capacity_gb=41,
@ -90,6 +91,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
timestamp=None, reserved_percentage=0, timestamp=None, reserved_percentage=0,
driver_handles_share_servers=False, driver_handles_share_servers=False,
snapshot_support=True, snapshot_support=True,
replication_type=None,
pools=[dict(pool_name='pool2', pools=[dict(pool_name='pool2',
total_capacity_gb=52, total_capacity_gb=52,
free_capacity_gb=42, free_capacity_gb=42,
@ -101,6 +103,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
timestamp=None, reserved_percentage=0, timestamp=None, reserved_percentage=0,
driver_handles_share_servers=False, driver_handles_share_servers=False,
snapshot_support=True, snapshot_support=True,
replication_type=None,
pools=[dict(pool_name='pool3', pools=[dict(pool_name='pool3',
total_capacity_gb=53, total_capacity_gb=53,
free_capacity_gb=43, free_capacity_gb=43,
@ -113,6 +116,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
timestamp=None, reserved_percentage=0, timestamp=None, reserved_percentage=0,
driver_handles_share_servers=False, driver_handles_share_servers=False,
snapshot_support=True, snapshot_support=True,
replication_type=None,
pools=[dict(pool_name='pool4a', pools=[dict(pool_name='pool4a',
total_capacity_gb=541, total_capacity_gb=541,
free_capacity_gb=441, free_capacity_gb=441,
@ -133,6 +137,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
timestamp=None, reserved_percentage=0, timestamp=None, reserved_percentage=0,
driver_handles_share_servers=False, driver_handles_share_servers=False,
snapshot_support=True, snapshot_support=True,
replication_type=None,
pools=[dict(pool_name='pool5a', pools=[dict(pool_name='pool5a',
total_capacity_gb=551, total_capacity_gb=551,
free_capacity_gb=451, free_capacity_gb=451,
@ -150,6 +155,8 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
'host6@FFF': dict(share_backend_name='FFF', 'host6@FFF': dict(share_backend_name='FFF',
timestamp=None, reserved_percentage=0, timestamp=None, reserved_percentage=0,
driver_handles_share_servers=False, driver_handles_share_servers=False,
snapshot_support=True,
replication_type=None,
pools=[dict(pool_name='pool6a', pools=[dict(pool_name='pool6a',
total_capacity_gb='unknown', total_capacity_gb='unknown',
free_capacity_gb='unknown', free_capacity_gb='unknown',
@ -184,7 +191,9 @@ class FakeHostManager(host_manager.HostManager):
'thin_provisioning': False, 'thin_provisioning': False,
'reserved_percentage': 10, 'reserved_percentage': 10,
'timestamp': None, 'timestamp': None,
'snapshot_support': True}, 'snapshot_support': True,
'replication_type': 'writable',
},
'host2': {'total_capacity_gb': 2048, 'host2': {'total_capacity_gb': 2048,
'free_capacity_gb': 300, 'free_capacity_gb': 300,
'allocated_capacity_gb': 1748, 'allocated_capacity_gb': 1748,
@ -193,7 +202,9 @@ class FakeHostManager(host_manager.HostManager):
'thin_provisioning': True, 'thin_provisioning': True,
'reserved_percentage': 10, 'reserved_percentage': 10,
'timestamp': None, 'timestamp': None,
'snapshot_support': True}, 'snapshot_support': True,
'replication_type': 'readable',
},
'host3': {'total_capacity_gb': 512, 'host3': {'total_capacity_gb': 512,
'free_capacity_gb': 256, 'free_capacity_gb': 256,
'allocated_capacity_gb': 256, 'allocated_capacity_gb': 256,
@ -202,8 +213,9 @@ class FakeHostManager(host_manager.HostManager):
'thin_provisioning': False, 'thin_provisioning': False,
'consistency_group_support': 'host', 'consistency_group_support': 'host',
'reserved_percentage': 0, 'reserved_percentage': 0,
'snapshot_support': True,
'timestamp': None, 'timestamp': None,
'snapshot_support': True}, },
'host4': {'total_capacity_gb': 2048, 'host4': {'total_capacity_gb': 2048,
'free_capacity_gb': 200, 'free_capacity_gb': 200,
'allocated_capacity_gb': 1848, 'allocated_capacity_gb': 1848,
@ -212,7 +224,9 @@ class FakeHostManager(host_manager.HostManager):
'thin_provisioning': True, 'thin_provisioning': True,
'reserved_percentage': 5, 'reserved_percentage': 5,
'timestamp': None, 'timestamp': None,
'snapshot_support': True}, 'snapshot_support': True,
'replication_type': 'dr',
},
'host5': {'total_capacity_gb': 2048, 'host5': {'total_capacity_gb': 2048,
'free_capacity_gb': 500, 'free_capacity_gb': 500,
'allocated_capacity_gb': 1548, 'allocated_capacity_gb': 1548,
@ -221,15 +235,18 @@ class FakeHostManager(host_manager.HostManager):
'thin_provisioning': True, 'thin_provisioning': True,
'reserved_percentage': 5, 'reserved_percentage': 5,
'timestamp': None, 'timestamp': None,
'snapshot_support': True,
'consistency_group_support': 'pool', 'consistency_group_support': 'pool',
'snapshot_support': True}, 'replication_type': None,
},
'host6': {'total_capacity_gb': 'unknown', 'host6': {'total_capacity_gb': 'unknown',
'free_capacity_gb': 'unknown', 'free_capacity_gb': 'unknown',
'allocated_capacity_gb': 1548, 'allocated_capacity_gb': 1548,
'thin_provisioning': False, 'thin_provisioning': False,
'reserved_percentage': 5, 'reserved_percentage': 5,
'snapshot_support': True,
'timestamp': None, 'timestamp': None,
'snapshot_support': True}, },
} }
@ -275,3 +292,45 @@ class FakeWeigher2(base_host_weigher.BaseHostWeigher):
class FakeClass(object): class FakeClass(object):
def __init__(self): def __init__(self):
pass pass
def fake_replica_request_spec(**kwargs):
request_spec = {
'share_properties': {
'id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'name': 'fakename',
'size': 1,
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'availability_zone': 'fake_az',
'replication_type': 'dr',
},
'share_instance_properties': {
'id': '8d5566df-1e83-4373-84b8-6f8153a0ac41',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'host': 'openstack@BackendZ#PoolA',
'status': 'available',
'availability_zone_id': 'f6e146d0-65f0-11e5-9d70-feff819cdc9f',
'share_network_id': '4ccd5318-65f1-11e5-9d70-feff819cdc9f',
'share_server_id': '53099868-65f1-11e5-9d70-feff819cdc9f',
},
'share_proto': 'nfs',
'share_id': 'f0e4bb5e-65f0-11e5-9d70-feff819cdc9f',
'snapshot_id': None,
'share_type': 'fake_share_type',
'consistency_group': None,
}
request_spec.update(kwargs)
return request_spec
def get_fake_host(host_name=None):
class FakeHost(object):
def __init__(self, host_name=None):
self.host = host_name or 'openstack@BackendZ#PoolA'
class FakeWeightedHost(object):
def __init__(self, host_name=None):
self.obj = FakeHost(host_name=host_name)
return FakeWeightedHost(host_name=host_name)

View File

@ -199,6 +199,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': False, 'consistency_group_support': False,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, { }, {
'name': 'host2@back1#BBB', 'name': 'host2@back1#BBB',
@ -222,6 +223,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': False, 'consistency_group_support': False,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, { }, {
'name': 'host2@back2#CCC', 'name': 'host2@back2#CCC',
@ -245,6 +247,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': False, 'consistency_group_support': False,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, },
] ]
@ -290,6 +293,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': False, 'consistency_group_support': False,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, { }, {
'name': 'host2@BBB#pool2', 'name': 'host2@BBB#pool2',
@ -314,6 +318,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': False, 'consistency_group_support': False,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, { }, {
'name': 'host3@CCC#pool3', 'name': 'host3@CCC#pool3',
@ -338,6 +343,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': 'pool', 'consistency_group_support': 'pool',
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, { }, {
'name': 'host4@DDD#pool4a', 'name': 'host4@DDD#pool4a',
@ -362,6 +368,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': 'host', 'consistency_group_support': 'host',
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, { }, {
'name': 'host4@DDD#pool4b', 'name': 'host4@DDD#pool4b',
@ -386,6 +393,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': 'host', 'consistency_group_support': 'host',
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, },
] ]
@ -443,6 +451,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': False, 'consistency_group_support': False,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, { }, {
'name': 'host2@back1#BBB', 'name': 'host2@back1#BBB',
@ -466,6 +475,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': False, 'consistency_group_support': False,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, },
] ]
@ -515,6 +525,7 @@ class HostManagerTestCase(test.TestCase):
'consistency_group_support': False, 'consistency_group_support': False,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,
'replication_type': None,
}, },
}, },
] ]

View File

@ -110,3 +110,10 @@ class SchedulerRpcAPITestCase(test.TestCase):
request_spec='fake_request_spec', request_spec='fake_request_spec',
filter_properties='filter_properties', filter_properties='filter_properties',
version='1.4') version='1.4')
def test_create_share_replica(self):
self._test_scheduler_api('create_share_replica',
rpc_method='cast',
request_spec='fake_request_spec',
filter_properties='filter_properties',
version='1.5')

View File

@ -34,6 +34,7 @@ from manila.share import api as share_api
from manila.share import share_types from manila.share import share_types
from manila import test from manila import test
from manila.tests import db_utils from manila.tests import db_utils
from manila.tests import fake_share as fakes
from manila.tests import utils as test_utils from manila.tests import utils as test_utils
from manila import utils from manila import utils
@ -213,12 +214,9 @@ class ShareAPITestCase(test.TestCase):
share_type_id=share_type_id, share_type_id=share_type_id,
) )
share_instance = db_utils.create_share_instance(share_id=share['id']) share_instance = db_utils.create_share_instance(share_id=share['id'])
share_metadata = {'fake': 'fake'}
share_type = {'fake': 'fake'} share_type = {'fake': 'fake'}
self.mock_object(db_api, 'share_instance_create', self.mock_object(db_api, 'share_instance_create',
mock.Mock(return_value=share_instance)) mock.Mock(return_value=share_instance))
self.mock_object(db_api, 'share_metadata_get',
mock.Mock(return_value=share_metadata))
self.mock_object(db_api, 'share_type_get', self.mock_object(db_api, 'share_type_get',
mock.Mock(return_value=share_type)) mock.Mock(return_value=share_type))
az_mock = mock.Mock() az_mock = mock.Mock()
@ -701,8 +699,6 @@ class ShareAPITestCase(test.TestCase):
'availability_zone_id': 'fake_id', 'availability_zone_id': 'fake_id',
} }
) )
db_api.share_metadata_get.assert_called_once_with(self.context,
share['id'])
db_api.share_type_get.assert_called_once_with(self.context, db_api.share_type_get.assert_called_once_with(self.context,
share['share_type_id']) share['share_type_id'])
self.api.share_rpcapi.create_share_instance.assert_called_once_with( self.api.share_rpcapi.create_share_instance.assert_called_once_with(
@ -1090,6 +1086,17 @@ class ShareAPITestCase(test.TestCase):
self.assertRaises(exception.InvalidShare, self.api.delete, self.assertRaises(exception.InvalidShare, self.api.delete,
self.context, share) self.context, share)
def test_delete_share_has_replicas(self):
share = self._setup_delete_mocks(constants.STATUS_AVAILABLE,
replication_type='writable')
db_utils.create_share_replica(share_id=share['id'],
replica_state='in_sync')
db_utils.create_share_replica(share_id=share['id'],
replica_state='out_of_sync')
self.assertRaises(exception.Conflict, self.api.delete,
self.context, share)
@mock.patch.object(db_api, 'count_cgsnapshot_members_in_share', @mock.patch.object(db_api, 'count_cgsnapshot_members_in_share',
mock.Mock(return_value=2)) mock.Mock(return_value=2))
def test_delete_dependent_cgsnapshot_members(self): def test_delete_dependent_cgsnapshot_members(self):
@ -1704,6 +1711,27 @@ class ShareAPITestCase(test.TestCase):
self.assertRaises(exception.InvalidShare, self.api.migrate_share, self.assertRaises(exception.InvalidShare, self.api.migrate_share,
self.context, share, host, True) self.context, share, host, True)
def test_migrate_share_has_replicas(self):
host = 'fake2@backend#pool'
share = db_utils.create_share(
host='fake@backend#pool', status=constants.STATUS_AVAILABLE,
replication_type='dr')
for i in range(1, 4):
db_utils.create_share_replica(
share_id=share['id'], replica_state='in_sync')
self.mock_object(db_api, 'share_snapshot_get_all_for_share',
mock.Mock(return_value=True))
mock_log = self.mock_object(share_api, 'LOG')
mock_snapshot_get_call = self.mock_object(
db_api, 'share_snapshot_get_all_for_share')
# Share was updated after adding replicas, grabbing it again.
share = db_api.share_get(self.context, share['id'])
self.assertRaises(exception.Conflict, self.api.migrate_share,
self.context, share, host, True)
self.assertTrue(mock_log.error.called)
self.assertFalse(mock_snapshot_get_call.called)
def test_migrate_share_invalid_host(self): def test_migrate_share_invalid_host(self):
host = 'fake@backend#pool' host = 'fake@backend#pool'
share = db_utils.create_share( share = db_utils.create_share(
@ -1745,6 +1773,211 @@ class ShareAPITestCase(test.TestCase):
db_api.share_update.assert_any_call( db_api.share_update.assert_any_call(
mock.ANY, share['id'], mock.ANY) mock.ANY, share['id'], mock.ANY)
@ddt.data({}, {'replication_type': None})
def test_create_share_replica_invalid_share_type(self, attributes):
share = fakes.fake_share(id='FAKE_SHARE_ID', **attributes)
mock_request_spec_call = self.mock_object(
self.api, '_create_share_instance_and_get_request_spec')
mock_db_update_call = self.mock_object(db_api, 'share_replica_update')
mock_scheduler_rpcapi_call = self.mock_object(
self.api.scheduler_rpcapi, 'create_share_replica')
self.assertRaises(exception.InvalidShare,
self.api.create_share_replica,
self.context, share)
self.assertFalse(mock_request_spec_call.called)
self.assertFalse(mock_db_update_call.called)
self.assertFalse(mock_scheduler_rpcapi_call.called)
def test_create_share_replica_busy_share(self):
share = fakes.fake_share(
id='FAKE_SHARE_ID',
task_state='doing_something_real_important',
is_busy=True,
replication_type='dr')
mock_request_spec_call = self.mock_object(
self.api, '_create_share_instance_and_get_request_spec')
mock_db_update_call = self.mock_object(db_api, 'share_replica_update')
mock_scheduler_rpcapi_call = self.mock_object(
self.api.scheduler_rpcapi, 'create_share_replica')
self.assertRaises(exception.ShareBusyException,
self.api.create_share_replica,
self.context, share)
self.assertFalse(mock_request_spec_call.called)
self.assertFalse(mock_db_update_call.called)
self.assertFalse(mock_scheduler_rpcapi_call.called)
@ddt.data(None, [])
def test_create_share_replica_no_active_replica(self, active_replicas):
share = fakes.fake_share(
id='FAKE_SHARE_ID', replication_type='dr')
mock_request_spec_call = self.mock_object(
self.api, '_create_share_instance_and_get_request_spec')
mock_db_update_call = self.mock_object(db_api, 'share_replica_update')
mock_scheduler_rpcapi_call = self.mock_object(
self.api.scheduler_rpcapi, 'create_share_replica')
self.mock_object(db_api, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=active_replicas))
self.assertRaises(exception.ReplicationException,
self.api.create_share_replica,
self.context, share)
self.assertFalse(mock_request_spec_call.called)
self.assertFalse(mock_db_update_call.called)
self.assertFalse(mock_scheduler_rpcapi_call.called)
def test_create_share_replica(self):
request_spec = fakes.fake_replica_request_spec()
replica = request_spec['share_instance_properties']
share = fakes.fake_share(
id=replica['share_id'], replication_type='dr')
fake_replica = fakes.fake_replica(replica['id'])
fake_request_spec = fakes.fake_replica_request_spec()
self.mock_object(db_api, 'share_replicas_get_available_active_replica',
mock.Mock(return_value='FAKE_ACTIVE_REPLICA'))
self.mock_object(
share_api.API, '_create_share_instance_and_get_request_spec',
mock.Mock(return_value=(fake_request_spec, fake_replica)))
self.mock_object(db_api, 'share_replica_update')
mock_sched_rpcapi_call = self.mock_object(
self.api.scheduler_rpcapi, 'create_share_replica')
result = self.api.create_share_replica(
self.context, share, availability_zone='FAKE_AZ')
self.assertTrue(mock_sched_rpcapi_call.called)
self.assertEqual(replica, result)
def test_delete_last_active_replica(self):
fake_replica = fakes.fake_replica(
share_id='FAKE_SHARE_ID',
replica_state=constants.REPLICA_STATE_ACTIVE)
self.mock_object(db_api, 'share_replicas_get_all_by_share',
mock.Mock(return_value=[fake_replica]))
mock_log = self.mock_object(share_api.LOG, 'info')
self.assertRaises(
exception.ReplicationException, self.api.delete_share_replica,
self.context, fake_replica)
self.assertFalse(mock_log.called)
def test_delete_share_replica_no_host(self):
replica = fakes.fake_replica('FAKE_ID', host='')
mock_sched_rpcapi_call = self.mock_object(
self.share_rpcapi, 'delete_share_replica')
mock_db_replica_delete_call = self.mock_object(
db_api, 'share_replica_delete')
mock_db_update_call = self.mock_object(db_api, 'share_replica_update')
self.api.delete_share_replica(self.context, replica)
self.assertFalse(mock_sched_rpcapi_call.called)
mock_db_replica_delete_call.assert_called_once_with(
self.context, replica['id'])
mock_db_update_call.assert_called_once_with(
self.context, replica['id'],
{'terminated_at': mock.ANY})
@ddt.data(True, False)
def test_delete_share_replica(self, force):
replica = fakes.fake_replica('FAKE_ID', host='HOSTA@BackendB#PoolC')
mock_sched_rpcapi_call = self.mock_object(
self.share_rpcapi, 'delete_share_replica')
mock_db_update_call = self.mock_object(db_api, 'share_replica_update')
self.api.delete_share_replica(self.context, replica, force=force)
mock_sched_rpcapi_call.assert_called_once_with(
self.context, replica['id'],
'HOSTA@BackendB', share_id=replica['share_id'], force=force)
mock_db_update_call.assert_called_once_with(
self.context, replica['id'],
{'status': constants.STATUS_DELETING,
'terminated_at': mock.ANY})
@ddt.data(constants.STATUS_CREATING, constants.STATUS_DELETING,
constants.STATUS_ERROR, constants.STATUS_EXTENDING,
constants.STATUS_REPLICATION_CHANGE, constants.STATUS_MANAGING,
constants.STATUS_ERROR_DELETING)
def test_promote_share_replica_non_available_status(self, status):
replica = fakes.fake_replica(
status=status, replica_state=constants.REPLICA_STATE_IN_SYNC)
mock_extract_host_call = self.mock_object(
share_api.share_utils, 'extract_host')
mock_rpcapi_promote_share_replica_call = self.mock_object(
self.share_rpcapi, 'promote_share_replica')
self.assertRaises(exception.ReplicationException,
self.api.promote_share_replica,
self.context,
replica)
self.assertFalse(mock_extract_host_call.called)
self.assertFalse(mock_rpcapi_promote_share_replica_call.called)
@ddt.data(constants.REPLICA_STATE_OUT_OF_SYNC, constants.STATUS_ERROR)
def test_promote_share_replica_out_of_sync_non_admin(self, replica_state):
fake_user_context = context.RequestContext(
user_id=None, project_id=None, is_admin=False,
read_deleted='no', overwrite=False)
replica = fakes.fake_replica(
status=constants.STATUS_AVAILABLE,
replica_state=replica_state)
mock_extract_host_call = self.mock_object(
share_api.share_utils, 'extract_host')
mock_rpcapi_promote_share_replica_call = self.mock_object(
self.share_rpcapi, 'promote_share_replica')
self.assertRaises(exception.AdminRequired,
self.api.promote_share_replica,
fake_user_context,
replica)
self.assertFalse(mock_extract_host_call.called)
self.assertFalse(mock_rpcapi_promote_share_replica_call.called)
@ddt.data(constants.REPLICA_STATE_OUT_OF_SYNC, constants.STATUS_ERROR)
def test_promote_share_replica_admin_authorized(self, replica_state):
replica = fakes.fake_replica(
status=constants.STATUS_AVAILABLE,
replica_state=replica_state, host='HOSTA@BackendB#PoolC')
self.mock_object(db_api, 'share_replica_get',
mock.Mock(return_value=replica))
mock_extract_host_call = self.mock_object(
share_api.share_utils, 'extract_host',
mock.Mock(return_value='HOSTA'))
mock_rpcapi_promote_share_replica_call = self.mock_object(
self.share_rpcapi, 'promote_share_replica')
mock_db_update_call = self.mock_object(db_api, 'share_replica_update')
retval = self.api.promote_share_replica(
self.context, replica)
self.assertEqual(replica, retval)
self.assertTrue(mock_extract_host_call.called)
mock_db_update_call.assert_called_once_with(
self.context, replica['id'],
{'status': constants.STATUS_REPLICATION_CHANGE})
mock_rpcapi_promote_share_replica_call.assert_called_once_with(
self.context, replica['id'], 'HOSTA', share_id=replica['share_id'])
def test_promote_share_replica(self):
replica = fakes.fake_replica('FAKE_ID', host='HOSTA@BackendB#PoolC')
self.mock_object(db_api, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db_api, 'share_replica_update')
mock_extract_host_call = self.mock_object(
share_api.share_utils, 'extract_host',
mock.Mock(return_value='HOSTA'))
mock_sched_rpcapi_call = self.mock_object(
self.share_rpcapi, 'promote_share_replica')
result = self.api.promote_share_replica(self.context, replica)
mock_sched_rpcapi_call.assert_called_once_with(
self.context, replica['id'], 'HOSTA', share_id=replica['share_id'])
mock_extract_host_call.assert_called_once_with('HOSTA@BackendB#PoolC')
self.assertEqual(replica, result)
class OtherTenantsShareActionsTestCase(test.TestCase): class OtherTenantsShareActionsTestCase(test.TestCase):
def setUp(self): def setUp(self):

View File

@ -699,3 +699,29 @@ class ShareDriverTestCase(test.TestCase):
'fake_share', 'fake_share',
'fake_access_rules' 'fake_access_rules'
) )
def test_create_replica(self):
share_driver = self._instantiate_share_driver(None, True)
self.assertRaises(NotImplementedError,
share_driver.create_replica,
'fake_context', 'fake_active_replica',
'fake_new_replica', [])
def test_delete_replica(self):
share_driver = self._instantiate_share_driver(None, True)
self.assertRaises(NotImplementedError,
share_driver.delete_replica,
'fake_context', 'fake_active_replica',
'fake_replica')
def test_promote_replica(self):
share_driver = self._instantiate_share_driver(None, True)
self.assertRaises(NotImplementedError,
share_driver.promote_replica,
'fake_context', [], 'fake_replica', [])
def test_update_replica_state(self):
share_driver = self._instantiate_share_driver(None, True)
self.assertRaises(NotImplementedError,
share_driver.update_replica_state,
'fake_context', [], 'fake_replica')

View File

@ -15,9 +15,11 @@
"""Test of Share Manager for Manila.""" """Test of Share Manager for Manila."""
import datetime import datetime
import random
import ddt import ddt
import mock import mock
from oslo_concurrency import lockutils
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import importutils from oslo_utils import importutils
from oslo_utils import timeutils from oslo_utils import timeutils
@ -36,11 +38,43 @@ from manila.share import migration
from manila.share import rpcapi from manila.share import rpcapi
from manila.share import share_types from manila.share import share_types
from manila import test from manila import test
from manila.tests.api import fakes as test_fakes
from manila.tests import db_utils from manila.tests import db_utils
from manila.tests import fake_share as fakes
from manila.tests import fake_utils
from manila.tests import utils as test_utils from manila.tests import utils as test_utils
from manila import utils from manila import utils
def fake_replica(**kwargs):
return fakes.fake_replica(for_manager=True, **kwargs)
class LockedOperationsTestCase(test.TestCase):
class FakeManager:
@manager.locked_share_replica_operation
def fake_replica_operation(self, context, replica, share_id=None):
pass
def setUp(self):
super(self.__class__, self).setUp()
self.manager = self.FakeManager()
self.fake_context = test_fakes.FakeRequestContext
self.lock_call = self.mock_object(
utils, 'synchronized', mock.Mock(return_value=lambda f: f))
@ddt.data({'id': 'FAKE_REPLICA_ID'}, 'FAKE_REPLICA_ID')
@ddt.unpack
def test_locked_share_replica_operation(self, **replica):
self.manager.fake_replica_operation(self.fake_context, replica,
share_id='FAKE_SHARE_ID')
self.assertTrue(self.lock_call.called)
@ddt.ddt @ddt.ddt
class ShareManagerTestCase(test.TestCase): class ShareManagerTestCase(test.TestCase):
@ -55,6 +89,10 @@ class ShareManagerTestCase(test.TestCase):
self.mock_object(self.share_manager.driver, 'check_for_setup_error') self.mock_object(self.share_manager.driver, 'check_for_setup_error')
self.context = context.get_admin_context() self.context = context.get_admin_context()
self.share_manager.driver.initialized = True self.share_manager.driver.initialized = True
mock.patch.object(
lockutils, 'lock', fake_utils.get_fake_lock_context())
self.synchronized_lock_decorator_call = self.mock_object(
utils, 'synchronized', mock.Mock(return_value=lambda f: f))
def test_share_manager_instance(self): def test_share_manager_instance(self):
fake_service_name = "fake_service" fake_service_name = "fake_service"
@ -161,6 +199,10 @@ class ShareManagerTestCase(test.TestCase):
"delete_consistency_group", "delete_consistency_group",
"create_cgsnapshot", "create_cgsnapshot",
"delete_cgsnapshot", "delete_cgsnapshot",
"create_share_replica",
"delete_share_replica",
"promote_share_replica",
"periodic_share_replica_update",
) )
def test_call_driver_when_its_init_failed(self, method_name): def test_call_driver_when_its_init_failed(self, method_name):
self.mock_object(self.share_manager.driver, 'do_setup', self.mock_object(self.share_manager.driver, 'do_setup',
@ -477,6 +519,660 @@ class ShareManagerTestCase(test.TestCase):
self.assertTrue(len(shr['export_location']) > 0) self.assertTrue(len(shr['export_location']) > 0)
self.assertEqual(2, len(shr['export_locations'])) self.assertEqual(2, len(shr['export_locations']))
def test_create_share_instance_for_share_with_replication_support(self):
"""Test update call is made to update replica_state."""
share = db_utils.create_share(replication_type='writable')
share_id = share['id']
self.share_manager.create_share_instance(self.context,
share.instance['id'])
self.assertEqual(share_id, db.share_get(context.get_admin_context(),
share_id).id)
shr = db.share_get(self.context, share_id)
shr_instance = db.share_instance_get(self.context,
share.instance['id'])
self.assertEqual(constants.STATUS_AVAILABLE, shr['status'],)
self.assertEqual(constants.REPLICA_STATE_ACTIVE,
shr_instance['replica_state'])
@ddt.data([], None)
def test_create_share_replica_no_active_replicas(self, active_replicas):
replica = fake_replica()
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=active_replicas))
self.mock_object(
db, 'share_replica_get', mock.Mock(return_value=replica))
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_driver_replica_call = self.mock_object(
self.share_manager.driver, 'create_replica')
self.assertRaises(exception.ReplicationException,
self.share_manager.create_share_replica,
self.context, replica)
mock_replica_update_call.assert_called_once_with(
mock.ANY, replica['id'], {'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})
self.assertFalse(mock_driver_replica_call.called)
def test_create_share_replica_with_share_network_id_and_not_dhss(self):
replica = fake_replica()
manager.CONF.set_default('driver_handles_share_servers', False)
self.mock_object(db, 'share_access_get_all_for_share',
mock.Mock(return_value=[]))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=fake_replica(id='fake2')))
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_driver_replica_call = self.mock_object(
self.share_manager.driver, 'create_replica')
self.assertRaises(exception.InvalidDriverMode,
self.share_manager.create_share_replica,
self.context, replica)
mock_replica_update_call.assert_called_once_with(
mock.ANY, replica['id'], {'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})
self.assertFalse(mock_driver_replica_call.called)
def test_create_share_replica_with_share_server_exception(self):
replica = fake_replica()
manager.CONF.set_default('driver_handles_share_servers', True)
self.mock_object(db, 'share_instance_access_copy',
mock.Mock(return_value=[]))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=fake_replica(id='fake2')))
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_driver_replica_call = self.mock_object(
self.share_manager.driver, 'create_replica')
self.assertRaises(exception.NotFound,
self.share_manager.create_share_replica,
self.context, replica)
mock_replica_update_call.assert_called_once_with(
mock.ANY, replica['id'], {'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})
self.assertFalse(mock_driver_replica_call.called)
def test_create_share_replica_driver_error_on_creation(self):
fake_access_rules = [{'id': '1'}, {'id': '2'}, {'id': '3'}]
replica = fake_replica(share_network_id='')
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_instance_access_copy',
mock.Mock(return_value=fake_access_rules))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=fake_replica(id='fake2')))
self.mock_object(self.share_manager,
'_provide_share_server_for_share',
mock.Mock(return_value=('FAKE_SERVER', replica)))
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_export_locs_update_call = self.mock_object(
db, 'share_export_locations_update')
mock_log_error = self.mock_object(manager.LOG, 'error')
mock_log_info = self.mock_object(manager.LOG, 'info')
self.mock_object(db, 'share_instance_access_get',
mock.Mock(return_value=fake_access_rules[0]))
mock_share_replica_access_update = self.mock_object(
db, 'share_instance_update_access_status')
self.mock_object(self.share_manager, '_get_share_server')
self.mock_object(self.share_manager.driver, 'create_replica',
mock.Mock(side_effect=exception.ManilaException))
self.assertRaises(exception.ManilaException,
self.share_manager.create_share_replica,
self.context, replica)
mock_replica_update_call.assert_called_once_with(
mock.ANY, replica['id'], {'status': constants.STATUS_ERROR,
'replica_state': constants.STATUS_ERROR})
self.assertEqual(1, mock_share_replica_access_update.call_count)
self.assertFalse(mock_export_locs_update_call.called)
self.assertTrue(mock_log_error.called)
self.assertFalse(mock_log_info.called)
def test_create_share_replica_invalid_locations_state(self):
driver_retval = {
'export_locations': 'FAKE_EXPORT_LOC',
}
replica = fake_replica(share_network='')
fake_access_rules = [{'id': '1'}, {'id': '2'}]
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=fake_replica(id='fake2')))
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_instance_access_copy',
mock.Mock(return_value=fake_access_rules))
self.mock_object(self.share_manager,
'_provide_share_server_for_share',
mock.Mock(return_value=('FAKE_SERVER', replica)))
self.mock_object(self.share_manager, '_get_share_server')
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_export_locs_update_call = self.mock_object(
db, 'share_export_locations_update')
mock_log_info = self.mock_object(manager.LOG, 'info')
mock_log_warning = self.mock_object(manager.LOG, 'warning')
mock_log_error = self.mock_object(manager.LOG, 'error')
self.mock_object(self.share_manager.driver, 'create_replica',
mock.Mock(return_value=driver_retval))
self.mock_object(db, 'share_instance_access_get',
mock.Mock(return_value=fake_access_rules[0]))
mock_share_replica_access_update = self.mock_object(
db, 'share_instance_update_access_status')
self.share_manager.create_share_replica(self.context, replica)
self.assertFalse(mock_replica_update_call.called)
self.assertEqual(1, mock_share_replica_access_update.call_count)
self.assertFalse(mock_export_locs_update_call.called)
self.assertTrue(mock_log_info.called)
self.assertTrue(mock_log_warning.called)
self.assertFalse(mock_log_error.called)
def test_create_share_replica_no_availability_zone(self):
replica = fake_replica(
availability_zone=None, share_network='',
replica_state=constants.REPLICA_STATE_OUT_OF_SYNC)
manager.CONF.set_default('storage_availability_zone', 'fake_az')
fake_access_rules = [{'id': '1'}, {'id': '2'}, {'id': '3'}]
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_instance_access_copy',
mock.Mock(return_value=fake_access_rules))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=fake_replica(id='fake2')))
self.mock_object(self.share_manager,
'_provide_share_server_for_share',
mock.Mock(return_value=('FAKE_SERVER', replica)))
mock_replica_update_call = self.mock_object(
db, 'share_replica_update', mock.Mock(return_value=replica))
mock_calls = [
mock.call(mock.ANY, replica['id'],
{'availability_zone': 'fake_az'}, with_share_data=True),
mock.call(mock.ANY, replica['id'],
{'status': constants.STATUS_AVAILABLE,
'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC}),
]
mock_export_locs_update_call = self.mock_object(
db, 'share_export_locations_update')
mock_log_info = self.mock_object(manager.LOG, 'info')
mock_log_warning = self.mock_object(manager.LOG, 'warning')
mock_log_error = self.mock_object(manager.LOG, 'warning')
self.mock_object(db, 'share_instance_access_get',
mock.Mock(return_value=fake_access_rules[0]))
mock_share_replica_access_update = self.mock_object(
self.share_manager, '_update_share_replica_access_rules_state')
self.mock_object(
self.share_manager.driver, 'create_replica',
mock.Mock(return_value=replica))
self.mock_object(self.share_manager, '_get_share_server')
self.share_manager.create_share_replica(self.context, replica)
mock_replica_update_call.assert_has_calls(mock_calls, any_order=False)
mock_share_replica_access_update.assert_called_once_with(
mock.ANY, replica['id'], replica['access_rules_status'])
self.assertTrue(mock_export_locs_update_call.called)
self.assertTrue(mock_log_info.called)
self.assertFalse(mock_log_warning.called)
self.assertFalse(mock_log_error.called)
def test_create_share_replica(self):
replica = fake_replica(
share_network='', replica_state=constants.REPLICA_STATE_IN_SYNC)
fake_access_rules = [{'id': '1'}, {'id': '2'}, {'id': '3'}]
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_instance_access_copy',
mock.Mock(return_value=fake_access_rules))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=fake_replica(id='fake2')))
self.mock_object(self.share_manager,
'_provide_share_server_for_share',
mock.Mock(return_value=('FAKE_SERVER', replica)))
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_export_locs_update_call = self.mock_object(
db, 'share_export_locations_update')
mock_log_info = self.mock_object(manager.LOG, 'info')
mock_log_warning = self.mock_object(manager.LOG, 'warning')
mock_log_error = self.mock_object(manager.LOG, 'warning')
self.mock_object(db, 'share_instance_access_get',
mock.Mock(return_value=fake_access_rules[0]))
mock_share_replica_access_update = self.mock_object(
db, 'share_instance_update_access_status')
self.mock_object(
self.share_manager.driver, 'create_replica',
mock.Mock(return_value=replica))
self.mock_object(self.share_manager, '_get_share_server')
self.share_manager.create_share_replica(self.context, replica)
mock_replica_update_call.assert_called_once_with(
mock.ANY, replica['id'],
{'status': constants.STATUS_AVAILABLE,
'replica_state': constants.REPLICA_STATE_IN_SYNC})
self.assertEqual(1, mock_share_replica_access_update.call_count)
self.assertTrue(mock_export_locs_update_call.called)
self.assertTrue(mock_log_info.called)
self.assertFalse(mock_log_warning.called)
self.assertFalse(mock_log_error.called)
def test_delete_share_replica_access_rules_exception(self):
replica = fake_replica()
active_replica = fake_replica(id='Current_active_replica')
mock_error_log = self.mock_object(manager.LOG, 'error')
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=active_replica))
self.mock_object(self.share_manager, '_get_share_server')
self.mock_object(self.share_manager.access_helper,
'update_access_rules')
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_replica_delete_call = self.mock_object(db, 'share_replica_delete')
mock_drv_delete_replica_call = self.mock_object(
self.share_manager.driver, 'delete_replica')
self.mock_object(
self.share_manager.access_helper, 'update_access_rules',
mock.Mock(side_effect=exception.ManilaException))
self.assertRaises(exception.ManilaException,
self.share_manager.delete_share_replica,
self.context, replica, share_id=replica['share_id'])
mock_replica_update_call.assert_called_once_with(
mock.ANY, replica['id'], {'status': constants.STATUS_ERROR})
self.assertFalse(mock_drv_delete_replica_call.called)
self.assertFalse(mock_replica_delete_call.called)
self.assertFalse(mock_error_log.called)
def test_delete_share_replica_drv_misbehavior_ignored_with_the_force(self):
replica = fake_replica()
active_replica = fake_replica(id='Current_active_replica')
mock_error_log = self.mock_object(manager.LOG, 'error')
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=active_replica))
self.mock_object(self.share_manager, '_get_share_server')
self.mock_object(self.share_manager.access_helper,
'update_access_rules')
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_replica_delete_call = self.mock_object(db, 'share_replica_delete')
mock_drv_delete_replica_call = self.mock_object(
self.share_manager.driver, 'delete_replica',
mock.Mock(side_effect=exception.ManilaException))
self.mock_object(
self.share_manager.access_helper, 'update_access_rules')
self.share_manager.delete_share_replica(
self.context, replica, share_id=replica['share_id'], force=True)
self.assertFalse(mock_replica_update_call.called)
self.assertTrue(mock_drv_delete_replica_call.called)
self.assertTrue(mock_replica_delete_call.called)
self.assertEqual(1, mock_error_log.call_count)
def test_delete_share_replica_driver_exception(self):
replica = fake_replica()
active_replica = fake_replica(id='Current_active_replica')
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=active_replica))
self.mock_object(self.share_manager, '_get_share_server')
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_replica_delete_call = self.mock_object(db, 'share_replica_delete')
self.mock_object(
self.share_manager.access_helper, 'update_access_rules')
self.mock_object(self.share_manager.driver, 'delete_replica',
mock.Mock(side_effect=exception.ManilaException))
self.assertRaises(exception.ManilaException,
self.share_manager.delete_share_replica,
self.context, replica)
self.assertTrue(mock_replica_update_call.called)
self.assertFalse(mock_replica_delete_call.called)
def test_delete_share_replica_drv_exception_ignored_with_the_force(self):
replica = fake_replica()
active_replica = fake_replica(id='Current_active_replica')
mock_error_log = self.mock_object(manager.LOG, 'error')
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=active_replica))
self.mock_object(self.share_manager, '_get_share_server')
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_replica_delete_call = self.mock_object(db, 'share_replica_delete')
self.mock_object(
self.share_manager.access_helper, 'update_access_rules')
self.mock_object(self.share_manager.driver, 'delete_replica',
mock.Mock(side_effect=exception.ManilaException))
self.share_manager.delete_share_replica(
self.context, replica, share_id=replica['share_id'], force=True)
self.assertFalse(mock_replica_update_call.called)
self.assertTrue(mock_replica_delete_call.called)
self.assertEqual(1, mock_error_log.call_count)
def test_delete_share_replica_both_exceptions_ignored_with_the_force(self):
replica = fake_replica()
active_replica = fake_replica(id='Current_active_replica')
mock_error_log = self.mock_object(manager.LOG, 'error')
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=active_replica))
self.mock_object(self.share_manager, '_get_share_server')
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_replica_delete_call = self.mock_object(db, 'share_replica_delete')
self.mock_object(
self.share_manager.access_helper, 'update_access_rules',
mock.Mock(side_effect=exception.ManilaException))
self.mock_object(self.share_manager.driver, 'delete_replica',
mock.Mock(side_effect=exception.ManilaException))
self.share_manager.delete_share_replica(
self.context, replica, share_id=replica['share_id'], force=True)
mock_replica_update_call.assert_called_once_with(
mock.ANY, replica['id'], {'status': constants.STATUS_ERROR})
self.assertTrue(mock_replica_delete_call.called)
self.assertEqual(2, mock_error_log.call_count)
def test_delete_share_replica(self):
replica = fake_replica()
active_replica = fake_replica(id='current_active_replica')
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=active_replica))
self.mock_object(self.share_manager, '_get_share_server')
mock_info_log = self.mock_object(manager.LOG, 'info')
mock_replica_update_call = self.mock_object(db, 'share_replica_update')
mock_replica_delete_call = self.mock_object(db, 'share_replica_delete')
self.mock_object(
self.share_manager.access_helper, 'update_access_rules')
self.mock_object(self.share_manager.driver, 'delete_replica')
self.share_manager.delete_share_replica(self.context, replica)
self.assertFalse(mock_replica_update_call.called)
self.assertTrue(mock_replica_delete_call.called)
self.assertTrue(mock_info_log.called)
def test_promote_share_replica_no_active_replica(self):
replica = fake_replica()
replica_list = [replica]
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(self.share_manager, '_get_share_server')
self.mock_object(db, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=replica_list))
mock_info_log = self.mock_object(manager.LOG, 'info')
mock_driver_call = self.mock_object(self.share_manager.driver,
'promote_replica')
mock_replica_update = self.mock_object(db, 'share_replica_update')
expected_update_call = mock.call(
mock.ANY, replica['id'], {'status': constants.STATUS_AVAILABLE})
self.assertRaises(exception.ReplicationException,
self.share_manager.promote_share_replica,
self.context, replica)
self.assertFalse(mock_info_log.called)
self.assertFalse(mock_driver_call.called)
mock_replica_update.assert_has_calls([expected_update_call])
def test_promote_share_replica_driver_exception(self):
replica = fake_replica()
active_replica = fake_replica(
id='current_active_replica',
replica_state=constants.REPLICA_STATE_ACTIVE)
replica_list = [replica, active_replica]
self.mock_object(db, 'share_access_get_all_for_share',
mock.Mock(return_value=[]))
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(self.share_manager, '_get_share_server')
self.mock_object(db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=replica_list))
self.mock_object(self.share_manager.driver, 'promote_replica',
mock.Mock(side_effect=exception.ManilaException))
mock_info_log = self.mock_object(manager.LOG, 'info')
mock_replica_update = self.mock_object(db, 'share_replica_update')
expected_update_calls = [mock.call(
mock.ANY, r['id'], {'status': constants.STATUS_ERROR})
for r in(replica, active_replica)]
self.assertRaises(exception.ManilaException,
self.share_manager.promote_share_replica,
self.context, replica)
mock_replica_update.assert_has_calls(expected_update_calls)
self.assertFalse(mock_info_log.called)
@ddt.data([], None)
def test_promote_share_replica_driver_updates_nothing(self, retval):
replica = fake_replica()
active_replica = fake_replica(
id='current_active_replica',
replica_state=constants.REPLICA_STATE_ACTIVE)
replica_list = [replica, active_replica]
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_access_get_all_for_share',
mock.Mock(return_value=[]))
self.mock_object(self.share_manager, '_get_share_server')
self.mock_object(db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=replica_list))
self.mock_object(
self.share_manager.driver, 'promote_replica',
mock.Mock(return_value=retval))
mock_info_log = self.mock_object(manager.LOG, 'info')
mock_export_locs_update = self.mock_object(
db, 'share_export_locations_update')
mock_replica_update = self.mock_object(db, 'share_replica_update')
call_1 = mock.call(mock.ANY, replica['id'],
{'status': constants.STATUS_AVAILABLE,
'replica_state': constants.REPLICA_STATE_ACTIVE})
call_2 = mock.call(
mock.ANY, 'current_active_replica',
{'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC})
expected_update_calls = [call_1, call_2]
self.share_manager.promote_share_replica(self.context, replica)
self.assertFalse(mock_export_locs_update.called)
mock_replica_update.assert_has_calls(expected_update_calls,
any_order=True)
self.assertTrue(mock_info_log.called)
def test_promote_share_replica_driver_updates_replica_list(self):
replica = fake_replica()
active_replica = fake_replica(
id='current_active_replica',
replica_state=constants.REPLICA_STATE_ACTIVE)
replica_list = [replica, active_replica, fake_replica(id=3)]
updated_replica_list = [
{
'id': replica['id'],
'export_locations': ['TEST1', 'TEST2'],
'replica_state': constants.REPLICA_STATE_ACTIVE,
},
{
'id': 'current_active_replica',
'export_locations': 'junk_return_value',
'replica_state': constants.REPLICA_STATE_IN_SYNC,
},
{
'id': 'current_active_replica',
'export_locations': ['TEST1', 'TEST2'],
'replica_state': constants.STATUS_ERROR,
},
]
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_access_get_all_for_share',
mock.Mock(return_value=[]))
self.mock_object(self.share_manager, '_get_share_server')
self.mock_object(db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=replica_list))
self.mock_object(
self.share_manager.driver, 'promote_replica',
mock.Mock(return_value=updated_replica_list))
mock_info_log = self.mock_object(manager.LOG, 'info')
mock_export_locs_update = self.mock_object(
db, 'share_export_locations_update')
mock_replica_update = self.mock_object(db, 'share_replica_update')
reset_replication_change_call = mock.call(
mock.ANY, replica['id'], {'replica_state': constants.STATUS_ACTIVE,
'status': constants.STATUS_AVAILABLE})
self.share_manager.promote_share_replica(self.context, replica)
self.assertEqual(2, mock_export_locs_update.call_count)
self.assertEqual(3, mock_replica_update.call_count)
self.assertTrue(
reset_replication_change_call in mock_replica_update.mock_calls)
self.assertTrue(mock_info_log.called)
@ddt.data(constants.REPLICA_STATE_IN_SYNC,
constants.REPLICA_STATE_OUT_OF_SYNC)
def test_update_share_replica_state_driver_exception(self, replica_state):
mock_debug_log = self.mock_object(manager.LOG, 'debug')
replica = fake_replica(replica_state=replica_state)
self.mock_object(self.share_manager.db, 'share_replicas_get_all',
mock.Mock(return_value=[replica]))
self.mock_object(db, 'share_server_get',
mock.Mock(return_value='fake_share_server'))
self.share_manager.host = replica['host']
self.mock_object(self.share_manager.driver, 'update_replica_state',
mock.Mock(side_effect=exception.ManilaException))
mock_db_update_call = self.mock_object(
self.share_manager.db, 'share_replica_update')
self.assertRaises(exception.ManilaException,
self.share_manager.periodic_share_replica_update,
self.context)
self.assertFalse(mock_db_update_call.called)
self.assertEqual(1, mock_debug_log.call_count)
@ddt.data('openstack1@watson#_pool0', 'openstack1@newton#_pool0')
def test_periodic_share_replica_update(self, host):
mock_debug_log = self.mock_object(manager.LOG, 'debug')
replicas = [
fake_replica(host='openstack1@watson#pool4'),
fake_replica(host='openstack1@watson#pool5'),
fake_replica(host='openstack1@newton#pool5'),
fake_replica(host='openstack1@newton#pool5'),
]
self.mock_object(self.share_manager.db, 'share_replicas_get_all',
mock.Mock(return_value=replicas))
mock_update_method = self.mock_object(
self.share_manager, '_share_replica_update')
self.share_manager.host = host
self.share_manager.periodic_share_replica_update(self.context)
self.assertEqual(2, mock_update_method.call_count)
self.assertEqual(1, mock_debug_log.call_count)
def test__share_replica_update_driver_exception_ignored(self):
mock_debug_log = self.mock_object(manager.LOG, 'debug')
replica = fake_replica(replica_state=constants.STATUS_ERROR)
self.mock_object(self.share_manager.db, 'share_replica_get',
mock.Mock(return_value=replica))
self.mock_object(db, 'share_server_get',
mock.Mock(return_value='fake_share_server'))
self.share_manager.host = replica['host']
self.mock_object(self.share_manager.driver, 'update_replica_state',
mock.Mock(side_effect=exception.ManilaException))
mock_db_update_call = self.mock_object(
self.share_manager.db, 'share_replica_update')
self.share_manager._share_replica_update(
self.context, replica, share_id=replica['share_id'])
self.assertFalse(mock_db_update_call.called)
self.assertEqual(1, mock_debug_log.call_count)
@ddt.data({'status': constants.STATUS_AVAILABLE,
'replica_state': constants.REPLICA_STATE_ACTIVE, },
{'status': constants.STATUS_DELETING,
'replica_state': constants.REPLICA_STATE_IN_SYNC, },
{'status': constants.STATUS_CREATING,
'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC, },
{'status': constants.STATUS_MANAGING,
'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC, },
{'status': constants.STATUS_UNMANAGING,
'replica_state': constants.REPLICA_STATE_ACTIVE, },
{'status': constants.STATUS_EXTENDING,
'replica_state': constants.REPLICA_STATE_IN_SYNC, },
{'status': constants.STATUS_SHRINKING,
'replica_state': constants.REPLICA_STATE_IN_SYNC, })
def test__share_replica_update_unqualified_replica(self, state):
mock_debug_log = self.mock_object(manager.LOG, 'debug')
mock_warning_log = self.mock_object(manager.LOG, 'warning')
mock_driver_call = self.mock_object(
self.share_manager.driver, 'update_replica_state')
mock_db_update_call = self.mock_object(
self.share_manager.db, 'share_replica_update')
replica = fake_replica(**state)
self.mock_object(db, 'share_server_get',
mock.Mock(return_value='fake_share_server'))
self.mock_object(db, 'share_replica_get',
mock.Mock(return_value=replica))
self.share_manager._share_replica_update(self.context, replica,
share_id=replica['share_id'])
self.assertFalse(mock_debug_log.called)
self.assertFalse(mock_warning_log.called)
self.assertFalse(mock_driver_call.called)
self.assertFalse(mock_db_update_call.called)
@ddt.data(None, constants.REPLICA_STATE_IN_SYNC,
constants.REPLICA_STATE_OUT_OF_SYNC,
constants.REPLICA_STATE_ACTIVE,
constants.STATUS_ERROR)
def test__share_replica_update(self, retval):
mock_debug_log = self.mock_object(manager.LOG, 'debug')
mock_warning_log = self.mock_object(manager.LOG, 'warning')
replica_states = [constants.REPLICA_STATE_IN_SYNC,
constants.REPLICA_STATE_OUT_OF_SYNC]
replica = fake_replica(replica_state=random.choice(replica_states),
share_server='fake_share_server')
del replica['availability_zone']
self.mock_object(db, 'share_server_get',
mock.Mock(return_value='fake_share_server'))
mock_db_update_calls = []
self.mock_object(self.share_manager.db, 'share_replica_get',
mock.Mock(return_value=replica))
mock_driver_call = self.mock_object(
self.share_manager.driver, 'update_replica_state',
mock.Mock(return_value=retval))
mock_db_update_call = self.mock_object(
self.share_manager.db, 'share_replica_update')
self.share_manager._share_replica_update(
self.context, replica, share_id=replica['share_id'])
if retval == constants.REPLICA_STATE_ACTIVE:
self.assertEqual(1, mock_warning_log.call_count)
elif retval:
self.assertEqual(0, mock_warning_log.call_count)
mock_driver_call.assert_called_once_with(
self.context, replica, [], 'fake_share_server')
mock_db_update_call.assert_has_calls(mock_db_update_calls)
self.assertEqual(1, mock_debug_log.call_count)
def test_create_delete_share_snapshot(self): def test_create_delete_share_snapshot(self):
"""Test share's snapshot can be created and deleted.""" """Test share's snapshot can be created and deleted."""

View File

@ -41,11 +41,16 @@ class ShareRpcAPITestCase(test.TestCase):
) )
access = db_utils.create_access(share_id=share['id']) access = db_utils.create_access(share_id=share['id'])
snapshot = db_utils.create_snapshot(share_id=share['id']) snapshot = db_utils.create_snapshot(share_id=share['id'])
share_replica = db_utils.create_share_replica(
id='fake_replica',
share_id='fake_share_id',
)
share_server = db_utils.create_share_server() share_server = db_utils.create_share_server()
cg = {'id': 'fake_cg_id', 'host': 'fake_host'} cg = {'id': 'fake_cg_id', 'host': 'fake_host'}
cgsnapshot = {'id': 'fake_cg_id'} cgsnapshot = {'id': 'fake_cg_id'}
host = {'host': 'fake_host', 'capabilities': 1} host = {'host': 'fake_host', 'capabilities': 1}
self.fake_share = jsonutils.to_primitive(share) self.fake_share = jsonutils.to_primitive(share)
self.fake_share_replica = jsonutils.to_primitive(share_replica)
self.fake_access = jsonutils.to_primitive(access) self.fake_access = jsonutils.to_primitive(access)
self.fake_snapshot = jsonutils.to_primitive(snapshot) self.fake_snapshot = jsonutils.to_primitive(snapshot)
self.fake_share_server = jsonutils.to_primitive(share_server) self.fake_share_server = jsonutils.to_primitive(share_server)
@ -93,6 +98,9 @@ class ShareRpcAPITestCase(test.TestCase):
if 'dest_host' in expected_msg: if 'dest_host' in expected_msg:
del expected_msg['dest_host'] del expected_msg['dest_host']
expected_msg['host'] = self.fake_host expected_msg['host'] = self.fake_host
if 'share_replica' in expected_msg:
share_replica = expected_msg.pop('share_replica', None)
expected_msg['share_replica_id'] = share_replica['id']
if 'host' in kwargs: if 'host' in kwargs:
host = kwargs['host'] host = kwargs['host']
@ -246,6 +254,23 @@ class ShareRpcAPITestCase(test.TestCase):
share_instance=self.fake_share, share_instance=self.fake_share,
share_server=self.fake_share_server) share_server=self.fake_share_server)
def test_delete_share_replica(self):
self._test_share_api('delete_share_replica',
rpc_method='cast',
version='1.8',
share_replica_id=self.fake_share_replica['id'],
share_id=self.fake_share_replica['share_id'],
force=False,
host='fake_host')
def test_promote_share_replica(self):
self._test_share_api('promote_share_replica',
rpc_method='cast',
version='1.8',
share_replica_id=self.fake_share_replica['id'],
share_id=self.fake_share_replica['share_id'],
host='fake_host')
class Desthost(object): class Desthost(object):
host = 'fake_host' host = 'fake_host'
capabilities = 1 capabilities = 1

View File

@ -118,6 +118,13 @@ class ManilaExceptionTestCase(test.TestCase):
exc2 = exception.ManilaException(exc1) exc2 = exception.ManilaException(exc1)
self.assertEqual("test message.", exc2.msg) self.assertEqual("test message.", exc2.msg)
def test_replication_exception(self):
# Verify response code for exception.ReplicationException
reason = "Something bad happened."
e = exception.ReplicationException(reason=reason)
self.assertEqual(500, e.code)
self.assertIn(reason, e.msg)
class ManilaExceptionResponseCode400(test.TestCase): class ManilaExceptionResponseCode400(test.TestCase):
@ -448,6 +455,13 @@ class ManilaExceptionResponseCode404(test.TestCase):
self.assertEqual(404, e.code) self.assertEqual(404, e.code)
self.assertIn(instance_id, e.msg) self.assertIn(instance_id, e.msg)
def test_share_replica_not_found_exception(self):
# Verify response code for exception.ShareReplicaNotFound
replica_id = "FAKE_REPLICA_ID"
e = exception.ShareReplicaNotFound(replica_id=replica_id)
self.assertEqual(404, e.code)
self.assertIn(replica_id, e.msg)
def test_storage_resource_not_found(self): def test_storage_resource_not_found(self):
# verify response code for exception.StorageResourceNotFound # verify response code for exception.StorageResourceNotFound
name = "fake_name" name = "fake_name"

View File

@ -36,7 +36,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the " help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."), "value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion", cfg.StrOpt("max_api_microversion",
default="2.10", default="2.11",
help="The maximum api microversion is configured to be the " help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."), "value of the latest microversion supported by Manila."),
cfg.StrOpt("region", cfg.StrOpt("region",

View File

@ -13,10 +13,10 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from tempest import config # noqa from tempest import config
from tempest import test # noqa from tempest import test
from tempest_lib import exceptions as lib_exc # noqa from tempest_lib import exceptions as lib_exc
import testtools # noqa import testtools
from manila_tempest_tests.tests.api import base from manila_tempest_tests.tests.api import base
from manila_tempest_tests import utils from manila_tempest_tests import utils
@ -78,6 +78,12 @@ class SharesNFSTest(base.BaseSharesTest):
detailed_elements.remove('export_location') detailed_elements.remove('export_location')
self.assertTrue(detailed_elements.issubset(share_get.keys()), msg) self.assertTrue(detailed_elements.issubset(share_get.keys()), msg)
# In v 2.11 and beyond, we expect key 'replication_type' in the
# share data returned by the share create API.
if utils.is_microversion_supported('2.11'):
detailed_elements.add('replication_type')
self.assertTrue(detailed_elements.issubset(share.keys()), msg)
# Delete share # Delete share
self.shares_v2_client.delete_share(share['id']) self.shares_v2_client.delete_share(share['id'])
self.shares_v2_client.wait_for_resource_deletion(share_id=share['id']) self.shares_v2_client.wait_for_resource_deletion(share_id=share['id'])

View File

@ -97,6 +97,8 @@ class SharesActionsTest(base.BaseSharesTest):
expected_keys.append("share_type_name") expected_keys.append("share_type_name")
if utils.is_microversion_ge(version, '2.10'): if utils.is_microversion_ge(version, '2.10'):
expected_keys.append("access_rules_status") expected_keys.append("access_rules_status")
if utils.is_microversion_ge(version, '2.11'):
expected_keys.append("replication_type")
actual_keys = list(share.keys()) actual_keys = list(share.keys())
[self.assertIn(key, actual_keys) for key in expected_keys] [self.assertIn(key, actual_keys) for key in expected_keys]
@ -143,6 +145,11 @@ class SharesActionsTest(base.BaseSharesTest):
def test_get_share_with_access_rules_status(self): def test_get_share_with_access_rules_status(self):
self._get_share('2.10') self._get_share('2.10')
@test.attr(type=["gate", ])
@utils.skip_if_microversion_not_supported('2.11')
def test_get_share_with_replication_type_key(self):
self._get_share('2.11')
@test.attr(type=["gate", ]) @test.attr(type=["gate", ])
def test_list_shares(self): def test_list_shares(self):
@ -183,6 +190,8 @@ class SharesActionsTest(base.BaseSharesTest):
keys.append("share_type_name") keys.append("share_type_name")
if utils.is_microversion_ge(version, '2.10'): if utils.is_microversion_ge(version, '2.10'):
keys.append("access_rules_status") keys.append("access_rules_status")
if utils.is_microversion_ge(version, '2.11'):
keys.append("replication_type")
[self.assertIn(key, sh.keys()) for sh in shares for key in keys] [self.assertIn(key, sh.keys()) for sh in shares for key in keys]
@ -220,6 +229,11 @@ class SharesActionsTest(base.BaseSharesTest):
def test_list_shares_with_detail_with_access_rules_status(self): def test_list_shares_with_detail_with_access_rules_status(self):
self._list_shares_with_detail('2.10') self._list_shares_with_detail('2.10')
@test.attr(type=["gate", ])
@utils.skip_if_microversion_not_supported('2.11')
def test_list_shares_with_detail_replication_type_key(self):
self._list_shares_with_detail('2.11')
@test.attr(type=["gate", ]) @test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_metadata(self): def test_list_shares_with_detail_filter_by_metadata(self):
filters = {'metadata': self.metadata} filters = {'metadata': self.metadata}

View File

@ -0,0 +1,4 @@
---
features:
- Shares can be replicated. Replicas can be added, listed, queried for
detail, promoted to be 'active' or removed.