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:
parent
e5f4e981f5
commit
f858e537dd
@ -110,5 +110,11 @@
|
||||
"cgsnapshot:update" : "rule:default",
|
||||
"cgsnapshot:delete": "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"
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ REST_API_VERSION_HISTORY = """
|
||||
* 2.9 - Add export locations API
|
||||
* 2.10 - Field 'access_rules_status' was added to shares and share
|
||||
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 minimum version of the API supported.
|
||||
_MIN_API_VERSION = "2.0"
|
||||
_MAX_API_VERSION = "2.10"
|
||||
_MAX_API_VERSION = "2.11"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -78,3 +78,10 @@ user documentation.
|
||||
2.10
|
||||
----
|
||||
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'.
|
||||
|
@ -93,6 +93,8 @@ class ShareMixin(object):
|
||||
raise exc.HTTPNotFound()
|
||||
except exception.InvalidShare as 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)
|
||||
|
||||
@ -116,7 +118,12 @@ class ShareMixin(object):
|
||||
except ValueError:
|
||||
raise exc.HTTPBadRequest(
|
||||
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)
|
||||
|
||||
def index(self, req):
|
||||
|
@ -41,6 +41,7 @@ from manila.api.v2 import services
|
||||
from manila.api.v2 import share_export_locations
|
||||
from manila.api.v2 import share_instance_export_locations
|
||||
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 shares
|
||||
from manila.api import versions
|
||||
@ -280,3 +281,9 @@ class APIRouter(manila.api.openstack.APIRouter):
|
||||
controller=self.resources["cgsnapshots"],
|
||||
collection={"detail": "GET"},
|
||||
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'})
|
||||
|
182
manila/api/v2/share_replicas.py
Normal file
182
manila/api/v2/share_replicas.py
Normal 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())
|
@ -21,6 +21,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
_detail_version_modifiers = [
|
||||
"remove_export_locations",
|
||||
"add_access_rules_status_field",
|
||||
"add_replication_fields",
|
||||
]
|
||||
|
||||
def detail_list(self, request, instances):
|
||||
@ -71,3 +72,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
instance_dict['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')
|
||||
|
75
manila/api/views/share_replicas.py
Normal file
75
manila/api/views/share_replicas.py
Normal 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
|
@ -27,6 +27,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
"modify_share_type_field",
|
||||
"remove_export_locations",
|
||||
"add_access_rules_status_field",
|
||||
"add_replication_fields",
|
||||
]
|
||||
|
||||
def summary_list(self, request, shares):
|
||||
@ -128,6 +129,11 @@ class ViewBuilder(common.ViewBuilder):
|
||||
def add_access_rules_status_field(self, share_dict, share):
|
||||
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):
|
||||
"""Provide a view for a list of shares."""
|
||||
shares_list = [func(request, share)['share'] for share in shares]
|
||||
|
@ -35,6 +35,7 @@ STATUS_SHRINKING_ERROR = 'shrinking_error'
|
||||
STATUS_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_IN_PROGRESS = 'migration_in_progress'
|
||||
STATUS_TASK_STATE_MIGRATION_ERROR = 'migration_error'
|
||||
@ -104,12 +105,17 @@ TASK_STATE_STATUSES = (
|
||||
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):
|
||||
|
||||
# Extra specs key names
|
||||
DRIVER_HANDLES_SHARE_SERVERS = "driver_handles_share_servers"
|
||||
SNAPSHOT_SUPPORT = "snapshot_support"
|
||||
REPLICATION_TYPE_SPEC = "replication_type"
|
||||
|
||||
# Extra specs containers
|
||||
REQUIRED = (
|
||||
@ -120,9 +126,8 @@ class ExtraSpecs(object):
|
||||
SNAPSHOT_SUPPORT,
|
||||
)
|
||||
# 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
|
||||
# list today, but that may not always remain true.
|
||||
TENANT_VISIBLE = UNDELETABLE
|
||||
# should be visible to non-admin users. UNDELETABLE specs are user-visible.
|
||||
TENANT_VISIBLE = UNDELETABLE + (REPLICATION_TYPE_SPEC, )
|
||||
BOOLEAN = (
|
||||
DRIVER_HANDLES_SHARE_SERVERS,
|
||||
SNAPSHOT_SUPPORT,
|
||||
|
@ -43,7 +43,6 @@ these objects be simple dictionaries.
|
||||
from oslo_config import cfg
|
||||
from oslo_db import api as db_api
|
||||
|
||||
|
||||
db_opts = [
|
||||
cfg.StrOpt('db_backend',
|
||||
default='sqlalchemy',
|
||||
@ -79,6 +78,8 @@ def authorize_quota_class_context(context, class_name):
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def service_destroy(context, service_id):
|
||||
"""Destroy the service or raise if it does not exist."""
|
||||
return IMPL.service_destroy(context, service_id)
|
||||
@ -415,6 +416,15 @@ def 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):
|
||||
"""Get share access rule."""
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
@ -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')
|
@ -1292,6 +1292,162 @@ def share_instances_get_all_by_consistency_group_id(context, cg_id):
|
||||
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
|
||||
def share_get(context, share_id, session=None):
|
||||
result = _share_get_query(context, session).filter_by(id=share_id).first()
|
||||
|
||||
if result is None:
|
||||
raise exception.NotFound()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@ -1574,6 +1732,23 @@ def share_access_create(context, values):
|
||||
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):
|
||||
access_ref = models.ShareInstanceAccessMapping()
|
||||
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
|
||||
def share_access_get_all_for_share(context, share_id):
|
||||
session = get_session()
|
||||
def share_access_get_all_for_share(context, share_id, session=None):
|
||||
session = session or get_session()
|
||||
return _share_access_get_query(context, session,
|
||||
{'share_id': share_id}).all()
|
||||
|
||||
|
@ -209,10 +209,16 @@ class Share(BASE, ManilaBase):
|
||||
|
||||
@property
|
||||
def export_locations(self):
|
||||
# TODO(u_glide): Return a map with lists of locations per AZ when
|
||||
# replication functionality will be implemented.
|
||||
# TODO(gouthamr): Return AZ specific export locations for replicated
|
||||
# shares.
|
||||
# NOTE(gouthamr): For a replicated share, export locations of the
|
||||
# 'active' instances are chosen, if 'available'.
|
||||
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:
|
||||
for export_location in instance.export_locations:
|
||||
all_export_locations.append(export_location['path'])
|
||||
@ -238,19 +244,46 @@ class Share(BASE, ManilaBase):
|
||||
def share_server_id(self):
|
||||
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
|
||||
def instance(self):
|
||||
# NOTE(ganso): We prefer instances with AVAILABLE status,
|
||||
# and we also prefer to show any status other than TRANSITIONAL ones.
|
||||
# NOTE(gouthamr): The order of preference: status 'replication_change',
|
||||
# followed by 'available' and 'error'. If replicated share and
|
||||
# not undergoing a 'replication_change', only 'active' instances are
|
||||
# preferred.
|
||||
result = None
|
||||
if len(self.instances) > 0:
|
||||
for instance in self.instances:
|
||||
if instance.status == constants.STATUS_AVAILABLE:
|
||||
return instance
|
||||
elif instance.status not in constants.TRANSITIONAL_STATUSES:
|
||||
result = instance
|
||||
if result is None:
|
||||
result = self.instances[0]
|
||||
order = (constants.STATUS_REPLICATION_CHANGE,
|
||||
constants.STATUS_AVAILABLE, constants.STATUS_ERROR)
|
||||
other_statuses = (
|
||||
[x['status'] for x in self.instances if
|
||||
x['status'] not in order and
|
||||
x['status'] not in constants.TRANSITIONAL_STATUSES]
|
||||
)
|
||||
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
|
||||
|
||||
@property
|
||||
@ -267,6 +300,7 @@ class Share(BASE, ManilaBase):
|
||||
display_description = Column(String(255))
|
||||
snapshot_id = Column(String(36))
|
||||
snapshot_support = Column(Boolean, default=True)
|
||||
replication_type = Column(String(255), nullable=True)
|
||||
share_proto = Column(String(255))
|
||||
share_type_id = Column(String(36), ForeignKey('share_types.id'),
|
||||
nullable=True)
|
||||
@ -300,7 +334,8 @@ class Share(BASE, ManilaBase):
|
||||
class ShareInstance(BASE, ManilaBase):
|
||||
__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',
|
||||
'display_name', 'display_description',
|
||||
'snapshot_id', 'share_proto', 'share_type_id',
|
||||
@ -345,6 +380,7 @@ class ShareInstance(BASE, ManilaBase):
|
||||
scheduled_at = Column(DateTime)
|
||||
launched_at = Column(DateTime)
|
||||
terminated_at = Column(DateTime)
|
||||
replica_state = Column(String(255), nullable=True)
|
||||
|
||||
availability_zone_id = Column(String(36),
|
||||
ForeignKey('availability_zones.id'),
|
||||
|
@ -718,3 +718,12 @@ class ShareMountException(ManilaException):
|
||||
|
||||
class ShareCopyDataException(ManilaException):
|
||||
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.")
|
||||
|
@ -51,6 +51,16 @@ def share_update_db(context, share_id, host):
|
||||
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):
|
||||
'''Set the host and set the updated_at field of a consistency group.
|
||||
|
||||
@ -114,3 +124,8 @@ class Scheduler(object):
|
||||
filter_properties):
|
||||
"""Must override schedule method for migration to work."""
|
||||
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"))
|
||||
|
@ -105,6 +105,32 @@ class FilterScheduler(base.Scheduler):
|
||||
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,
|
||||
request_spec):
|
||||
|
||||
|
@ -127,6 +127,7 @@ class HostState(object):
|
||||
self.consistency_group_support = False
|
||||
self.dedupe = False
|
||||
self.compression = False
|
||||
self.replication_type = None
|
||||
|
||||
# PoolState for all pools
|
||||
self.pools = {}
|
||||
@ -292,6 +293,9 @@ class HostState(object):
|
||||
if 'compression' not in pool_cap:
|
||||
pool_cap['compression'] = self.compression
|
||||
|
||||
if not pool_cap.get('replication_type'):
|
||||
pool_cap['replication_type'] = self.replication_type
|
||||
|
||||
def update_backend(self, capability):
|
||||
self.share_backend_name = capability.get('share_backend_name')
|
||||
self.vendor_name = capability.get('vendor_name')
|
||||
@ -303,6 +307,7 @@ class HostState(object):
|
||||
self.consistency_group_support = capability.get(
|
||||
'consistency_group_support', False)
|
||||
self.updated = capability['timestamp']
|
||||
self.replication_type = capability.get('replication_type')
|
||||
|
||||
def consume_from_share(self, share):
|
||||
"""Incrementally update host state from an share."""
|
||||
@ -365,6 +370,8 @@ class PoolState(HostState):
|
||||
'dedupe', False)
|
||||
self.compression = capability.get(
|
||||
'compression', False)
|
||||
self.replication_type = capability.get(
|
||||
'replication_type', self.replication_type)
|
||||
|
||||
def update_pools(self, capability):
|
||||
# Do nothing, since we don't have pools within pool, yet
|
||||
|
@ -59,7 +59,7 @@ MAPPING = {
|
||||
class SchedulerManager(manager.Manager):
|
||||
"""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,
|
||||
*args, **kwargs):
|
||||
@ -206,3 +206,21 @@ class SchedulerManager(manager.Manager):
|
||||
with excutils.save_and_reraise_exception():
|
||||
self._set_cg_error_state('create_consistency_group',
|
||||
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})
|
||||
|
@ -36,15 +36,16 @@ class SchedulerAPI(object):
|
||||
Replace create_share() - > create_share_instance()
|
||||
1.3 - Add create_consistency_group 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):
|
||||
super(SchedulerAPI, self).__init__()
|
||||
target = messaging.Target(topic=CONF.scheduler_topic,
|
||||
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,
|
||||
filter_properties=None):
|
||||
@ -98,3 +99,14 @@ class SchedulerAPI(object):
|
||||
force_host_copy=force_host_copy,
|
||||
request_spec=request_spec_p,
|
||||
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,
|
||||
)
|
||||
|
@ -170,6 +170,8 @@ class API(base.Base):
|
||||
share_type.get('extra_specs', {}).get(
|
||||
'snapshot_support', True) if share_type else True,
|
||||
strict=True)
|
||||
replication_type = share_type.get('extra_specs', {}).get(
|
||||
'replication_type') if share_type else None
|
||||
except ValueError as e:
|
||||
raise exception.InvalidParameterValue(six.text_type(e))
|
||||
|
||||
@ -221,6 +223,7 @@ class API(base.Base):
|
||||
'project_id': context.project_id,
|
||||
'snapshot_id': snapshot_id,
|
||||
'snapshot_support': snapshot_support,
|
||||
'replication_type': replication_type,
|
||||
'metadata': metadata,
|
||||
'display_name': name,
|
||||
'display_description': description,
|
||||
@ -264,25 +267,11 @@ class API(base.Base):
|
||||
consistency_group=None, cgsnapshot_member=None):
|
||||
policy.check_policy(context, 'share', 'create')
|
||||
|
||||
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,
|
||||
}
|
||||
)
|
||||
request_spec, share_instance = (
|
||||
self._create_share_instance_and_get_request_spec(
|
||||
context, share, availability_zone=availability_zone,
|
||||
consistency_group=consistency_group, host=host,
|
||||
share_network_id=share_network_id))
|
||||
|
||||
if cgsnapshot_member:
|
||||
host = cgsnapshot_member['share']['host']
|
||||
@ -292,45 +281,6 @@ class API(base.Base):
|
||||
# NOTE(ameade): Do not cast to driver if creating from cgsnapshot
|
||||
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:
|
||||
self.share_rpcapi.create_share_instance(
|
||||
context,
|
||||
@ -348,6 +298,168 @@ class API(base.Base):
|
||||
|
||||
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):
|
||||
policy.check_policy(context, 'share', 'manage')
|
||||
|
||||
@ -419,6 +531,13 @@ class API(base.Base):
|
||||
"statuses": statuses}
|
||||
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)
|
||||
if len(snapshots):
|
||||
msg = _("Share still has %d dependent snapshots") % len(snapshots)
|
||||
@ -580,6 +699,14 @@ class API(base.Base):
|
||||
|
||||
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
|
||||
if share_instance['status'] != constants.STATUS_AVAILABLE:
|
||||
msg = _('Share instance %(instance_id)s status must be available, '
|
||||
|
@ -618,6 +618,20 @@ class ShareDriver(object):
|
||||
should be added/deleted. Driver can ignore rules in 'access_rules' and
|
||||
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 share: Share model with share data.
|
||||
:param access_rules: All access rules for given share
|
||||
@ -1096,3 +1110,290 @@ class ShareDriver(object):
|
||||
:return: list of 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()
|
||||
|
@ -89,6 +89,11 @@ share_manager_opts = [
|
||||
'will wait for a share server to go unutilized before '
|
||||
'deleting it.',
|
||||
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
|
||||
@ -107,6 +112,28 @@ MAPPING = {
|
||||
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 wrapped(self, *args, **kwargs):
|
||||
@ -137,7 +164,7 @@ def add_hooks(f):
|
||||
class ShareManager(manager.SchedulerDependentManager):
|
||||
"""Manages NAS storages."""
|
||||
|
||||
RPC_API_VERSION = '1.7'
|
||||
RPC_API_VERSION = '1.8'
|
||||
|
||||
def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
|
||||
"""Load the driver from args, or from flags."""
|
||||
@ -812,9 +839,376 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
self.db.share_instance_update(
|
||||
context, share_instance_id,
|
||||
{'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
|
||||
@utils.require_driver_initialized
|
||||
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"),
|
||||
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
|
||||
|
@ -46,6 +46,10 @@ class ShareAPI(object):
|
||||
get_migration_info()
|
||||
get_driver_migration_info()
|
||||
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'
|
||||
@ -54,7 +58,7 @@ class ShareAPI(object):
|
||||
super(ShareAPI, self).__init__()
|
||||
target = messaging.Target(topic=CONF.share_topic,
|
||||
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,
|
||||
request_spec, filter_properties,
|
||||
@ -200,3 +204,40 @@ class ShareAPI(object):
|
||||
ctxt,
|
||||
'delete_cgsnapshot',
|
||||
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,
|
||||
)
|
||||
|
@ -43,13 +43,20 @@ def stub_share(id, **kwargs):
|
||||
'share_server_id': 'fake_share_server_id',
|
||||
'is_public': False,
|
||||
'snapshot_support': True,
|
||||
'replication_type': None,
|
||||
'has_replicas': False,
|
||||
}
|
||||
share.update(kwargs)
|
||||
|
||||
# NOTE(ameade): We must wrap the dictionary in an class in order to stub
|
||||
# object attributes.
|
||||
class wrapper(dict):
|
||||
pass
|
||||
def __getattr__(self, item):
|
||||
try:
|
||||
return self[item]
|
||||
except KeyError:
|
||||
raise AttributeError()
|
||||
|
||||
fake_share = wrapper()
|
||||
fake_share.instance = {'id': "fake_instance_id"}
|
||||
fake_share.update(share)
|
||||
|
@ -121,7 +121,19 @@ class ShareInstancesAPITest(test.TestCase):
|
||||
self.mock_policy_check.assert_called_once_with(
|
||||
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):
|
||||
test_share = db_utils.create_share(size=1)
|
||||
id = test_share['id']
|
||||
@ -147,6 +159,9 @@ class ShareInstancesAPITest(test.TestCase):
|
||||
assert_method = self.assertIn
|
||||
assert_method("export_location", 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([
|
||||
get_instances_policy_check_call, share_policy_check_call])
|
||||
|
||||
|
514
manila/tests/api/v2/test_share_replicas.py
Normal file
514
manila/tests/api/v2/test_share_replicas.py
Normal 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)
|
@ -25,6 +25,7 @@ import webob
|
||||
|
||||
from manila.api import common
|
||||
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.common import constants
|
||||
from manila import context
|
||||
@ -203,6 +204,29 @@ class ShareAPITest(test.TestCase):
|
||||
self.controller.create, req, {'share': self.share})
|
||||
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):
|
||||
shr = {
|
||||
"size": 100,
|
||||
@ -250,6 +274,22 @@ class ShareAPITest(test.TestCase):
|
||||
self.mock_object(share_api.API, 'migrate_share')
|
||||
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')
|
||||
def test_migrate_share_no_share_id(self, version):
|
||||
req = fakes.HTTPRequest.blank('/shares/%s/action' % 'fake_id',
|
||||
@ -500,11 +540,40 @@ class ShareAPITest(test.TestCase):
|
||||
self.controller.show,
|
||||
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):
|
||||
req = fakes.HTTPRequest.blank('/shares/1')
|
||||
resp = self.controller.delete(req, 1)
|
||||
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):
|
||||
fake_share = stubs.stub_share('fake_share',
|
||||
consistency_group_id='fake_cg_id')
|
||||
@ -827,6 +896,58 @@ class ShareAPITest(test.TestCase):
|
||||
expected['shares'][0].pop('export_locations')
|
||||
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):
|
||||
ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False)
|
||||
search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'}
|
||||
|
@ -36,6 +36,12 @@ class FakeModel(object):
|
||||
def __repr__(self):
|
||||
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):
|
||||
"""Set the stubs in mapping in the db api."""
|
||||
|
@ -34,6 +34,7 @@ See BaseMigrationChecks class for more information.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import datetime
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
@ -351,3 +352,121 @@ class AccessRulesStatusMigrationChecks(BaseMigrationChecks):
|
||||
for rule in engine.execute(share_instances_rules_table.select()):
|
||||
valid_state = valid_statuses[rule['share_instance_id']]
|
||||
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'))
|
||||
|
@ -217,6 +217,344 @@ class ShareDatabaseAPITestCase(test.TestCase):
|
||||
self.assertEqual(2, len(actual_result))
|
||||
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
|
||||
class ConsistencyGroupDatabaseAPITestCase(test.TestCase):
|
||||
|
@ -76,6 +76,55 @@ class ShareTestCase(test.TestCase):
|
||||
|
||||
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):
|
||||
share = db_utils.create_share(instances=[])
|
||||
|
||||
|
@ -98,6 +98,18 @@ def create_share_instance(**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):
|
||||
"""Create a snapshot object."""
|
||||
with_share = kwargs.pop('with_share', False)
|
||||
|
@ -13,6 +13,8 @@
|
||||
# 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 datetime
|
||||
import uuid
|
||||
|
||||
from manila.tests.db import fakes as db_fakes
|
||||
|
||||
@ -29,6 +31,8 @@ def fake_share(**kwargs):
|
||||
'project_id': 'fake_project_uuid',
|
||||
'availability_zone': 'fake_az',
|
||||
'snapshot_support': 'True',
|
||||
'replication_type': None,
|
||||
'is_busy': False,
|
||||
}
|
||||
share.update(kwargs)
|
||||
return db_fakes.FakeModel(share)
|
||||
@ -58,3 +62,61 @@ def fake_access(**kwargs):
|
||||
}
|
||||
access.update(kwargs)
|
||||
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)
|
||||
|
@ -17,6 +17,7 @@
|
||||
import re
|
||||
|
||||
from eventlet import greenthread
|
||||
import mock
|
||||
from oslo_log import log
|
||||
import six
|
||||
|
||||
@ -109,3 +110,10 @@ def stub_out_utils_execute(testcase):
|
||||
fake_execute_set_repliers([])
|
||||
fake_execute_clear_log()
|
||||
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
|
||||
|
@ -30,6 +30,7 @@ from manila.tests.scheduler.drivers import test_base
|
||||
from manila.tests.scheduler import fakes
|
||||
|
||||
SNAPSHOT_SUPPORT = constants.ExtraSpecs.SNAPSHOT_SUPPORT
|
||||
REPLICATION_TYPE_SPEC = constants.ExtraSpecs.REPLICATION_TYPE_SPEC
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@ -133,6 +134,59 @@ class FilterSchedulerTestCase(test_base.SchedulerTestCase):
|
||||
self.assertIsNone(weighed_host)
|
||||
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')
|
||||
def test_schedule_share_with_cg_pool_support(
|
||||
self, _mock_service_get_all_by_topic):
|
||||
@ -450,3 +504,38 @@ class FilterSchedulerTestCase(test_base.SchedulerTestCase):
|
||||
sched.host_passes_filters,
|
||||
ctx, 'host3#_pool0', request_spec, {})
|
||||
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={})
|
||||
|
@ -79,6 +79,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
timestamp=None, reserved_percentage=0,
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool1',
|
||||
total_capacity_gb=51,
|
||||
free_capacity_gb=41,
|
||||
@ -90,6 +91,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
timestamp=None, reserved_percentage=0,
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool2',
|
||||
total_capacity_gb=52,
|
||||
free_capacity_gb=42,
|
||||
@ -101,6 +103,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
timestamp=None, reserved_percentage=0,
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool3',
|
||||
total_capacity_gb=53,
|
||||
free_capacity_gb=43,
|
||||
@ -113,6 +116,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
timestamp=None, reserved_percentage=0,
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool4a',
|
||||
total_capacity_gb=541,
|
||||
free_capacity_gb=441,
|
||||
@ -133,6 +137,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
timestamp=None, reserved_percentage=0,
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool5a',
|
||||
total_capacity_gb=551,
|
||||
free_capacity_gb=451,
|
||||
@ -150,6 +155,8 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
'host6@FFF': dict(share_backend_name='FFF',
|
||||
timestamp=None, reserved_percentage=0,
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool6a',
|
||||
total_capacity_gb='unknown',
|
||||
free_capacity_gb='unknown',
|
||||
@ -184,7 +191,9 @@ class FakeHostManager(host_manager.HostManager):
|
||||
'thin_provisioning': False,
|
||||
'reserved_percentage': 10,
|
||||
'timestamp': None,
|
||||
'snapshot_support': True},
|
||||
'snapshot_support': True,
|
||||
'replication_type': 'writable',
|
||||
},
|
||||
'host2': {'total_capacity_gb': 2048,
|
||||
'free_capacity_gb': 300,
|
||||
'allocated_capacity_gb': 1748,
|
||||
@ -193,7 +202,9 @@ class FakeHostManager(host_manager.HostManager):
|
||||
'thin_provisioning': True,
|
||||
'reserved_percentage': 10,
|
||||
'timestamp': None,
|
||||
'snapshot_support': True},
|
||||
'snapshot_support': True,
|
||||
'replication_type': 'readable',
|
||||
},
|
||||
'host3': {'total_capacity_gb': 512,
|
||||
'free_capacity_gb': 256,
|
||||
'allocated_capacity_gb': 256,
|
||||
@ -202,8 +213,9 @@ class FakeHostManager(host_manager.HostManager):
|
||||
'thin_provisioning': False,
|
||||
'consistency_group_support': 'host',
|
||||
'reserved_percentage': 0,
|
||||
'snapshot_support': True,
|
||||
'timestamp': None,
|
||||
'snapshot_support': True},
|
||||
},
|
||||
'host4': {'total_capacity_gb': 2048,
|
||||
'free_capacity_gb': 200,
|
||||
'allocated_capacity_gb': 1848,
|
||||
@ -212,7 +224,9 @@ class FakeHostManager(host_manager.HostManager):
|
||||
'thin_provisioning': True,
|
||||
'reserved_percentage': 5,
|
||||
'timestamp': None,
|
||||
'snapshot_support': True},
|
||||
'snapshot_support': True,
|
||||
'replication_type': 'dr',
|
||||
},
|
||||
'host5': {'total_capacity_gb': 2048,
|
||||
'free_capacity_gb': 500,
|
||||
'allocated_capacity_gb': 1548,
|
||||
@ -221,15 +235,18 @@ class FakeHostManager(host_manager.HostManager):
|
||||
'thin_provisioning': True,
|
||||
'reserved_percentage': 5,
|
||||
'timestamp': None,
|
||||
'snapshot_support': True,
|
||||
'consistency_group_support': 'pool',
|
||||
'snapshot_support': True},
|
||||
'replication_type': None,
|
||||
},
|
||||
'host6': {'total_capacity_gb': 'unknown',
|
||||
'free_capacity_gb': 'unknown',
|
||||
'allocated_capacity_gb': 1548,
|
||||
'thin_provisioning': False,
|
||||
'reserved_percentage': 5,
|
||||
'snapshot_support': True,
|
||||
'timestamp': None,
|
||||
'snapshot_support': True},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -275,3 +292,45 @@ class FakeWeigher2(base_host_weigher.BaseHostWeigher):
|
||||
class FakeClass(object):
|
||||
def __init__(self):
|
||||
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)
|
||||
|
@ -199,6 +199,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
}, {
|
||||
'name': 'host2@back1#BBB',
|
||||
@ -222,6 +223,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
}, {
|
||||
'name': 'host2@back2#CCC',
|
||||
@ -245,6 +247,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
},
|
||||
]
|
||||
@ -290,6 +293,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
}, {
|
||||
'name': 'host2@BBB#pool2',
|
||||
@ -314,6 +318,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
}, {
|
||||
'name': 'host3@CCC#pool3',
|
||||
@ -338,6 +343,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': 'pool',
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
}, {
|
||||
'name': 'host4@DDD#pool4a',
|
||||
@ -362,6 +368,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': 'host',
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
}, {
|
||||
'name': 'host4@DDD#pool4b',
|
||||
@ -386,6 +393,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': 'host',
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
},
|
||||
]
|
||||
@ -443,6 +451,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
}, {
|
||||
'name': 'host2@back1#BBB',
|
||||
@ -466,6 +475,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
},
|
||||
]
|
||||
@ -515,6 +525,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
'replication_type': None,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
@ -110,3 +110,10 @@ class SchedulerRpcAPITestCase(test.TestCase):
|
||||
request_spec='fake_request_spec',
|
||||
filter_properties='filter_properties',
|
||||
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')
|
||||
|
@ -34,6 +34,7 @@ from manila.share import api as share_api
|
||||
from manila.share import share_types
|
||||
from manila import test
|
||||
from manila.tests import db_utils
|
||||
from manila.tests import fake_share as fakes
|
||||
from manila.tests import utils as test_utils
|
||||
from manila import utils
|
||||
|
||||
@ -213,12 +214,9 @@ class ShareAPITestCase(test.TestCase):
|
||||
share_type_id=share_type_id,
|
||||
)
|
||||
share_instance = db_utils.create_share_instance(share_id=share['id'])
|
||||
share_metadata = {'fake': 'fake'}
|
||||
share_type = {'fake': 'fake'}
|
||||
self.mock_object(db_api, 'share_instance_create',
|
||||
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',
|
||||
mock.Mock(return_value=share_type))
|
||||
az_mock = mock.Mock()
|
||||
@ -701,8 +699,6 @@ class ShareAPITestCase(test.TestCase):
|
||||
'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,
|
||||
share['share_type_id'])
|
||||
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.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.Mock(return_value=2))
|
||||
def test_delete_dependent_cgsnapshot_members(self):
|
||||
@ -1704,6 +1711,27 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.assertRaises(exception.InvalidShare, self.api.migrate_share,
|
||||
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):
|
||||
host = 'fake@backend#pool'
|
||||
share = db_utils.create_share(
|
||||
@ -1745,6 +1773,211 @@ class ShareAPITestCase(test.TestCase):
|
||||
db_api.share_update.assert_any_call(
|
||||
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):
|
||||
def setUp(self):
|
||||
|
@ -699,3 +699,29 @@ class ShareDriverTestCase(test.TestCase):
|
||||
'fake_share',
|
||||
'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')
|
||||
|
@ -15,9 +15,11 @@
|
||||
|
||||
"""Test of Share Manager for Manila."""
|
||||
import datetime
|
||||
import random
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import timeutils
|
||||
@ -36,11 +38,43 @@ from manila.share import migration
|
||||
from manila.share import rpcapi
|
||||
from manila.share import share_types
|
||||
from manila import test
|
||||
from manila.tests.api import fakes as test_fakes
|
||||
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 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
|
||||
class ShareManagerTestCase(test.TestCase):
|
||||
|
||||
@ -55,6 +89,10 @@ class ShareManagerTestCase(test.TestCase):
|
||||
self.mock_object(self.share_manager.driver, 'check_for_setup_error')
|
||||
self.context = context.get_admin_context()
|
||||
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):
|
||||
fake_service_name = "fake_service"
|
||||
@ -161,6 +199,10 @@ class ShareManagerTestCase(test.TestCase):
|
||||
"delete_consistency_group",
|
||||
"create_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):
|
||||
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.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):
|
||||
"""Test share's snapshot can be created and deleted."""
|
||||
|
||||
|
@ -41,11 +41,16 @@ class ShareRpcAPITestCase(test.TestCase):
|
||||
)
|
||||
access = db_utils.create_access(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()
|
||||
cg = {'id': 'fake_cg_id', 'host': 'fake_host'}
|
||||
cgsnapshot = {'id': 'fake_cg_id'}
|
||||
host = {'host': 'fake_host', 'capabilities': 1}
|
||||
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_snapshot = jsonutils.to_primitive(snapshot)
|
||||
self.fake_share_server = jsonutils.to_primitive(share_server)
|
||||
@ -93,6 +98,9 @@ class ShareRpcAPITestCase(test.TestCase):
|
||||
if 'dest_host' in expected_msg:
|
||||
del expected_msg['dest_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:
|
||||
host = kwargs['host']
|
||||
@ -246,6 +254,23 @@ class ShareRpcAPITestCase(test.TestCase):
|
||||
share_instance=self.fake_share,
|
||||
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):
|
||||
host = 'fake_host'
|
||||
capabilities = 1
|
||||
|
@ -118,6 +118,13 @@ class ManilaExceptionTestCase(test.TestCase):
|
||||
exc2 = exception.ManilaException(exc1)
|
||||
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):
|
||||
|
||||
@ -448,6 +455,13 @@ class ManilaExceptionResponseCode404(test.TestCase):
|
||||
self.assertEqual(404, e.code)
|
||||
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):
|
||||
# verify response code for exception.StorageResourceNotFound
|
||||
name = "fake_name"
|
||||
|
@ -36,7 +36,7 @@ ShareGroup = [
|
||||
help="The minimum api microversion is configured to be the "
|
||||
"value of the minimum microversion supported by Manila."),
|
||||
cfg.StrOpt("max_api_microversion",
|
||||
default="2.10",
|
||||
default="2.11",
|
||||
help="The maximum api microversion is configured to be the "
|
||||
"value of the latest microversion supported by Manila."),
|
||||
cfg.StrOpt("region",
|
||||
|
@ -13,10 +13,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest import config # noqa
|
||||
from tempest import test # noqa
|
||||
from tempest_lib import exceptions as lib_exc # noqa
|
||||
import testtools # noqa
|
||||
from tempest import config
|
||||
from tempest import test
|
||||
from tempest_lib import exceptions as lib_exc
|
||||
import testtools
|
||||
|
||||
from manila_tempest_tests.tests.api import base
|
||||
from manila_tempest_tests import utils
|
||||
@ -78,6 +78,12 @@ class SharesNFSTest(base.BaseSharesTest):
|
||||
detailed_elements.remove('export_location')
|
||||
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
|
||||
self.shares_v2_client.delete_share(share['id'])
|
||||
self.shares_v2_client.wait_for_resource_deletion(share_id=share['id'])
|
||||
|
@ -97,6 +97,8 @@ class SharesActionsTest(base.BaseSharesTest):
|
||||
expected_keys.append("share_type_name")
|
||||
if utils.is_microversion_ge(version, '2.10'):
|
||||
expected_keys.append("access_rules_status")
|
||||
if utils.is_microversion_ge(version, '2.11'):
|
||||
expected_keys.append("replication_type")
|
||||
actual_keys = list(share.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):
|
||||
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", ])
|
||||
def test_list_shares(self):
|
||||
|
||||
@ -183,6 +190,8 @@ class SharesActionsTest(base.BaseSharesTest):
|
||||
keys.append("share_type_name")
|
||||
if utils.is_microversion_ge(version, '2.10'):
|
||||
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]
|
||||
|
||||
@ -220,6 +229,11 @@ class SharesActionsTest(base.BaseSharesTest):
|
||||
def test_list_shares_with_detail_with_access_rules_status(self):
|
||||
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", ])
|
||||
def test_list_shares_with_detail_filter_by_metadata(self):
|
||||
filters = {'metadata': self.metadata}
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Shares can be replicated. Replicas can be added, listed, queried for
|
||||
detail, promoted to be 'active' or removed.
|
Loading…
Reference in New Issue
Block a user