Create share from snapshot in another pool or backend

This patch enables the creation of a share from snapshot
specifying another pool or backend. In the scheduler, a
new filter and weigher were implemented in order to consider
this operation if the backend supports it. Also, a new
field called 'progress' was added in the share and share
instance. The 'progress' field indicates the status
of the operation create share from snapshot (in percentage).
Finally, a new periodic task was added in order to constantly
check the share status.

Partially-implements: bp create-share-from-snapshot-in-another-pool-or-backend

DOCImpact
Change-Id: Iab13a0961eb4a387a502246e5d4b79bc9046e04b
Co-authored-by: carloss <ces.eduardo98@gmail.com>
Co-authored-by: dviroel <viroel@gmail.com>
This commit is contained in:
Douglas Viroel 2020-01-13 16:37:08 +00:00
parent 5eaa9e6046
commit 6c47b193b0
66 changed files with 1498 additions and 207 deletions

View File

@ -468,8 +468,8 @@ source_share_group_snapshot_id_query:
status_query: status_query:
description: | description: |
Filters by a share status. A valid value is Filters by a share status. A valid value is
``creating``, ``error``, ``available``, ``deleting``, ``creating``, ``creating_from_snapshot``, ``error``, ``available``,
``error_deleting``, ``manage_starting``, ``manage_error``, ``deleting``, ``error_deleting``, ``manage_starting``, ``manage_error``,
``unmanage_starting``, ``unmanage_error``, ``migrating``, ``unmanage_starting``, ``unmanage_error``, ``migrating``,
``extending``, ``extending_error``, ``shrinking``, ``extending``, ``extending_error``, ``shrinking``,
``shrinking_error``, or ``shrinking_possible_data_loss_error``. ``shrinking_error``, or ``shrinking_possible_data_loss_error``.
@ -1540,6 +1540,13 @@ progress:
in: body in: body
required: true required: true
type: string type: string
progress_share_instance:
description: |
The progress of the share creation.
in: body
min_version: 2.54
required: true
type: string
project: project:
description: | description: |
The UUID of the project to which access to the The UUID of the project to which access to the
@ -2713,7 +2720,7 @@ status_1:
type: string type: string
status_16: status_16:
description: | description: |
The share status, which is ``creating``, The share status, which is ``creating``, ``creating_from_snapshot``,
``error``, ``available``, ``deleting``, ``error_deleting``, ``error``, ``available``, ``deleting``, ``error_deleting``,
``manage_starting``, ``manage_error``, ``unmanage_starting``, ``manage_starting``, ``manage_error``, ``unmanage_starting``,
``unmanage_error``, ``unmanaged``, ``extend``, ``unmanage_error``, ``unmanaged``, ``extend``,
@ -2737,15 +2744,16 @@ status_3:
``extending_error``. Extend share failed. - ``shrinking``. Share ``extending_error``. Extend share failed. - ``shrinking``. Share
is being shrunk. - ``shrinking_error``. Failed to update quota on is being shrunk. - ``shrinking_error``. Failed to update quota on
share shrinking. - ``shrinking_possible_data_loss_error``. Shrink share shrinking. - ``shrinking_possible_data_loss_error``. Shrink
share failed due to possible data loss. share failed due to possible data loss. - ``creating_from_snapshot``.
The share is being created from a parent snapshot.
in: body in: body
required: true required: true
type: string type: string
status_5: status_5:
description: | description: |
The share instance status. A valid value is The share instance status. A valid value is
``available``, ``error``, ``creating``, ``deleting``, and ``available``, ``error``, ``creating``, ``deleting``,
``error_deleting``. ``creating_from_snapshot``, or ``error_deleting``.
in: body in: body
required: true required: true
type: string type: string

View File

@ -1,6 +1,7 @@
{ {
"share": { "share": {
"status": null, "status": null,
"progress": null,
"share_server_id": null, "share_server_id": null,
"project_id": "16e1ab15c35a457e9c2b2aa189f544e1", "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
"name": "share_London", "name": "share_London",

View File

@ -2,6 +2,7 @@
"share_instances": [ "share_instances": [
{ {
"status": "error", "status": "error",
"progress": null,
"share_id": "406ea93b-32e9-4907-a117-148b3945749f", "share_id": "406ea93b-32e9-4907-a117-148b3945749f",
"availability_zone": "nova", "availability_zone": "nova",
"replica_state": null, "replica_state": null,
@ -14,6 +15,7 @@
}, },
{ {
"status": "available", "status": "available",
"progress": "100%",
"share_id": "d94a8548-2079-4be0-b21c-0a887acd31ca", "share_id": "d94a8548-2079-4be0-b21c-0a887acd31ca",
"availability_zone": "nova", "availability_zone": "nova",
"replica_state": null, "replica_state": null,
@ -23,6 +25,19 @@
"share_server_id": "ba11930a-bf1a-4aa7-bae4-a8dfbaa3cc73", "share_server_id": "ba11930a-bf1a-4aa7-bae4-a8dfbaa3cc73",
"host": "manila2@generic1#GENERIC1", "host": "manila2@generic1#GENERIC1",
"id": "75559a8b-c90c-42a7-bda2-edbe86acfb7b" "id": "75559a8b-c90c-42a7-bda2-edbe86acfb7b"
},
{
"status": "creating_from_snapshot",
"progress": "30%",
"share_id": "9bb15af4-27e5-4174-ae15-dc549d4a3b51",
"availability_zone": "nova",
"replica_state": null,
"created_at": "2015-09-07T09:01:15.000000",
"share_network_id": "713df749-aac0-4a54-af52-10f6c991e80c",
"cast_rules_to_readonly": false,
"share_server_id": "ba11930a-bf1a-4aa7-bae4-a8dfbaa3cc73",
"host": "manila2@generic1#GENERIC1",
"id": "48155648-2fd3-480d-b02b-44b995c24bab"
} }
] ]
} }

View File

@ -1,6 +1,7 @@
{ {
"share_instance": { "share_instance": {
"status": "available", "status": "available",
"progress": "100%",
"share_id": "d94a8548-2079-4be0-b21c-0a887acd31ca", "share_id": "d94a8548-2079-4be0-b21c-0a887acd31ca",
"availability_zone": "nova", "availability_zone": "nova",
"replica_state": null, "replica_state": null,

View File

@ -27,6 +27,7 @@
"aim": "doc" "aim": "doc"
}, },
"status": "available", "status": "available",
"progress": "100%",
"description": "My custom share London", "description": "My custom share London",
"host": "manila2@generic1#GENERIC1", "host": "manila2@generic1#GENERIC1",
"access_rules_status": "active", "access_rules_status": "active",

View File

@ -25,6 +25,7 @@
"project_id": "16e1ab15c35a457e9c2b2aa189f544e1", "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
"metadata": {}, "metadata": {},
"status": "error", "status": "error",
"progress": null,
"access_rules_status": "active", "access_rules_status": "active",
"description": "There is a share description.", "description": "There is a share description.",
"host": "manila2@generic1#GENERIC1", "host": "manila2@generic1#GENERIC1",
@ -64,6 +65,7 @@
"project_id": "16e1ab15c35a457e9c2b2aa189f544e1", "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
"metadata": {}, "metadata": {},
"status": "available", "status": "available",
"progress": "100%",
"access_rules_status": "active", "access_rules_status": "active",
"description": "Changed description.", "description": "Changed description.",
"host": "manila2@generic1#GENERIC1", "host": "manila2@generic1#GENERIC1",

View File

@ -48,6 +48,7 @@ Response parameters
- status: status_5 - status: status_5
- access_rules_status: access_rules_status - access_rules_status: access_rules_status
- share_id: share_id_2 - share_id: share_id_2
- progress: progress_share_instance
- availability_zone: availability_zone_1 - availability_zone: availability_zone_1
- created_at: created_at - created_at: created_at
- replica_state: replica_state - replica_state: replica_state
@ -105,6 +106,7 @@ Response parameters
- status: status_5 - status: status_5
- access_rules_status: access_rules_status - access_rules_status: access_rules_status
- share_id: share_id_2 - share_id: share_id_2
- progress: progress_share_instance
- availability_zone: availability_zone_1 - availability_zone: availability_zone_1
- created_at: created_at - created_at: created_at
- replica_state: replica_state - replica_state: replica_state

View File

@ -39,6 +39,8 @@ A share has one of these status values:
+----------------------------------------+--------------------------------------------------------+ +----------------------------------------+--------------------------------------------------------+
| ``creating`` | The share is being created. | | ``creating`` | The share is being created. |
+----------------------------------------+--------------------------------------------------------+ +----------------------------------------+--------------------------------------------------------+
| ``creating_from_snapshot`` | The share is being created from a parent snapshot. |
+----------------------------------------+--------------------------------------------------------+
| ``deleting`` | The share is being deleted. | | ``deleting`` | The share is being deleted. |
+----------------------------------------+--------------------------------------------------------+ +----------------------------------------+--------------------------------------------------------+
| ``deleted`` | The share was deleted. | | ``deleted`` | The share was deleted. |
@ -220,6 +222,7 @@ Response parameters
- project_id: project_id - project_id: project_id
- metadata: metadata - metadata: metadata
- status: status_16 - status: status_16
- progress: progress_share_instance
- description: description - description: description
- host: host_1 - host: host_1
- access_rules_status: access_rules_status - access_rules_status: access_rules_status
@ -291,6 +294,7 @@ Response parameters
- project_id: project_id - project_id: project_id
- metadata: metadata - metadata: metadata
- status: status_16 - status: status_16
- progress: progress_share_instance
- description: description - description: description
- host: host_9 - host: host_9
- access_rules_status: access_rules_status - access_rules_status: access_rules_status
@ -369,6 +373,7 @@ Response parameters
- id: id_4 - id: id_4
- status: status_3 - status: status_3
- progress: progress_share_instance
- links: links - links: links
- project_id: project_id - project_id: project_id
- share_proto: share_proto - share_proto: share_proto

View File

@ -144,13 +144,16 @@ REST_API_VERSION_HISTORY = """
filters, support querying user messages within the specified time filters, support querying user messages within the specified time
period. period.
* 2.53 - Added quota control to share replicas. * 2.53 - Added quota control to share replicas.
* 2.54 - Share and share instance objects include a new field called
"progress" which indicates the completion of a share creation
operation as a percentage.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
# The default api version request is defined to be the # The default api version request is defined to be the
# minimum version of the API supported. # minimum version of the API supported.
_MIN_API_VERSION = "2.0" _MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.53" _MAX_API_VERSION = "2.54"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -296,3 +296,8 @@ user documentation.
2.53 2.53
---- ----
Added quota control for share replicas and replica gigabytes. Added quota control for share replicas and replica gigabytes.
2.54
----
Share and share instance objects include a new field called "progress" which
indicates the completion of a share creation operation as a percentage.

View File

@ -11,6 +11,7 @@
# under the License. # under the License.
from manila.api import common from manila.api import common
from manila.common import constants
class ViewBuilder(common.ViewBuilder): class ViewBuilder(common.ViewBuilder):
@ -25,6 +26,8 @@ class ViewBuilder(common.ViewBuilder):
"add_replication_fields", "add_replication_fields",
"add_share_type_field", "add_share_type_field",
"add_cast_rules_to_readonly_field", "add_cast_rules_to_readonly_field",
"add_progress_field",
"translate_creating_from_snapshot_status",
] ]
def detail_list(self, request, instances): def detail_list(self, request, instances):
@ -47,6 +50,7 @@ class ViewBuilder(common.ViewBuilder):
'export_location': share_instance.get('export_location'), 'export_location': share_instance.get('export_location'),
'export_locations': export_locations, 'export_locations': export_locations,
} }
self.update_versioned_resource_dict( self.update_versioned_resource_dict(
request, instance_dict, share_instance) request, instance_dict, share_instance)
return {'share_instance': instance_dict} return {'share_instance': instance_dict}
@ -91,3 +95,14 @@ class ViewBuilder(common.ViewBuilder):
share_instance): share_instance):
instance_dict['cast_rules_to_readonly'] = share_instance.get( instance_dict['cast_rules_to_readonly'] = share_instance.get(
'cast_rules_to_readonly', False) 'cast_rules_to_readonly', False)
@common.ViewBuilder.versioned_method("1.0", "2.53")
def translate_creating_from_snapshot_status(self, context, instance_dict,
share_instance):
if (share_instance.get('status') ==
constants.STATUS_CREATING_FROM_SNAPSHOT):
instance_dict['status'] = constants.STATUS_CREATING
@common.ViewBuilder.versioned_method("2.54")
def add_progress_field(self, context, instance_dict, share_instance):
instance_dict['progress'] = share_instance.get('progress')

View File

@ -34,6 +34,8 @@ class ViewBuilder(common.ViewBuilder):
"translate_access_rules_status", "translate_access_rules_status",
"add_share_group_fields", "add_share_group_fields",
"add_mount_snapshot_support_field", "add_mount_snapshot_support_field",
"add_progress_field",
"translate_creating_from_snapshot_status",
] ]
def summary_list(self, request, shares, count=None): def summary_list(self, request, shares, count=None):
@ -92,6 +94,7 @@ class ViewBuilder(common.ViewBuilder):
'is_public': share.get('is_public'), 'is_public': share.get('is_public'),
'export_locations': export_locations, 'export_locations': export_locations,
} }
self.update_versioned_resource_dict(request, share_dict, share) self.update_versioned_resource_dict(request, share_dict, share)
if context.is_admin: if context.is_admin:
@ -184,3 +187,13 @@ class ViewBuilder(common.ViewBuilder):
shares_dict['shares_links'] = shares_links shares_dict['shares_links'] = shares_links
return shares_dict return shares_dict
@common.ViewBuilder.versioned_method("1.0", "2.53")
def translate_creating_from_snapshot_status(self, context, share_dict,
share):
if share.get('status') == constants.STATUS_CREATING_FROM_SNAPSHOT:
share_dict['status'] = constants.STATUS_CREATING
@common.ViewBuilder.versioned_method("2.54")
def add_progress_field(self, context, share_dict, share):
share_dict['progress'] = share.get('progress')

View File

@ -18,6 +18,7 @@ DB_MAX_INT = 0x7FFFFFFF
# SHARE AND GENERAL STATUSES # SHARE AND GENERAL STATUSES
STATUS_CREATING = 'creating' STATUS_CREATING = 'creating'
STATUS_CREATING_FROM_SNAPSHOT = 'creating_from_snapshot'
STATUS_DELETING = 'deleting' STATUS_DELETING = 'deleting'
STATUS_DELETED = 'deleted' STATUS_DELETED = 'deleted'
STATUS_ERROR = 'error' STATUS_ERROR = 'error'

View File

@ -340,10 +340,11 @@ def share_instances_get_all_by_share_server(context, share_server_id):
share_server_id) share_server_id)
def share_instances_get_all_by_host(context, host, with_share_data=False): def share_instances_get_all_by_host(context, host, with_share_data=False,
status=None):
"""Returns all share instances with given host.""" """Returns all share instances with given host."""
return IMPL.share_instances_get_all_by_host( return IMPL.share_instances_get_all_by_host(
context, host, with_share_data=with_share_data) context, host, with_share_data=with_share_data, status=status)
def share_instances_get_all_by_share_network(context, share_network_id): def share_instances_get_all_by_share_network(context, share_network_id):

View File

@ -0,0 +1,62 @@
# 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-progress-field-to-share-instance
Revision ID: e6d88547b381
Revises: 805685098bd2
Create Date: 2020-01-31 14:06:15.952747
"""
# revision identifiers, used by Alembic.
revision = 'e6d88547b381'
down_revision = '805685098bd2'
from alembic import op
from manila.common import constants
from manila.db.migrations import utils
from oslo_log import log
import sqlalchemy as sa
LOG = log.getLogger(__name__)
def upgrade():
try:
connection = op.get_bind()
op.add_column('share_instances',
sa.Column('progress', sa.String(32), nullable=True,
default=None))
share_instances_table = utils.load_table('share_instances', connection)
updated_data = {'progress': '100%'}
# pylint: disable=no-value-for-parameter
op.execute(
share_instances_table.update().where(
share_instances_table.c.status == constants.STATUS_AVAILABLE,
).values(updated_data)
)
except Exception:
LOG.error("Column share_instances.progress not created.")
raise
def downgrade():
try:
op.drop_column('share_instances', 'progress')
except Exception:
LOG.error("Column share_instances.progress not dropped.")
raise

View File

@ -1579,7 +1579,7 @@ def _set_instances_share_data(context, instances, session):
@require_admin_context @require_admin_context
def share_instances_get_all_by_host(context, host, with_share_data=False, def share_instances_get_all_by_host(context, host, with_share_data=False,
session=None): status=None, session=None):
"""Retrieves all share instances hosted on a host.""" """Retrieves all share instances hosted on a host."""
session = session or get_session() session = session or get_session()
instances = ( instances = (
@ -1588,8 +1588,12 @@ def share_instances_get_all_by_host(context, host, with_share_data=False,
models.ShareInstance.host == host, models.ShareInstance.host == host,
models.ShareInstance.host.like("{0}#%".format(host)) models.ShareInstance.host.like("{0}#%".format(host))
) )
).all() )
) )
if status is not None:
instances = instances.filter(models.ShareInstance.status == status)
# Returns list of all instances that satisfy filters.
instances = instances.all()
if with_share_data: if with_share_data:
instances = _set_instances_share_data(context, instances, session) instances = _set_instances_share_data(context, instances, session)

View File

@ -257,6 +257,11 @@ class Share(BASE, ManilaBase):
return len(replicas) > 1 return len(replicas) > 1
return False return False
@property
def progress(self):
if len(self.instances) > 0:
return self.instance.progress
@property @property
def instance(self): def instance(self):
# NOTE(gouthamr): The order of preference: status 'replication_change', # NOTE(gouthamr): The order of preference: status 'replication_change',
@ -364,6 +369,7 @@ class ShareInstance(BASE, ManilaBase):
deleted = Column(String(36), default='False') deleted = Column(String(36), default='False')
host = Column(String(255)) host = Column(String(255))
status = Column(String(255)) status = Column(String(255))
progress = Column(String(32))
ACCESS_STATUS_PRIORITIES = { ACCESS_STATUS_PRIORITIES = {
constants.STATUS_ACTIVE: 0, constants.STATUS_ACTIVE: 0,

View File

@ -80,6 +80,12 @@ class Detail(object):
"set to extending_error. This action cannot be re-attempted until " "set to extending_error. This action cannot be re-attempted until "
"the status has been rectified. Contact your administrator to " "the status has been rectified. Contact your administrator to "
"determine the cause of this failure.")) "determine the cause of this failure."))
FILTER_CREATE_FROM_SNAPSHOT = ('016', FILTER_MSG % 'CreateFromSnapshot')
DRIVER_FAILED_CREATING_FROM_SNAP = (
'017',
_("Share Driver has failed to create the share from snapshot. This "
"operation can be re-attempted by creating a new share. Contact "
"your administrator to determine the cause of this failure."))
ALL = (UNKNOWN_ERROR, ALL = (UNKNOWN_ERROR,
NO_VALID_HOST, NO_VALID_HOST,
@ -95,7 +101,9 @@ class Detail(object):
FILTER_JSON, FILTER_JSON,
FILTER_RETRY, FILTER_RETRY,
FILTER_REPLICATION, FILTER_REPLICATION,
DRIVER_FAILED_EXTEND) DRIVER_FAILED_EXTEND,
FILTER_CREATE_FROM_SNAPSHOT,
DRIVER_FAILED_CREATING_FROM_SNAP)
# Exception and detail mappings # Exception and detail mappings
EXCEPTION_DETAIL_MAPPINGS = { EXCEPTION_DETAIL_MAPPINGS = {
@ -113,6 +121,7 @@ class Detail(object):
'JsonFilter': FILTER_JSON, 'JsonFilter': FILTER_JSON,
'RetryFilter': FILTER_RETRY, 'RetryFilter': FILTER_RETRY,
'ShareReplicationFilter': FILTER_REPLICATION, 'ShareReplicationFilter': FILTER_REPLICATION,
'CreateFromSnapshotFilter': FILTER_CREATE_FROM_SNAPSHOT,
} }

View File

@ -162,21 +162,27 @@ class FilterScheduler(base.Scheduler):
share_group = request_spec.get('share_group') share_group = request_spec.get('share_group')
# NOTE(gouthamr): If 'active_replica_host' is present in the request # NOTE(gouthamr): If 'active_replica_host' or 'snapshot_host' is
# spec, pass that host's 'replication_domain' to the # present in the request spec, pass that host's 'replication_domain' to
# ShareReplication filter. # the ShareReplication and CreateFromSnapshot filters.
active_replica_host = request_spec.get('active_replica_host') active_replica_host = request_spec.get('active_replica_host')
replication_domain = None snapshot_host = request_spec.get('snapshot_host')
allowed_hosts = []
if active_replica_host: if active_replica_host:
allowed_hosts.append(active_replica_host)
if snapshot_host:
allowed_hosts.append(snapshot_host)
replication_domain = None
if active_replica_host or snapshot_host:
temp_hosts = self.host_manager.get_all_host_states_share(elevated) temp_hosts = self.host_manager.get_all_host_states_share(elevated)
ar_host = next((host for host in temp_hosts matching_host = next((host for host in temp_hosts
if host.host == active_replica_host), None) if host.host in allowed_hosts), None)
if ar_host: if matching_host:
replication_domain = ar_host.replication_domain replication_domain = matching_host.replication_domain
# NOTE(zengyingzhe): remove the 'share_backend_name' extra spec, # NOTE(zengyingzhe): remove the 'share_backend_name' extra spec,
# let scheduler choose the available host for this replica # let scheduler choose the available host for this replica or
# creation request. # snapshot clone creation request.
share_type.get('extra_specs', {}).pop('share_backend_name', None) share_type.get('extra_specs', {}).pop('share_backend_name', None)
if filter_properties is None: if filter_properties is None:

View File

@ -0,0 +1,71 @@
# Copyright 2019 NetApp, Inc.
# 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 oslo_log import log
from manila.scheduler.filters import base_host
from manila.share import utils as share_utils
LOG = log.getLogger(__name__)
class CreateFromSnapshotFilter(base_host.BaseHostFilter):
"""CreateFromSnapshotFilter filters hosts based on replication_domain."""
def host_passes(self, host_state, filter_properties):
"""Return True if new share's host is compatible with snapshot's host.
Design of this filter:
- Creating shares from snapshots in another pool or backend needs
to match with one of the below conditions:
- The backend of the new share must be the same as its parent
snapshot.
- Both new share and snapshot are in the same replication_domain
"""
snapshot_id = filter_properties.get('request_spec', {}).get(
'snapshot_id')
snapshot_host = filter_properties.get(
'request_spec', {}).get('snapshot_host')
if None in [snapshot_id, snapshot_host]:
# NOTE(silvacarlose): if the request does not contain a snapshot_id
# or a snapshot_host, the user is not creating a share from a
# snapshot and we don't need to filter out the host.
return True
snapshot_backend = share_utils.extract_host(snapshot_host, 'backend')
snapshot_rep_domain = filter_properties.get('replication_domain')
host_backend = share_utils.extract_host(host_state.host, 'backend')
host_rep_domain = host_state.replication_domain
# Same backend
if host_backend == snapshot_backend:
return True
# Same replication domain
if snapshot_rep_domain and snapshot_rep_domain == host_rep_domain:
return True
msg = ("The parent's snapshot %(snapshot_id)s back end and "
"replication domain don't match with the back end and "
"replication domain of the Host %(host)s.")
kwargs = {
"snapshot_id": snapshot_id,
"host": host_state.host
}
LOG.debug(msg, kwargs)
return False

View File

@ -48,6 +48,7 @@ host_manager_opts = [
'CapabilitiesFilter', 'CapabilitiesFilter',
'DriverFilter', 'DriverFilter',
'ShareReplicationFilter', 'ShareReplicationFilter',
'CreateFromSnapshotFilter',
], ],
help='Which filter class names to use for filtering hosts ' help='Which filter class names to use for filtering hosts '
'when not specified in the request.'), 'when not specified in the request.'),
@ -55,6 +56,7 @@ host_manager_opts = [
default=[ default=[
'CapacityWeigher', 'CapacityWeigher',
'GoodnessWeigher', 'GoodnessWeigher',
'HostAffinityWeigher',
], ],
help='Which weigher class names to use for weighing hosts.'), help='Which weigher class names to use for weighing hosts.'),
cfg.ListOpt( cfg.ListOpt(

View File

@ -0,0 +1,65 @@
# Copyright 2019 NetApp, Inc.
# 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 import context
from manila.db import api as db_api
from manila.scheduler.weighers import base_host
from manila.share import utils as share_utils
class HostAffinityWeigher(base_host.BaseHostWeigher):
def _weigh_object(self, obj, weight_properties):
"""Weigh hosts based on their proximity to the source's share pool.
If no snapshot_id was provided will return 0, otherwise, if source and
destination hosts are located on:
1. same back ends and pools: host is a perfect choice (100)
2. same back ends and different pools: host is a very good choice (75)
3. different back ends with the same AZ: host is a good choice (50)
4. different back ends and AZs: host isn't so good choice (25)
"""
ctx = context.get_admin_context()
request_spec = weight_properties.get('request_spec')
snapshot_id = request_spec.get('snapshot_id')
snapshot_host = request_spec.get('snapshot_host')
if None in [snapshot_id, snapshot_host]:
# NOTE(silvacarlose): if the request does not contain a snapshot_id
# or a snapshot_host, the user is not creating a share from a
# snapshot and we don't need to weigh the host.
return 0
snapshot_ref = db_api.share_snapshot_get(ctx, snapshot_id)
# Source host info: pool, backend and availability zone
src_pool = share_utils.extract_host(snapshot_host, 'pool')
src_backend = share_utils.extract_host(
request_spec.get('snapshot_host'), 'backend')
src_az = snapshot_ref['share']['availability_zone']
# Destination host info: pool, backend and availability zone
dst_pool = share_utils.extract_host(obj.host, 'pool')
dst_backend = share_utils.extract_host(obj.host, 'backend')
# NOTE(dviroel): All hosts were already filtered by the availability
# zone parameter.
dst_az = None
if weight_properties['availability_zone_id']:
dst_az = db_api.availability_zone_get(
ctx, weight_properties['availability_zone_id']).name
if src_backend == dst_backend:
return 100 if (src_pool and src_pool == dst_pool) else 75
else:
return 50 if (src_az and src_az == dst_az) else 25

View File

@ -46,7 +46,11 @@ share_api_opts = [
default=False, default=False,
help='If set to False, then share creation from snapshot will ' help='If set to False, then share creation from snapshot will '
'be performed on the same host. ' 'be performed on the same host. '
'If set to True, then scheduling step will be used.') 'If set to True, then scheduler will be used.'
'When enabling this option make sure that filter '
'CreateShareFromSnapshot is enabled and to have hosts '
'reporting replication_domain option.'
)
] ]
CONF = cfg.CONF CONF = cfg.CONF
@ -190,7 +194,18 @@ class API(base.Base):
share_type_id = share_type['id'] if share_type else None share_type_id = share_type['id'] if share_type else None
else: else:
source_share = self.db.share_get(context, snapshot['share_id']) source_share = self.db.share_get(context, snapshot['share_id'])
availability_zone = source_share['instance']['availability_zone'] source_share_az = source_share['instance']['availability_zone']
if availability_zone is None:
availability_zone = source_share_az
elif (availability_zone != source_share_az
and not CONF.use_scheduler_creating_share_from_snapshot):
LOG.error("The specified availability zone must be the same "
"as parent share when you have the configuration "
"option 'use_scheduler_creating_share_from_snapshot'"
" set to False.")
msg = _("The specified availability zone must be the same "
"as the parent share when creating from snapshot.")
raise exception.InvalidInput(reason=msg)
if share_type is None: if share_type is None:
# Grab the source share's share_type if no new share type # Grab the source share's share_type if no new share type
# has been provided. # has been provided.
@ -327,19 +342,23 @@ class API(base.Base):
context, reservations, share_type_id=share_type_id) context, reservations, share_type_id=share_type_id)
host = None host = None
if snapshot and not CONF.use_scheduler_creating_share_from_snapshot: snapshot_host = None
# Shares from snapshots with restriction - source host only. if snapshot:
# It is common situation for different types of backends. snapshot_host = snapshot['share']['instance']['host']
host = snapshot['share']['instance']['host'] if not CONF.use_scheduler_creating_share_from_snapshot:
# Shares from snapshots with restriction - source host only.
# It is common situation for different types of backends.
host = snapshot['share']['instance']['host']
elif share_group: if share_group and host is None:
host = share_group['host'] host = share_group['host']
self.create_instance( self.create_instance(
context, share, share_network_id=share_network_id, host=host, context, share, share_network_id=share_network_id, host=host,
availability_zone=availability_zone, share_group=share_group, availability_zone=availability_zone, share_group=share_group,
share_group_snapshot_member=share_group_snapshot_member, share_group_snapshot_member=share_group_snapshot_member,
share_type_id=share_type_id, availability_zones=availability_zones) share_type_id=share_type_id, availability_zones=availability_zones,
snapshot_host=snapshot_host)
# Retrieve the share with instance details # Retrieve the share with instance details
share = self.db.share_get(context, share['id']) share = self.db.share_get(context, share['id'])
@ -415,14 +434,16 @@ class API(base.Base):
def create_instance(self, context, share, share_network_id=None, def create_instance(self, context, share, share_network_id=None,
host=None, availability_zone=None, host=None, availability_zone=None,
share_group=None, share_group_snapshot_member=None, share_group=None, share_group_snapshot_member=None,
share_type_id=None, availability_zones=None): share_type_id=None, availability_zones=None,
snapshot_host=None):
request_spec, share_instance = ( request_spec, share_instance = (
self.create_share_instance_and_get_request_spec( self.create_share_instance_and_get_request_spec(
context, share, availability_zone=availability_zone, context, share, availability_zone=availability_zone,
share_group=share_group, host=host, share_group=share_group, host=host,
share_network_id=share_network_id, share_network_id=share_network_id,
share_type_id=share_type_id, share_type_id=share_type_id,
availability_zones=availability_zones)) availability_zones=availability_zones,
snapshot_host=snapshot_host))
if share_group_snapshot_member: if share_group_snapshot_member:
# Inherit properties from the share_group_snapshot_member # Inherit properties from the share_group_snapshot_member
@ -461,7 +482,7 @@ class API(base.Base):
self, context, share, availability_zone=None, self, context, share, availability_zone=None,
share_group=None, host=None, share_network_id=None, share_group=None, host=None, share_network_id=None,
share_type_id=None, cast_rules_to_readonly=False, share_type_id=None, cast_rules_to_readonly=False,
availability_zones=None): availability_zones=None, snapshot_host=None):
availability_zone_id = None availability_zone_id = None
if availability_zone: if availability_zone:
@ -528,6 +549,7 @@ class API(base.Base):
'share_proto': share['share_proto'], 'share_proto': share['share_proto'],
'share_id': share['id'], 'share_id': share['id'],
'snapshot_id': share['snapshot_id'], 'snapshot_id': share['snapshot_id'],
'snapshot_host': snapshot_host,
'share_type': share_type, 'share_type': share_type,
'share_group': share_group, 'share_group': share_group,
'availability_zone_id': availability_zone_id, 'availability_zone_id': availability_zone_id,

View File

@ -24,6 +24,7 @@ import time
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from manila.common import constants
from manila import exception from manila import exception
from manila.i18n import _ from manila.i18n import _
from manila import network from manila import network
@ -649,8 +650,46 @@ class ShareDriver(object):
raise NotImplementedError() raise NotImplementedError()
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot.
Creating a share from snapshot can take longer than a simple clone
operation if data copy is required from one host to another. For this
reason driver will be able complete this creation asynchronously, by
providing a 'creating_from_snapshot' status in the model update.
When answering asynchronously, drivers must implement the call
'get_share_status' in order to provide updates for shares with
'creating_from_snapshot' status.
It is expected that the driver returns a model update to the share
manager that contains: share status and a list of export_locations.
A list of 'export_locations' is mandatory only for share in 'available'
status.
The current supported status are 'available' and
'creating_from_snapshot'.
:param context: Current context
:param share: Share instance model with share data.
:param snapshot: Snapshot instance model .
:param share_server: Share server model or None.
:param parent_share: Share model from parent snapshot with share data
and share server model.
:returns: a dictionary of updates containing current share status and
its export_location (if available).
Example::
{
'status': 'available',
'export_locations': [{...}, {...}],
}
:raises: ShareBackendException.
A ShareBackendException in this method will set the instance to
'error' and the operation will end.
"""
raise NotImplementedError() raise NotImplementedError()
def create_snapshot(self, context, snapshot, share_server=None): def create_snapshot(self, context, snapshot, share_server=None):
@ -1311,6 +1350,15 @@ class ShareDriver(object):
share_server=None): share_server=None):
"""Create a share group from a share group snapshot. """Create a share group from a share group snapshot.
When creating a share from snapshot operation takes longer than a
simple clone operation, drivers will be able to complete this creation
asynchronously, by providing a 'creating_from_snapshot' status in the
returned model update. The current supported status are 'available' and
'creating_from_snapshot'.
In order to provide updates for shares with 'creating_from_snapshot'
status, drivers must implement the call 'get_share_status'.
:param context: :param context:
:param share_group_dict: The share group details :param share_group_dict: The share group details
EXAMPLE: EXAMPLE:
@ -1372,12 +1420,27 @@ class ShareDriver(object):
share_update_list - a list of dictionaries containing dicts for share_update_list - a list of dictionaries containing dicts for
every share created in the share group. Any share dicts should at a every share created in the share group. Any share dicts should at a
minimum contain the 'id' key and 'export_locations'. minimum contain the 'id' key and, for synchronous creation, the
'export_locations'. For asynchronous share creation this dict
must also contain the key 'status' with the value set to
'creating_from_snapshot'. The current supported status are
'available' and 'creating_from_snapshot'.
Export locations should be in the same format as returned by Export locations should be in the same format as returned by
a share_create. This list may be empty or None. EXAMPLE: a share_create. This list may be empty or None. EXAMPLE:
.. code:: .. code::
[{'id': 'uuid', 'export_locations': [{...}, {...}]}] [
{
'id': 'uuid',
'export_locations': [{...}, {...}],
},
{
'id': 'uuid',
'export_locations': [],
'status': 'creating_from_snapshot',
},
]
""" """
# Ensure that the share group snapshot has members # Ensure that the share group snapshot has members
if not share_group_snapshot_dict['share_group_snapshot_members']: if not share_group_snapshot_dict['share_group_snapshot_members']:
@ -1389,18 +1452,38 @@ class ShareDriver(object):
LOG.debug('Creating share group from group snapshot %s.', LOG.debug('Creating share group from group snapshot %s.',
share_group_snapshot_dict['id']) share_group_snapshot_dict['id'])
for clone in clone_list: for clone in clone_list:
kwargs = {} kwargs = {}
share_update_info = {}
if self.driver_handles_share_servers: if self.driver_handles_share_servers:
kwargs['share_server'] = share_server kwargs['share_server'] = share_server
export_locations = ( model_update = (
self.create_share_from_snapshot( self.create_share_from_snapshot(
context, clone['share'], clone['snapshot'], **kwargs)) context, clone['share'], clone['snapshot'], **kwargs))
share_update_list.append({ if isinstance(model_update, dict):
status = model_update.get('status')
# NOTE(dviroel): share status is mandatory when answering
# a model update. If not provided, won't be possible to
# determine if was successfully created.
if status is None:
msg = _("Driver didn't provide a share status.")
raise exception.InvalidShareInstance(reason=msg)
if status not in [constants.STATUS_AVAILABLE,
constants.STATUS_CREATING_FROM_SNAPSHOT]:
msg = _('Driver returned an invalid status: %s') % status
raise exception.InvalidShareInstance(reason=msg)
share_update_info.update({'status': status})
export_locations = model_update.get('export_locations', [])
else:
# NOTE(dviroel): the driver that doesn't implement the new
# model_update will return only the export locations
export_locations = model_update
share_update_info.update({
'id': clone['share']['id'], 'id': clone['share']['id'],
'export_locations': export_locations, 'export_locations': export_locations,
}) })
share_update_list.append(share_update_info)
return None, share_update_list return None, share_update_list
def delete_share_group(self, context, share_group_dict, share_server=None): def delete_share_group(self, context, share_group_dict, share_server=None):
@ -2742,3 +2825,31 @@ class ShareDriver(object):
this share server. this share server.
""" """
raise NotImplementedError() raise NotImplementedError()
def get_share_status(self, share, share_server=None):
"""Invoked periodically to get the current status of a given share.
Driver can use this method to update the status of a share that is
still pending from other operations.
This method is expected to be called in a periodic interval set by the
'periodic_interval' configuration in seconds.
:param share: share to get updated status from.
:param share_server: share server model or None.
:returns: a dictionary of updates with the current share status, that
must be 'available', 'creating_from_snapshot' or 'error', a list of
export locations, if available, and a progress field which
indicates the completion of the share creation operation.
EXAMPLE::
{
'status': 'available',
'export_locations': [{...}, {...}],
'progress': '50%'
}
:raises: ShareBackendException.
A ShareBackendException in this method will set the instance status
to 'error'.
"""
raise NotImplementedError()

View File

@ -107,7 +107,7 @@ class EMCShareDriver(driver.ShareDriver):
return location return location
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
location = self.plugin.create_share_from_snapshot( location = self.plugin.create_share_from_snapshot(
context, share, snapshot, share_server) context, share, snapshot, share_server)

View File

@ -214,7 +214,7 @@ class PowerMaxStorageConnection(driver.StorageConnection):
'share_name': share_name}) 'share_name': share_name})
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from a snapshot - clone a snapshot.""" """Create a share from a snapshot - clone a snapshot."""
share_name = share['id'] share_name = share['id']

View File

@ -223,7 +223,7 @@ class UnityStorageConnection(driver.StorageConnection):
return locations return locations
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from a snapshot - clone a snapshot.""" """Create a share from a snapshot - clone a snapshot."""
share_name = share['id'] share_name = share['id']

View File

@ -211,7 +211,7 @@ class VNXStorageConnection(driver.StorageConnection):
'share_name': share_name}) 'share_name': share_name})
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from a snapshot - clone a snapshot.""" """Create a share from a snapshot - clone a snapshot."""
share_name = share['id'] share_name = share['id']

View File

@ -640,7 +640,7 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
@ensure_server @ensure_server
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
return self._create_share( return self._create_share(
context, share, context, share,

View File

@ -235,7 +235,7 @@ class GlusterfsShareLayoutBase(object):
@abc.abstractmethod @abc.abstractmethod
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
@abc.abstractmethod @abc.abstractmethod

View File

@ -188,7 +188,7 @@ class GlusterfsDirectoryMappedLayout(layout.GlusterfsShareLayoutBase):
pass pass
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
raise NotImplementedError raise NotImplementedError
def create_snapshot(self, context, snapshot, share_server=None): def create_snapshot(self, context, snapshot, share_server=None):

View File

@ -463,7 +463,7 @@ class GlusterfsVolumeMappedLayout(layout.GlusterfsShareLayoutBase):
return backend_snapshot_name return backend_snapshot_name
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
old_gmgr = self._share_manager(snapshot['share_instance']) old_gmgr = self._share_manager(snapshot['share_instance'])
# Snapshot clone feature in GlusterFS server essential to support this # Snapshot clone feature in GlusterFS server essential to support this

View File

@ -227,7 +227,7 @@ class HDFSNativeShareDriver(driver.ExecuteMixin, driver.ShareDriver):
return self._get_share_path(share) return self._get_share_path(share)
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Creates a snapshot.""" """Creates a snapshot."""
self._create_share(share) self._create_share(share)
share_path = '/' + share['name'] share_path = '/' + share['name']

View File

@ -448,7 +448,7 @@ class HitachiHNASDriver(driver.ShareDriver):
{'id': snapshot['id']}) {'id': snapshot['id']})
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
r"""Creates a new share from snapshot. r"""Creates a new share from snapshot.
:param context: The `context.RequestContext` object for the request :param context: The `context.RequestContext` object for the request

View File

@ -501,7 +501,7 @@ class HPE3ParShareDriver(driver.ShareDriver):
return self._hpe3par.build_export_locations(protocol, ips, path) return self._hpe3par.build_export_locations(protocol, ips, path)
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
fpg, vfs, ips = self._get_pool_location(share, share_server) fpg, vfs, ips = self._get_pool_location(share, share_server)

View File

@ -66,7 +66,7 @@ class HuaweiBase(object):
@abc.abstractmethod @abc.abstractmethod
def create_share_from_snapshot(self, share, snapshot, def create_share_from_snapshot(self, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create share from snapshot.""" """Create share from snapshot."""
@abc.abstractmethod @abc.abstractmethod

View File

@ -119,7 +119,7 @@ class HuaweiNasDriver(driver.ShareDriver):
self.plugin.extend_share(share, new_size, share_server) self.plugin.extend_share(share, new_size, share_server)
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from snapshot.""" """Create a share from snapshot."""
LOG.debug("Create a share from snapshot %s.", snapshot['snapshot_id']) LOG.debug("Create a share from snapshot %s.", snapshot['snapshot_id'])
location = self.plugin.create_share_from_snapshot(share, snapshot) location = self.plugin.create_share_from_snapshot(share, snapshot)

View File

@ -383,7 +383,7 @@ class V3StorageConnection(driver.HuaweiBase):
return share return share
def create_share_from_snapshot(self, share, snapshot, def create_share_from_snapshot(self, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from snapshot.""" """Create a share from snapshot."""
share_fs_id = self.helper.get_fsid_by_name(snapshot['share_name']) share_fs_id = self.helper.get_fsid_by_name(snapshot['share_name'])
if not share_fs_id: if not share_fs_id:

View File

@ -493,7 +493,7 @@ class GPFSShareDriver(driver.ExecuteMixin, driver.GaneshaMixin,
return location return location
def create_share_from_snapshot(self, ctx, share, snapshot, def create_share_from_snapshot(self, ctx, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from a snapshot.""" """Is called to create share from a snapshot."""
share_path = self._get_share_path(share) share_path = self._get_share_path(share)
self._create_share_from_snapshot(share, snapshot, share_path) self._create_share_from_snapshot(share, snapshot, share_path)

View File

@ -403,7 +403,7 @@ class InfiniboxShareDriver(driver.ShareDriver):
@infinisdk_to_manila_exceptions @infinisdk_to_manila_exceptions
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
name = self._make_share_name(share) name = self._make_share_name(share)
infinidat_snapshot = self._get_infinidat_snapshot(snapshot) infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
infinidat_new_share = infinidat_snapshot.create_snapshot( infinidat_new_share = infinidat_snapshot.create_snapshot(

View File

@ -343,7 +343,7 @@ class AS13000ShareDriver(driver.ShareDriver):
@inspur_driver_debug_trace @inspur_driver_debug_trace
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from snapshot.""" """Create a share from snapshot."""
pool, name, size, proto = self._get_share_instance_pnsp(share) pool, name, size, proto = self._get_share_instance_pnsp(share)

View File

@ -242,7 +242,7 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
return location return location
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
self._allocate_container(share) self._allocate_container(share)
snapshot_device_name = self._get_local_path(snapshot) snapshot_device_name = self._get_local_path(snapshot)

View File

@ -206,7 +206,7 @@ class MapRFSNativeShareDriver(driver.ExecuteMixin, driver.ShareDriver):
raise exception.ShareResourceNotFound(share_id=share['share_id']) raise exception.ShareResourceNotFound(share_id=share['share_id'])
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Creates a share from snapshot.""" """Creates a share from snapshot."""
metadata = self.api.get_share_metadata(context, metadata = self.api.get_share_metadata(context,
{'id': share['share_id']}) {'id': share['share_id']})

View File

@ -485,7 +485,7 @@ class NetAppCmodeFileStorageLibrary(object):
@na_utils.trace @na_utils.trace
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Creates new share from snapshot.""" """Creates new share from snapshot."""
vserver, vserver_client = self._get_vserver(share_server=share_server) vserver, vserver_client = self._get_vserver(share_server=share_server)
self._allocate_container_from_snapshot( self._allocate_container_from_snapshot(

View File

@ -77,7 +77,7 @@ class NexentaNasDriver(driver.ShareDriver):
return self.helper.create_filesystem(share) return self.helper.create_filesystem(share)
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
LOG.debug('Creating share from snapshot %s.', snapshot['name']) LOG.debug('Creating share from snapshot %s.', snapshot['name'])
return self.helper.create_share_from_snapshot(share, snapshot) return self.helper.create_share_from_snapshot(share, snapshot)

View File

@ -189,7 +189,7 @@ class NexentaNasDriver(driver.ShareDriver):
return '%s:%s' % (self.nas_host, filesystem['mountPoint']) return '%s:%s' % (self.nas_host, filesystem['mountPoint'])
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
snapshot_path = self._get_snapshot_path(snapshot) snapshot_path = self._get_snapshot_path(snapshot)
LOG.debug('Creating share from snapshot %s.', snapshot_path) LOG.debug('Creating share from snapshot %s.', snapshot_path)

View File

@ -519,7 +519,7 @@ class QnapShareDriver(driver.ShareDriver):
retries=5) retries=5)
@utils.synchronized('qnap-create_share_from_snapshot') @utils.synchronized('qnap-create_share_from_snapshot')
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from a snapshot.""" """Create a share from a snapshot."""
LOG.debug('Entering create_share_from_snapshot. The source ' LOG.debug('Entering create_share_from_snapshot. The source '
'snapshot=%(snap)s. The created share=%(share)s', 'snapshot=%(snap)s. The created share=%(share)s',

View File

@ -281,7 +281,7 @@ class TegileShareDriver(driver.ShareDriver):
@debugger @debugger
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from a snapshot - clone a snapshot.""" """Create a share from a snapshot - clone a snapshot."""
pool, project, share_name = self._get_pool_project_share_name(share) pool, project, share_name = self._get_pool_project_share_name(share)

View File

@ -524,7 +524,7 @@ class ACCESSShareDriver(driver.ExecuteMixin, driver.ShareDriver):
json.dumps(data2), 'DELETE') json.dumps(data2), 'DELETE')
def create_share_from_snapshot(self, ctx, share, snapshot, def create_share_from_snapshot(self, ctx, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""create share from a snapshot.""" """create share from a snapshot."""
sharename = snapshot['share_name'] sharename = snapshot['share_name']
va_sharename = self._get_va_share_name(sharename) va_sharename = self._get_va_share_name(sharename)

View File

@ -580,7 +580,7 @@ class ZFSonLinuxShareDriver(zfs_utils.ExecuteMixin, driver.ShareDriver):
@ensure_share_server_not_provided @ensure_share_server_not_provided
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create a share from snapshot.""" """Is called to create a share from snapshot."""
dataset_name = self._get_dataset_name(share) dataset_name = self._get_dataset_name(share)
ssh_cmd = '%(username)s@%(host)s' % { ssh_cmd = '%(username)s@%(host)s' % {

View File

@ -242,7 +242,7 @@ class ZFSSAShareDriver(driver.ShareDriver):
snapshot['id']) snapshot['id'])
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Create a share from a snapshot - clone a snapshot.""" """Create a share from a snapshot - clone a snapshot."""
lcfg = self.configuration lcfg = self.configuration
LOG.debug("ZFSSAShareDriver.create_share_from_snapshot: clone=%s", LOG.debug("ZFSSAShareDriver.create_share_from_snapshot: clone=%s",

View File

@ -403,7 +403,7 @@ class ShareManager(manager.SchedulerDependentManager):
self._ensure_share_instance_has_pool(ctxt, share_instance) self._ensure_share_instance_has_pool(ctxt, share_instance)
share_instance = self.db.share_instance_get( share_instance = self.db.share_instance_get(
ctxt, share_instance['id'], with_share_data=True) ctxt, share_instance['id'], with_share_data=True)
share_instance_dict = self._get_share_replica_dict( share_instance_dict = self._get_share_instance_dict(
ctxt, share_instance) ctxt, share_instance)
update_share_instances.append(share_instance_dict) update_share_instances.append(share_instance_dict)
@ -541,6 +541,7 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_instance_update(context, share_instance['id'], self.db.share_instance_update(context, share_instance['id'],
{'status': constants.STATUS_ERROR}) {'status': constants.STATUS_ERROR})
parent_share_server = None parent_share_server = None
parent_share_same_dest = False
if snapshot: if snapshot:
parent_share_server_id = ( parent_share_server_id = (
snapshot['share']['instance']['share_server_id']) snapshot['share']['instance']['share_server_id'])
@ -562,6 +563,8 @@ class ShareManager(manager.SchedulerDependentManager):
raise exception.InvalidShareServer( raise exception.InvalidShareServer(
share_server_id=parent_share_server share_server_id=parent_share_server
) )
parent_share_same_dest = (snapshot['share']['instance']['host']
== share_instance['host'])
share_network_subnet_id = None share_network_subnet_id = None
if share_network_id: if share_network_id:
share_network_subnet = ( share_network_subnet = (
@ -578,7 +581,7 @@ class ShareManager(manager.SchedulerDependentManager):
parent_share_server['share_network_subnet_id']) parent_share_server['share_network_subnet_id'])
def get_available_share_servers(): def get_available_share_servers():
if parent_share_server: if parent_share_server and parent_share_same_dest:
return [parent_share_server] return [parent_share_server]
else: else:
return ( return (
@ -929,9 +932,9 @@ class ShareManager(manager.SchedulerDependentManager):
"All snapshots must have '%(status)s' status to be " "All snapshots must have '%(status)s' status to be "
"migrated by the driver along with share " "migrated by the driver along with share "
"%(share)s.") % { "%(share)s.") % {
'share': share_ref['id'], 'share': share_ref['id'],
'status': constants.STATUS_AVAILABLE 'status': constants.STATUS_AVAILABLE
} }
raise exception.ShareMigrationFailed(reason=msg) raise exception.ShareMigrationFailed(reason=msg)
src_snap_instances = [x.instance for x in src_snapshots] src_snap_instances = [x.instance for x in src_snapshots]
@ -1376,7 +1379,7 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_instance_update( self.db.share_instance_update(
context, dest_share_instance['id'], context, dest_share_instance['id'],
{'status': constants.STATUS_AVAILABLE}) {'status': constants.STATUS_AVAILABLE, 'progress': '100%'})
self.db.share_instance_update(context, src_share_instance['id'], self.db.share_instance_update(context, src_share_instance['id'],
{'status': constants.STATUS_INACTIVE}) {'status': constants.STATUS_INACTIVE})
@ -1557,7 +1560,7 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_instance_update( self.db.share_instance_update(
context, dest_share_instance['id'], context, dest_share_instance['id'],
{'status': constants.STATUS_AVAILABLE}) {'status': constants.STATUS_AVAILABLE, 'progress': '100%'})
self.db.share_instance_update(context, src_instance_id, self.db.share_instance_update(context, src_instance_id,
{'status': constants.STATUS_INACTIVE}) {'status': constants.STATUS_INACTIVE})
@ -1753,17 +1756,44 @@ class ShareManager(manager.SchedulerDependentManager):
else: else:
share_server = None share_server = None
status = constants.STATUS_AVAILABLE
try: try:
if snapshot_ref: if snapshot_ref:
export_locations = self.driver.create_share_from_snapshot( # NOTE(dviroel): we need to provide the parent share info to
# assist drivers that create shares from snapshot in different
# pools or back ends
parent_share_instance = self.db.share_instance_get(
context, snapshot_ref['share']['instance']['id'],
with_share_data=True)
parent_share_dict = self._get_share_instance_dict(
context, parent_share_instance)
model_update = self.driver.create_share_from_snapshot(
context, share_instance, snapshot_ref.instance, context, share_instance, snapshot_ref.instance,
share_server=share_server) share_server=share_server, parent_share=parent_share_dict)
if isinstance(model_update, list):
# NOTE(dviroel): the driver that doesn't implement the new
# model_update will return only the export locations
export_locations = model_update
else:
# NOTE(dviroel): share status is mandatory when answering
# a model update. If not provided, won't be possible to
# determine if was successfully created.
status = model_update.get('status')
if status is None:
msg = _("Driver didn't provide a share status.")
raise exception.InvalidShareInstance(reason=msg)
export_locations = model_update.get('export_locations')
else: else:
export_locations = self.driver.create_share( export_locations = self.driver.create_share(
context, share_instance, share_server=share_server) context, share_instance, share_server=share_server)
if status not in [constants.STATUS_AVAILABLE,
constants.STATUS_CREATING_FROM_SNAPSHOT]:
msg = _('Driver returned an invalid status: %s') % status
raise exception.InvalidShareInstance(reason=msg)
self.db.share_export_locations_update( if export_locations:
context, share_instance['id'], export_locations) self.db.share_export_locations_update(
context, share_instance['id'], export_locations)
except Exception as e: except Exception as e:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
@ -1801,9 +1831,11 @@ class ShareManager(manager.SchedulerDependentManager):
else: else:
LOG.info("Share instance %s created successfully.", LOG.info("Share instance %s created successfully.",
share_instance_id) share_instance_id)
progress = '100%' if status == constants.STATUS_AVAILABLE else '0%'
updates = { updates = {
'status': constants.STATUS_AVAILABLE, 'status': status,
'launched_at': timeutils.utcnow(), 'launched_at': timeutils.utcnow(),
'progress': progress
} }
if share.get('replication_type'): if share.get('replication_type'):
updates['replica_state'] = constants.REPLICA_STATE_ACTIVE updates['replica_state'] = constants.REPLICA_STATE_ACTIVE
@ -1957,9 +1989,9 @@ class ShareManager(manager.SchedulerDependentManager):
with_share_data=True, with_share_server=True) with_share_data=True, with_share_server=True)
) )
replica_list = [self._get_share_replica_dict(context, r) replica_list = [self._get_share_instance_dict(context, r)
for r in replica_list] for r in replica_list]
share_replica = self._get_share_replica_dict(context, share_replica) share_replica = self._get_share_instance_dict(context, share_replica)
try: try:
replica_ref = self.driver.create_replica( replica_ref = self.driver.create_replica(
@ -1999,7 +2031,8 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_replica_update( self.db.share_replica_update(
context, share_replica['id'], context, share_replica['id'],
{'status': constants.STATUS_AVAILABLE, {'status': constants.STATUS_AVAILABLE,
'replica_state': replica_ref.get('replica_state')}) 'replica_state': replica_ref.get('replica_state'),
'progress': '100%'})
if replica_ref.get('access_rules_status'): if replica_ref.get('access_rules_status'):
self._update_share_replica_access_rules_state( self._update_share_replica_access_rules_state(
@ -2037,12 +2070,12 @@ class ShareManager(manager.SchedulerDependentManager):
with_share_data=True, with_share_server=True) with_share_data=True, with_share_server=True)
) )
replica_list = [self._get_share_replica_dict(context, r) replica_list = [self._get_share_instance_dict(context, r)
for r in replica_list] for r in replica_list]
replica_snapshots = [self._get_snapshot_instance_dict(context, s) replica_snapshots = [self._get_snapshot_instance_dict(context, s)
for s in replica_snapshots] for s in replica_snapshots]
share_server = self._get_share_server(context, share_replica) share_server = self._get_share_server(context, share_replica)
share_replica = self._get_share_replica_dict(context, share_replica) share_replica = self._get_share_instance_dict(context, share_replica)
try: try:
self.access_helper.update_access_rules( self.access_helper.update_access_rules(
@ -2157,9 +2190,9 @@ class ShareManager(manager.SchedulerDependentManager):
access_rules = self.db.share_access_get_all_for_share( access_rules = self.db.share_access_get_all_for_share(
context, share_replica['share_id']) context, share_replica['share_id'])
replica_list = [self._get_share_replica_dict(context, r) replica_list = [self._get_share_instance_dict(context, r)
for r in replica_list] for r in replica_list]
share_replica = self._get_share_replica_dict(context, share_replica) share_replica = self._get_share_instance_dict(context, share_replica)
try: try:
updated_replica_list = ( updated_replica_list = (
@ -2344,10 +2377,10 @@ class ShareManager(manager.SchedulerDependentManager):
for x in share_snapshots for x in share_snapshots
if x['aggregate_status'] == constants.STATUS_AVAILABLE] if x['aggregate_status'] == constants.STATUS_AVAILABLE]
replica_list = [self._get_share_replica_dict(context, r) replica_list = [self._get_share_instance_dict(context, r)
for r in replica_list] for r in replica_list]
share_replica = self._get_share_replica_dict(context, share_replica) share_replica = self._get_share_instance_dict(context, share_replica)
try: try:
replica_state = self.driver.update_replica_state( replica_state = self.driver.update_replica_state(
@ -3258,7 +3291,7 @@ class ShareManager(manager.SchedulerDependentManager):
# Make primitives to pass the information to the driver. # Make primitives to pass the information to the driver.
replica_list = [self._get_share_replica_dict(context, r) replica_list = [self._get_share_instance_dict(context, r)
for r in replica_list] for r in replica_list]
replica_snapshots = [self._get_snapshot_instance_dict(context, s) replica_snapshots = [self._get_snapshot_instance_dict(context, s)
for s in replica_snapshots] for s in replica_snapshots]
@ -3317,9 +3350,9 @@ class ShareManager(manager.SchedulerDependentManager):
context, snapshot_instance_filters))[0] context, snapshot_instance_filters))[0]
# Make primitives to pass the information to the driver # Make primitives to pass the information to the driver
replica_list = [self._get_share_replica_dict(context, replica) replica_list = [self._get_share_instance_dict(context, replica)
for replica in replica_list] for replica in replica_list]
active_replica = self._get_share_replica_dict(context, active_replica) active_replica = self._get_share_instance_dict(context, active_replica)
replica_snapshots = [self._get_snapshot_instance_dict(context, s) replica_snapshots = [self._get_snapshot_instance_dict(context, s)
for s in replica_snapshots] for s in replica_snapshots]
active_replica_snapshot = self._get_snapshot_instance_dict( active_replica_snapshot = self._get_snapshot_instance_dict(
@ -3391,7 +3424,7 @@ class ShareManager(manager.SchedulerDependentManager):
with_share_server=True) with_share_server=True)
) )
replica_list = [self._get_share_replica_dict(context, r) replica_list = [self._get_share_instance_dict(context, r)
for r in replica_list] for r in replica_list]
replica_snapshots = [self._get_snapshot_instance_dict(context, s) replica_snapshots = [self._get_snapshot_instance_dict(context, s)
for s in replica_snapshots] for s in replica_snapshots]
@ -3521,7 +3554,7 @@ class ShareManager(manager.SchedulerDependentManager):
with_share_data=True, with_share_server=True) with_share_data=True, with_share_server=True)
) )
replica_list = [self._get_share_replica_dict(context, r) replica_list = [self._get_share_instance_dict(context, r)
for r in replica_list] for r in replica_list]
replica_snapshots = replica_snapshots or [] replica_snapshots = replica_snapshots or []
@ -3531,7 +3564,7 @@ class ShareManager(manager.SchedulerDependentManager):
for s in replica_snapshots] for s in replica_snapshots]
replica_snapshot = self._get_snapshot_instance_dict( replica_snapshot = self._get_snapshot_instance_dict(
context, replica_snapshot) context, replica_snapshot)
share_replica = self._get_share_replica_dict(context, share_replica) share_replica = self._get_share_instance_dict(context, share_replica)
share_server = share_replica['share_server'] share_server = share_replica['share_server']
snapshot_update = None snapshot_update = None
@ -4075,6 +4108,20 @@ class ShareManager(manager.SchedulerDependentManager):
if share_update_list: if share_update_list:
for share in share_update_list: for share in share_update_list:
values = copy.deepcopy(share) values = copy.deepcopy(share)
# NOTE(dviroel): To keep backward compatibility we can't
# keep 'status' as a mandatory parameter. We'll set its
# value to 'available' as default.
i_status = values.get('status', constants.STATUS_AVAILABLE)
if i_status not in [
constants.STATUS_AVAILABLE,
constants.STATUS_CREATING_FROM_SNAPSHOT]:
msg = _(
'Driver returned an invalid status %s') % i_status
raise exception.InvalidShareInstance(reason=msg)
values['status'] = i_status
values['progress'] = (
'100%' if i_status == constants.STATUS_AVAILABLE
else '0%')
values.pop('id') values.pop('id')
export_locations = values.pop('export_locations') export_locations = values.pop('export_locations')
self.db.share_instance_update(context, share['id'], values) self.db.share_instance_update(context, share['id'], values)
@ -4308,45 +4355,83 @@ class ShareManager(manager.SchedulerDependentManager):
LOG.info("Share group snapshot %s: deleted successfully", LOG.info("Share group snapshot %s: deleted successfully",
share_group_snapshot_id) share_group_snapshot_id)
def _get_share_replica_dict(self, context, share_replica): def _get_share_server_dict(self, context, share_server):
# TODO(gouthamr): remove method when the db layer returns primitives share_server_ref = {
share_replica_ref = { 'id': share_server.get('id'),
'id': share_replica.get('id'), 'project_id': share_server.get('project_id'),
'name': share_replica.get('name'), 'updated_at': share_server.get('updated_at'),
'share_id': share_replica.get('share_id'), 'status': share_server.get('status'),
'host': share_replica.get('host'), 'host': share_server.get('host'),
'status': share_replica.get('status'), 'share_network_name': share_server.get('share_network_name'),
'replica_state': share_replica.get('replica_state'), 'share_network_id': share_server.get('share_network_id'),
'availability_zone_id': share_replica.get('availability_zone_id'), 'created_at': share_server.get('created_at'),
'export_locations': share_replica.get('export_locations') or [], 'backend_details': share_server.get('backend_details'),
'share_network_id': share_replica.get('share_network_id'), 'share_network_subnet_id':
'share_server_id': share_replica.get('share_server_id'), share_server.get('share_network_subnet_id', None),
'deleted': share_replica.get('deleted'), 'is_auto_deletable': share_server.get('is_auto_deletable', None),
'terminated_at': share_replica.get('terminated_at'), 'identifier': share_server.get('identifier', None),
'launched_at': share_replica.get('launched_at'), 'network_allocations': share_server.get('network_allocations',
'scheduled_at': share_replica.get('scheduled_at'), None)
'updated_at': share_replica.get('updated_at'),
'deleted_at': share_replica.get('deleted_at'),
'created_at': share_replica.get('created_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'),
'share_group_id': share_replica.get('share_group_id'),
'source_share_group_snapshot_member_id': share_replica.get(
'source_share_group_snapshot_member_id'),
'availability_zone': share_replica.get('availability_zone'),
} }
return share_server_ref
return share_replica_ref def _get_export_location_dict(self, context, export_location):
export_location_ref = {
'id': export_location.get('uuid'),
'path': export_location.get('path'),
'created_at': export_location.get('created_at'),
'updated_at': export_location.get('updated_at'),
'share_instance_id':
export_location.get('share_instance_id', None),
'is_admin_only': export_location.get('is_admin_only', None),
}
return export_location_ref
def _get_share_instance_dict(self, context, share_instance):
# TODO(gouthamr): remove method when the db layer returns primitives
share_instance_ref = {
'id': share_instance.get('id'),
'name': share_instance.get('name'),
'share_id': share_instance.get('share_id'),
'host': share_instance.get('host'),
'status': share_instance.get('status'),
'replica_state': share_instance.get('replica_state'),
'availability_zone_id': share_instance.get('availability_zone_id'),
'share_network_id': share_instance.get('share_network_id'),
'share_server_id': share_instance.get('share_server_id'),
'deleted': share_instance.get('deleted'),
'terminated_at': share_instance.get('terminated_at'),
'launched_at': share_instance.get('launched_at'),
'scheduled_at': share_instance.get('scheduled_at'),
'updated_at': share_instance.get('updated_at'),
'deleted_at': share_instance.get('deleted_at'),
'created_at': share_instance.get('created_at'),
'share_server': self._get_share_server(context, share_instance),
'access_rules_status': share_instance.get('access_rules_status'),
# Share details
'user_id': share_instance.get('user_id'),
'project_id': share_instance.get('project_id'),
'size': share_instance.get('size'),
'display_name': share_instance.get('display_name'),
'display_description': share_instance.get('display_description'),
'snapshot_id': share_instance.get('snapshot_id'),
'share_proto': share_instance.get('share_proto'),
'share_type_id': share_instance.get('share_type_id'),
'is_public': share_instance.get('is_public'),
'share_group_id': share_instance.get('share_group_id'),
'source_share_group_snapshot_member_id': share_instance.get(
'source_share_group_snapshot_member_id'),
'availability_zone': share_instance.get('availability_zone'),
}
if share_instance_ref['share_server']:
share_instance_ref['share_server'] = self._get_share_server_dict(
context, share_instance_ref['share_server']
)
share_instance_ref['export_locations'] = [
self._get_export_location_dict(context, el) for
el in share_instance.get('export_locations') or []
]
return share_instance_ref
def _get_snapshot_instance_dict(self, context, snapshot_instance, def _get_snapshot_instance_dict(self, context, snapshot_instance,
snapshot=None): snapshot=None):
@ -4415,3 +4500,83 @@ class ShareManager(manager.SchedulerDependentManager):
context, share, share_instance, "consumed.size", context, share, share_instance, "consumed.size",
extra_usage_info={'used_size': si['used_size'], extra_usage_info={'used_size': si['used_size'],
'gathered_at': si['gathered_at']}) 'gathered_at': si['gathered_at']})
@periodic_task.periodic_task(spacing=CONF.periodic_interval)
@utils.require_driver_initialized
def periodic_share_status_update(self, context):
"""Invokes share driver to update shares status."""
LOG.debug("Updating status of share instances.")
share_instances = self.db.share_instances_get_all_by_host(
context, self.host, with_share_data=True,
status=constants.STATUS_CREATING_FROM_SNAPSHOT)
for si in share_instances:
si_dict = self._get_share_instance_dict(context, si)
self._update_share_status(context, si_dict)
def _update_share_status(self, context, share_instance):
share_server = self._get_share_server(context, share_instance)
if share_server is not None:
share_server = self._get_share_server_dict(context,
share_server)
try:
data_updates = self.driver.get_share_status(share_instance,
share_server)
except Exception:
LOG.exception(
("Unexpected driver error occurred while updating status for "
"share instance %(id)s that belongs to share '%(share_id)s'"),
{'id': share_instance['id'],
'share_id': share_instance['share_id']}
)
data_updates = {
'status': constants.STATUS_ERROR
}
status = data_updates.get('status')
if status == constants.STATUS_ERROR:
msg = ("Status of share instance %(id)s that belongs to share "
"%(share_id)s was updated to '%(status)s'."
% {'id': share_instance['id'],
'share_id': share_instance['share_id'],
'status': status})
LOG.error(msg)
self.db.share_instance_update(
context, share_instance['id'],
{'status': constants.STATUS_ERROR,
'progress': None})
self.message_api.create(
context,
message_field.Action.UPDATE,
share_instance['project_id'],
resource_type=message_field.Resource.SHARE,
resource_id=share_instance['share_id'],
detail=message_field.Detail.DRIVER_FAILED_CREATING_FROM_SNAP)
return
export_locations = data_updates.get('export_locations')
progress = data_updates.get('progress')
statuses_requiring_update = [
constants.STATUS_AVAILABLE,
constants.STATUS_CREATING_FROM_SNAPSHOT]
if status in statuses_requiring_update:
si_updates = {
'status': status,
}
progress = ('100%' if status == constants.STATUS_AVAILABLE
else progress)
if progress is not None:
si_updates.update({'progress': progress})
self.db.share_instance_update(
context, share_instance['id'], si_updates)
msg = ("Status of share instance %(id)s that belongs to share "
"%(share_id)s was updated to '%(status)s'."
% {'id': share_instance['id'],
'share_id': share_instance['share_id'],
'status': status})
LOG.debug(msg)
if export_locations:
self.db.share_export_locations_update(
context, share_instance['id'], export_locations)

View File

@ -122,13 +122,20 @@ class ShareInstancesAPITest(test.TestCase):
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'index') req_context, self.resource_name, 'index')
def test_show(self): @ddt.data('2.3', '2.54')
def test_show(self, version):
test_instance = db_utils.create_share(size=1).instance test_instance = db_utils.create_share(size=1).instance
id = test_instance['id'] id = test_instance['id']
actual_result = self.controller.show(self._get_request('fake'), id) actual_result = self.controller.show(
self._get_request('fake', version=version), id)
self.assertEqual(id, actual_result['share_instance']['id']) self.assertEqual(id, actual_result['share_instance']['id'])
if (api_version_request.APIVersionRequest(version) >=
api_version_request.APIVersionRequest("2.54")):
self.assertIn("progress", actual_result['share_instance'])
else:
self.assertNotIn("progress", actual_result['share_instance'])
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
self.admin_context, self.resource_name, 'show') self.admin_context, self.resource_name, 'show')

View File

@ -13,9 +13,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import ddt import ddt
from manila.api.views import shares from manila.api.views import shares
from manila.common import constants
from manila import test from manila import test
from manila.tests.api.contrib import stubs from manila.tests.api.contrib import stubs
from manila.tests.api import fakes from manila.tests.api import fakes
@ -44,6 +46,7 @@ class ViewBuilderTestCase(test.TestCase):
'name': 'fake_share_type_name', 'name': 'fake_share_type_name',
}, },
'share_type_id': 'fake_share_type_id', 'share_type_id': 'fake_share_type_id',
'progress': '100%',
}, },
'replication_type': 'fake_replication_type', 'replication_type': 'fake_replication_type',
'has_replicas': False, 'has_replicas': False,
@ -51,13 +54,14 @@ class ViewBuilderTestCase(test.TestCase):
'snapshot_support': True, 'snapshot_support': True,
'create_share_from_snapshot_support': True, 'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True, 'revert_to_snapshot_support': True,
'progress': '100%',
} }
return stubs.stub_share('fake_id', **fake_share) return stubs.stub_share('fake_id', **fake_share)
def test__collection_name(self): def test__collection_name(self):
self.assertEqual('shares', self.builder._collection_name) self.assertEqual('shares', self.builder._collection_name)
@ddt.data('2.6', '2.9', '2.10', '2.11', '2.16', '2.24', '2.27') @ddt.data('2.6', '2.9', '2.10', '2.11', '2.16', '2.24', '2.27', '2.54')
def test_detail(self, microversion): def test_detail(self, microversion):
req = fakes.HTTPRequest.blank('/shares', version=microversion) req = fakes.HTTPRequest.blank('/shares', version=microversion)
@ -85,5 +89,26 @@ class ViewBuilderTestCase(test.TestCase):
expected['create_share_from_snapshot_support'] = True expected['create_share_from_snapshot_support'] = True
if self.is_microversion_ge(microversion, '2.27'): if self.is_microversion_ge(microversion, '2.27'):
expected['revert_to_snapshot_support'] = True expected['revert_to_snapshot_support'] = True
if self.is_microversion_ge(microversion, '2.54'):
expected['progress'] = '100%'
self.assertSubDictMatch(expected, result['share'])
@ddt.data('1.0', '2.51', '2.54')
def test_detail_translate_creating_from_snapshot_status(self,
microversion):
req = fakes.HTTPRequest.blank('/shares', version=microversion)
new_share_status = constants.STATUS_CREATING_FROM_SNAPSHOT
fake_shr = copy.deepcopy(self.fake_share)
fake_shr.update(
{'status': new_share_status})
result = self.builder.detail(req, fake_shr)
expected = {
'status': constants.STATUS_CREATING,
}
if self.is_microversion_ge(microversion, '2.54'):
expected['status'] = new_share_status
self.assertSubDictMatch(expected, result['share']) self.assertSubDictMatch(expected, result['share'])

View File

@ -2916,3 +2916,33 @@ class ShareNetworkSubnetMigrationChecks(BaseMigrationChecks):
self.test_case.assertRaises( self.test_case.assertRaises(
sa_exc.NoSuchTableError, sa_exc.NoSuchTableError,
utils.load_table, self.sns_table_name, engine) utils.load_table, self.sns_table_name, engine)
@map_to_migration('e6d88547b381')
class ShareInstanceProgressFieldChecks(BaseMigrationChecks):
si_table_name = 'share_instances'
progress_field_name = 'progress'
def setup_upgrade_data(self, engine):
pass
def check_upgrade(self, engine, data):
si_table = utils.load_table(self.si_table_name, engine)
for si_record in engine.execute(si_table.select()):
self.test_case.assertTrue(hasattr(si_record,
self.progress_field_name))
if si_record['status'] == constants.STATUS_AVAILABLE:
self.test_case.assertEqual('100%',
si_record[self.progress_field_name])
else:
self.test_case.assertIsNone(
si_record[self.progress_field_name])
def check_downgrade(self, engine):
si_table = utils.load_table(self.si_table_name, engine)
for si_record in engine.execute(si_table.select()):
self.test_case.assertFalse(hasattr(si_record,
self.progress_field_name))

View File

@ -441,11 +441,15 @@ class ShareDatabaseAPITestCase(test.TestCase):
self.assertEqual('share-%s' % instance['id'], instance['name']) self.assertEqual('share-%s' % instance['id'], instance['name'])
@ddt.data(True, False) @ddt.data({'with_share_data': True, 'status': constants.STATUS_AVAILABLE},
def test_share_instance_get_all_by_host(self, with_share_data): {'with_share_data': False, 'status': None})
db_utils.create_share() @ddt.unpack
def test_share_instance_get_all_by_host(self, with_share_data, status):
kwargs = {'status': status} if status else {}
db_utils.create_share(**kwargs)
instances = db_api.share_instances_get_all_by_host( instances = db_api.share_instances_get_all_by_host(
self.ctxt, 'fake_host', with_share_data) self.ctxt, 'fake_host', with_share_data=with_share_data,
status=status)
self.assertEqual(1, len(instances)) self.assertEqual(1, len(instances))
instance = instances[0] instance = instances[0]

View File

@ -16,6 +16,7 @@
from oslo_log import log from oslo_log import log
import six import six
from manila.common import constants
from manila.share import driver from manila.share import driver
from manila.tests import fake_service_instance from manila.tests import fake_service_instance
@ -74,8 +75,11 @@ class FakeShareDriver(driver.ShareDriver):
return ['/fake/path', '/fake/path2'] return ['/fake/path', '/fake/path2']
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
return ['/fake/path', '/fake/path2'] return {
'export_locations': ['/fake/path', '/fake/path2'],
'status': constants.STATUS_AVAILABLE,
}
def delete_share(self, context, share, share_server=None): def delete_share(self, context, share, share_server=None):
pass pass
@ -115,3 +119,9 @@ class FakeShareDriver(driver.ShareDriver):
def delete_share_group(self, context, group_id, share_server=None): def delete_share_group(self, context, group_id, share_server=None):
pass pass
def get_share_status(self, share, share_server=None):
return {
'export_locations': ['/fake/path', '/fake/path2'],
'status': constants.STATUS_AVAILABLE,
}

View File

@ -0,0 +1,92 @@
# Copyright 2020 NetApp, Inc.
# 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.
"""
Tests for the CreateFromSnapshotFilter.
"""
import ddt
from manila.scheduler.filters import create_from_snapshot
from manila import test
from manila.tests.scheduler import fakes
@ddt.ddt
class CreateFromSnapshotFilterTestCase(test.TestCase):
"""Test case for CreateFromSnapshotFilter."""
def setUp(self):
super(CreateFromSnapshotFilterTestCase, self).setUp()
self.filter = create_from_snapshot.CreateFromSnapshotFilter()
@staticmethod
def _create_request(snapshot_id=None,
snapshot_host=None,
replication_domain=None):
return {
'request_spec': {
'snapshot_id': snapshot_id,
'snapshot_host': snapshot_host,
},
'replication_domain': replication_domain,
}
@staticmethod
def _create_host_state(host=None, rep_domain=None):
return fakes.FakeHostState(host,
{
'replication_domain': rep_domain,
})
def test_without_snapshot_id(self):
request = self._create_request()
host = self._create_host_state(host='fake_host')
self.assertTrue(self.filter.host_passes(host, request))
def test_without_snapshot_host(self):
request = self._create_request(snapshot_id='fake_snapshot_id',
replication_domain="fake_domain")
host = self._create_host_state(host='fake_host',
rep_domain='fake_domain_2')
self.assertTrue(self.filter.host_passes(host, request))
@ddt.data(('host1@AAA#pool1', 'host1@AAA#pool1'),
('host1@AAA#pool1', 'host1@AAA#pool2'))
@ddt.unpack
def test_same_backend(self, request_host, host_state):
request = self._create_request(snapshot_id='fake_snapshot_id',
snapshot_host=request_host)
host = self._create_host_state(host=host_state)
self.assertTrue(self.filter.host_passes(host, request))
def test_same_availability_zone(self):
request = self._create_request(snapshot_id='fake_snapshot_id',
snapshot_host='fake_host',
replication_domain="fake_domain")
host = self._create_host_state(host='fake_host_2',
rep_domain='fake_domain')
self.assertTrue(self.filter.host_passes(host, request))
def test_different_backend_and_availability_zone(self):
request = self._create_request(snapshot_id='fake_snapshot_id',
snapshot_host='fake_host',
replication_domain="fake_domain")
host = self._create_host_state(host='fake_host_2',
rep_domain='fake_domain_2')
self.assertFalse(self.filter.host_passes(host, request))

View File

@ -0,0 +1,138 @@
# Copyright 2020 NetApp, Inc.
# 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.
"""
Tests for Host Affinity Weigher.
"""
import mock
from manila.common import constants
from manila.db import api as db_api
from manila.scheduler.weighers import host_affinity
from manila import test
from manila.tests import db_utils
from manila.tests.scheduler import fakes
class HostAffinityWeigherTestCase(test.TestCase):
def setUp(self):
super(HostAffinityWeigherTestCase, self).setUp()
self.weigher = host_affinity.HostAffinityWeigher()
@staticmethod
def _create_weight_properties(snapshot_id=None,
snapshot_host=None,
availability_zone_id=None):
return {
'request_spec': {
'snapshot_id': snapshot_id,
'snapshot_host': snapshot_host,
},
'availability_zone_id': availability_zone_id,
}
def test_without_snapshot_id(self):
host_state = fakes.FakeHostState('host1', {
'host': 'host1@AAA#pool2',
})
weight_properties = self._create_weight_properties(
snapshot_host='fake_snapshot_host')
weight = self.weigher._weigh_object(host_state, weight_properties)
self.assertEqual(0, weight)
def test_without_snapshot_host(self):
host_state = fakes.FakeHostState('host1', {
'host': 'host1@AAA#pool2',
})
weight_properties = self._create_weight_properties(
snapshot_id='fake_snapshot_id')
weight = self.weigher._weigh_object(host_state, weight_properties)
self.assertEqual(0, weight)
def test_same_backend_and_pool(self):
share = db_utils.create_share(host="host1@AAA#pool1",
status=constants.STATUS_AVAILABLE)
snapshot = db_utils.create_snapshot(share_id=share['id'])
self.mock_object(db_api, 'share_snapshot_get',
mock.Mock(return_value=snapshot))
host_state = fakes.FakeHostState('host1@AAA#pool1', {})
weight_properties = self._create_weight_properties(
snapshot_id=snapshot['id'], snapshot_host=share['host'])
weight = self.weigher._weigh_object(host_state, weight_properties)
self.assertEqual(100, weight)
def test_same_backend_different_pool(self):
share = db_utils.create_share(host="host1@AAA#pool1",
status=constants.STATUS_AVAILABLE)
snapshot = db_utils.create_snapshot(share_id=share['id'])
self.mock_object(db_api, 'share_snapshot_get',
mock.Mock(return_value=snapshot))
host_state = fakes.FakeHostState('host1@AAA#pool2', {})
weight_properties = self._create_weight_properties(
snapshot_id=snapshot['id'], snapshot_host=share['host'])
weight = self.weigher._weigh_object(host_state, weight_properties)
self.assertEqual(75, weight)
def test_different_backend_same_availability_zone(self):
share = db_utils.create_share(
host="host1@AAA#pool1", status=constants.STATUS_AVAILABLE,
availability_zone=fakes.FAKE_AZ_1['name'])
snapshot = db_utils.create_snapshot(share_id=share['id'])
self.mock_object(db_api, 'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.mock_object(db_api, 'availability_zone_get',
mock.Mock(return_value=type(
'FakeAZ', (object, ), {
'id': fakes.FAKE_AZ_1['id'],
'name': fakes.FAKE_AZ_1['name'],
})))
host_state = fakes.FakeHostState('host2@BBB#pool1', {})
weight_properties = self._create_weight_properties(
snapshot_id=snapshot['id'], snapshot_host=share['host'],
availability_zone_id='zone1')
weight = self.weigher._weigh_object(host_state, weight_properties)
self.assertEqual(50, weight)
def test_different_backend_and_availability_zone(self):
share = db_utils.create_share(
host="host1@AAA#pool1", status=constants.STATUS_AVAILABLE,
availability_zone=fakes.FAKE_AZ_1['name'])
snapshot = db_utils.create_snapshot(share_id=share['id'])
self.mock_object(db_api, 'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.mock_object(db_api, 'availability_zone_get',
mock.Mock(return_value=type(
'FakeAZ', (object,), {
'id': fakes.FAKE_AZ_2['id'],
'name': fakes.FAKE_AZ_2['name'],
})))
host_state = fakes.FakeHostState('host2@BBB#pool1', {})
weight_properties = self._create_weight_properties(
snapshot_id=snapshot['id'], snapshot_host=share['host'],
availability_zone_id='zone1'
)
weight = self.weigher._weigh_object(host_state, weight_properties)
self.assertEqual(25, weight)

View File

@ -226,9 +226,13 @@ class DummyDriver(driver.ShareDriver):
@slow_me_down @slow_me_down
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
return self._create_share(share, share_server=share_server) export_locations = self._create_share(share, share_server=share_server)
return {
'export_locations': export_locations,
'status': constants.STATUS_AVAILABLE
}
def _create_snapshot(self, snapshot, share_server=None): def _create_snapshot(self, snapshot, share_server=None):
snapshot_name = self._get_snapshot_name(snapshot) snapshot_name = self._get_snapshot_name(snapshot)
@ -733,3 +737,10 @@ class DummyDriver(driver.ShareDriver):
return return
self.private_storage.update(server_details['server_id'], self.private_storage.update(server_details['server_id'],
server_details) server_details)
def get_share_status(self, share, share_server=None):
return {
'status': constants.STATUS_AVAILABLE,
'export_locations': self.private_storage.get(share['id'],
key='export_location')
}

View File

@ -264,7 +264,7 @@ class GlusterfsShareLayoutBaseTestCase(test.TestCase):
"""Is called to create share.""" """Is called to create share."""
def create_share_from_snapshot(self, context, share, snapshot, def create_share_from_snapshot(self, context, share, snapshot,
share_server=None): share_server=None, parent_share=None):
"""Is called to create share from snapshot.""" """Is called to create share from snapshot."""
def create_snapshot(self, context, snapshot, share_server=None): def create_snapshot(self, context, snapshot, share_server=None):

View File

@ -769,7 +769,7 @@ class ShareAPITestCase(test.TestCase):
self.context, share, share_network_id=fake_share_network_id, self.context, share, share_network_id=fake_share_network_id,
host=None, availability_zone=None, share_group=None, host=None, availability_zone=None, share_group=None,
share_group_snapshot_member=None, share_type_id=None, share_group_snapshot_member=None, share_type_id=None,
availability_zones=expected_azs availability_zones=expected_azs, snapshot_host=None
) )
db_api.share_get.assert_called_once() db_api.share_get.assert_called_once()
@ -915,6 +915,23 @@ class ShareAPITestCase(test.TestCase):
self.context, request_spec=mock.ANY, filter_properties={})) self.context, request_spec=mock.ANY, filter_properties={}))
self.assertFalse(self.api.share_rpcapi.create_share_instance.called) self.assertFalse(self.api.share_rpcapi.create_share_instance.called)
def test_create_share_instance_from_snapshot(self):
snapshot, share, _, _ = self._setup_create_from_snapshot_mocks()
request_spec, share_instance = (
self.api.create_share_instance_and_get_request_spec(
self.context, share)
)
self.assertIsNotNone(share_instance)
self.assertEqual(share['id'],
request_spec['share_instance_properties']['share_id'])
self.assertEqual(share['snapshot_id'], request_spec['snapshot_id'])
self.assertFalse(
self.api.share_rpcapi.create_share_instance_and_get_request_spec
.called)
def test_create_instance_share_group_snapshot_member(self): def test_create_instance_share_group_snapshot_member(self):
fake_req_spec = { fake_req_spec = {
'share_properties': 'fake_share_properties', 'share_properties': 'fake_share_properties',
@ -2126,10 +2143,14 @@ class ShareAPITestCase(test.TestCase):
share_api.policy.check_policy.assert_called_once_with( share_api.policy.check_policy.assert_called_once_with(
self.context, 'share', 'create_snapshot', share) self.context, 'share', 'create_snapshot', share)
@ddt.data({'use_scheduler': False, 'valid_host': 'fake'}, @ddt.data({'use_scheduler': False, 'valid_host': 'fake',
{'use_scheduler': True, 'valid_host': None}) 'az': None},
{'use_scheduler': True, 'valid_host': None,
'az': None},
{'use_scheduler': True, 'valid_host': None,
'az': "fakeaz2"})
@ddt.unpack @ddt.unpack
def test_create_from_snapshot(self, use_scheduler, valid_host): def test_create_from_snapshot(self, use_scheduler, valid_host, az):
snapshot, share, share_data, request_spec = ( snapshot, share, share_data, request_spec = (
self._setup_create_from_snapshot_mocks( self._setup_create_from_snapshot_mocks(
use_scheduler=use_scheduler, host=valid_host) use_scheduler=use_scheduler, host=valid_host)
@ -2138,8 +2159,8 @@ class ShareAPITestCase(test.TestCase):
mock_get_share_type_call = self.mock_object( mock_get_share_type_call = self.mock_object(
share_types, 'get_share_type', mock.Mock(return_value=share_type)) share_types, 'get_share_type', mock.Mock(return_value=share_type))
az = share_data.pop('availability_zone')
az = snapshot['share']['availability_zone'] if not az else az
self.api.create( self.api.create(
self.context, self.context,
share_data['share_proto'], share_data['share_proto'],
@ -2147,9 +2168,11 @@ class ShareAPITestCase(test.TestCase):
share_data['display_name'], share_data['display_name'],
share_data['display_description'], share_data['display_description'],
snapshot_id=snapshot['id'], snapshot_id=snapshot['id'],
availability_zone=az availability_zone=az,
) )
share_data.pop('availability_zone')
mock_get_share_type_call.assert_called_once_with( mock_get_share_type_call.assert_called_once_with(
self.context, share['share_type_id']) self.context, share['share_type_id'])
self.assertSubDictMatch(share_data, self.assertSubDictMatch(share_data,
@ -2157,9 +2180,10 @@ class ShareAPITestCase(test.TestCase):
self.api.create_instance.assert_called_once_with( self.api.create_instance.assert_called_once_with(
self.context, share, share_network_id=share['share_network_id'], self.context, share, share_network_id=share['share_network_id'],
host=valid_host, share_type_id=share_type['id'], host=valid_host, share_type_id=share_type['id'],
availability_zone=snapshot['share']['availability_zone'], availability_zone=az,
share_group=None, share_group_snapshot_member=None, share_group=None, share_group_snapshot_member=None,
availability_zones=None) availability_zones=None,
snapshot_host=snapshot['share']['instance']['host'])
share_api.policy.check_policy.assert_called_once_with( share_api.policy.check_policy.assert_called_once_with(
self.context, 'share_snapshot', 'get_snapshot') self.context, 'share_snapshot', 'get_snapshot')
quota.QUOTAS.reserve.assert_called_once_with( quota.QUOTAS.reserve.assert_called_once_with(
@ -2196,6 +2220,19 @@ class ShareAPITestCase(test.TestCase):
self.context, 'reservation', share_type_id=share_type['id'] self.context, 'reservation', share_type_id=share_type['id']
) )
def test_create_from_snapshot_az_different_from_source(self):
snapshot, share, share_data, request_spec = (
self._setup_create_from_snapshot_mocks(use_scheduler=False)
)
self.assertRaises(exception.InvalidInput, self.api.create,
self.context, share_data['share_proto'],
share_data['size'],
share_data['display_name'],
share_data['display_description'],
snapshot_id=snapshot['id'],
availability_zone='fake_different_az')
def test_create_from_snapshot_with_different_share_type(self): def test_create_from_snapshot_with_different_share_type(self):
snapshot, share, share_data, request_spec = ( snapshot, share, share_data, request_spec = (
self._setup_create_from_snapshot_mocks() self._setup_create_from_snapshot_mocks()

View File

@ -21,6 +21,7 @@ import ddt
import mock import mock
from mock import PropertyMock from mock import PropertyMock
from manila.common import constants
from manila import exception from manila import exception
from manila import network from manila import network
from manila.share import configuration from manila.share import configuration
@ -153,6 +154,12 @@ class ShareDriverTestCase(test.TestCase):
self.assertIn(key, result) self.assertIn(key, result)
self.assertEqual('Open Source', result['vendor_name']) self.assertEqual('Open Source', result['vendor_name'])
def test_get_share_status(self):
share_driver = self._instantiate_share_driver(None, False)
self.assertRaises(NotImplementedError,
share_driver.get_share_status,
None, None)
@ddt.data( @ddt.data(
{'opt': True, 'allowed': True}, {'opt': True, 'allowed': True},
{'opt': True, 'allowed': (True, False)}, {'opt': True, 'allowed': (True, False)},
@ -789,6 +796,97 @@ class ShareDriverTestCase(test.TestCase):
self.assertIsNone(share_group_update) self.assertIsNone(share_group_update)
self.assertEqual(expected_share_updates, share_update) self.assertEqual(expected_share_updates, share_update)
def test_create_share_group_from_share_group_snapshot_with_dict_raise(
self):
share_driver = self._instantiate_share_driver(None, False)
fake_shares = [
{'id': 'fake_share_%d' % i,
'source_share_group_snapshot_member_id': 'fake_member_%d' % i}
for i in (1, 2)]
fake_share_group_dict = {
'source_share_group_snapshot_id': 'some_fake_uuid_abc',
'shares': fake_shares,
'id': 'some_fake_uuid_def',
}
fake_share_group_snapshot_dict = {
'share_group_snapshot_members': [
{'id': 'fake_member_1'}, {'id': 'fake_member_2'}],
'id': 'fake_share_group_snapshot_id',
}
self.mock_object(
share_driver, 'create_share_from_snapshot',
mock.Mock(side_effect=[{
'export_locations': 'fake_export1',
'status': constants.STATUS_CREATING},
{'export_locations': 'fake_export2',
'status': constants.STATUS_CREATING}]))
self.assertRaises(
exception.InvalidShareInstance,
share_driver.create_share_group_from_share_group_snapshot,
'fake_context',
fake_share_group_dict,
fake_share_group_snapshot_dict)
def test_create_share_group_from_share_group_snapshot_with_dict(
self):
share_driver = self._instantiate_share_driver(None, False)
fake_shares = [
{'id': 'fake_share_%d' % i,
'source_share_group_snapshot_member_id': 'fake_member_%d' % i}
for i in (1, 2)]
fake_share_group_dict = {
'source_share_group_snapshot_id': 'some_fake_uuid_abc',
'shares': fake_shares,
'id': 'some_fake_uuid_def',
}
fake_share_group_snapshot_dict = {
'share_group_snapshot_members': [
{'id': 'fake_member_1'}, {'id': 'fake_member_2'}],
'id': 'fake_share_group_snapshot_id',
}
mock_create = self.mock_object(
share_driver, 'create_share_from_snapshot',
mock.Mock(side_effect=[{
'export_locations': 'fake_export1',
'status': constants.STATUS_CREATING_FROM_SNAPSHOT},
{'export_locations': 'fake_export2',
'status': constants.STATUS_AVAILABLE}]))
expected_share_updates = [
{
'id': 'fake_share_1',
'status': constants.STATUS_CREATING_FROM_SNAPSHOT,
'export_locations': 'fake_export1',
},
{
'id': 'fake_share_2',
'status': constants.STATUS_AVAILABLE,
'export_locations': 'fake_export2',
},
]
share_group_update, share_update = (
share_driver.create_share_group_from_share_group_snapshot(
'fake_context', fake_share_group_dict,
fake_share_group_snapshot_dict))
mock_create.assert_has_calls([
mock.call(
'fake_context',
{'id': 'fake_share_1',
'source_share_group_snapshot_member_id': 'fake_member_1'},
{'id': 'fake_member_1'}),
mock.call(
'fake_context',
{'id': 'fake_share_2',
'source_share_group_snapshot_member_id': 'fake_member_2'},
{'id': 'fake_member_2'})
])
self.assertIsNone(share_group_update)
self.assertEqual(expected_share_updates, share_update)
def test_create_share_group_from_sg_snapshot_with_no_members(self): def test_create_share_group_from_sg_snapshot_with_no_members(self):
share_driver = self._instantiate_share_driver(None, False) share_driver = self._instantiate_share_driver(None, False)
fake_share_group_dict = {} fake_share_group_dict = {}

View File

@ -287,6 +287,9 @@ class ShareManagerTestCase(test.TestCase):
status=constants.STATUS_MIGRATING, status=constants.STATUS_MIGRATING,
task_state=constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS, task_state=constants.TASK_STATE_MIGRATION_DRIVER_IN_PROGRESS,
display_name='fake_name_6').instance, display_name='fake_name_6').instance,
db_utils.create_share(
id='fake_id_7', status=constants.STATUS_CREATING_FROM_SNAPSHOT,
display_name='fake_name_7').instance,
] ]
instances[4]['access_rules_status'] = ( instances[4]['access_rules_status'] = (
@ -320,7 +323,7 @@ class ShareManagerTestCase(test.TestCase):
sorted(new_backend_info.items())).encode('utf-8')).hexdigest() if sorted(new_backend_info.items())).encode('utf-8')).hexdigest() if
new_backend_info else None) new_backend_info else None)
old_backend_info = {'info_hash': old_backend_info_hash} old_backend_info = {'info_hash': old_backend_info_hash}
share_server = 'fake_share_server_does_not_matter' share_server = fakes.fake_share_server_get()
instances, rules = self._setup_init_mocks() instances, rules = self._setup_init_mocks()
fake_export_locations = ['fake/path/1', 'fake/path'] fake_export_locations = ['fake/path/1', 'fake/path']
fake_update_instances = { fake_update_instances = {
@ -350,6 +353,8 @@ class ShareManagerTestCase(test.TestCase):
self.mock_object(self.share_manager, '_ensure_share_instance_has_pool') self.mock_object(self.share_manager, '_ensure_share_instance_has_pool')
self.mock_object(self.share_manager, '_get_share_server', self.mock_object(self.share_manager, '_get_share_server',
mock.Mock(return_value=share_server)) mock.Mock(return_value=share_server))
self.mock_object(self.share_manager, '_get_share_server_dict',
mock.Mock(return_value=share_server))
self.mock_object(self.share_manager, 'publish_service_capabilities', self.mock_object(self.share_manager, 'publish_service_capabilities',
mock.Mock()) mock.Mock())
self.mock_object(self.share_manager.db, self.mock_object(self.share_manager.db,
@ -361,7 +366,7 @@ class ShareManagerTestCase(test.TestCase):
mock.Mock(side_effect=raise_share_access_exists) mock.Mock(side_effect=raise_share_access_exists)
) )
dict_instances = [self._get_share_replica_dict( dict_instances = [self._get_share_instance_dict(
instance, share_server=share_server) for instance in instances] instance, share_server=share_server) for instance in instances]
# call of 'init_host' method # call of 'init_host' method
@ -472,7 +477,7 @@ class ShareManagerTestCase(test.TestCase):
raise NotImplementedError raise NotImplementedError
instances = self._setup_init_mocks(setup_access_rules=False) instances = self._setup_init_mocks(setup_access_rules=False)
share_server = 'fake_share_server_does_not_matter' share_server = fakes.fake_share_server_get()
self.mock_object(self.share_manager.db, self.mock_object(self.share_manager.db,
'share_instances_get_all_by_host', 'share_instances_get_all_by_host',
mock.Mock(return_value=instances)) mock.Mock(return_value=instances))
@ -488,11 +493,13 @@ class ShareManagerTestCase(test.TestCase):
self.share_manager, '_ensure_share_instance_has_pool') self.share_manager, '_ensure_share_instance_has_pool')
self.mock_object(self.share_manager, '_get_share_server', self.mock_object(self.share_manager, '_get_share_server',
mock.Mock(return_value=share_server)) mock.Mock(return_value=share_server))
self.mock_object(self.share_manager, '_get_share_server_dict',
mock.Mock(return_value=share_server))
self.mock_object(self.share_manager, 'publish_service_capabilities') self.mock_object(self.share_manager, 'publish_service_capabilities')
self.mock_object(manager.LOG, 'error') self.mock_object(manager.LOG, 'error')
self.mock_object(manager.LOG, 'info') self.mock_object(manager.LOG, 'info')
dict_instances = [self._get_share_replica_dict( dict_instances = [self._get_share_instance_dict(
instance, share_server=share_server) for instance in instances] instance, share_server=share_server) for instance in instances]
# call of 'init_host' method # call of 'init_host' method
@ -537,44 +544,44 @@ class ShareManagerTestCase(test.TestCase):
{'id': instances[1]['id'], 'status': instances[1]['status']}, {'id': instances[1]['id'], 'status': instances[1]['status']},
) )
def _get_share_replica_dict(self, share_replica, **kwargs): def _get_share_instance_dict(self, share_instance, **kwargs):
# TODO(gouthamr): remove method when the db layer returns primitives # TODO(gouthamr): remove method when the db layer returns primitives
share_replica_ref = { share_instance_ref = {
'id': share_replica.get('id'), 'id': share_instance.get('id'),
'name': share_replica.get('name'), 'name': share_instance.get('name'),
'share_id': share_replica.get('share_id'), 'share_id': share_instance.get('share_id'),
'host': share_replica.get('host'), 'host': share_instance.get('host'),
'status': share_replica.get('status'), 'status': share_instance.get('status'),
'replica_state': share_replica.get('replica_state'), 'replica_state': share_instance.get('replica_state'),
'availability_zone_id': share_replica.get('availability_zone_id'), 'availability_zone_id': share_instance.get('availability_zone_id'),
'export_locations': share_replica.get('export_locations') or [], 'share_network_id': share_instance.get('share_network_id'),
'share_network_id': share_replica.get('share_network_id'), 'share_server_id': share_instance.get('share_server_id'),
'share_server_id': share_replica.get('share_server_id'), 'deleted': share_instance.get('deleted'),
'deleted': share_replica.get('deleted'), 'terminated_at': share_instance.get('terminated_at'),
'terminated_at': share_replica.get('terminated_at'), 'launched_at': share_instance.get('launched_at'),
'launched_at': share_replica.get('launched_at'), 'scheduled_at': share_instance.get('scheduled_at'),
'scheduled_at': share_replica.get('scheduled_at'), 'updated_at': share_instance.get('updated_at'),
'updated_at': share_replica.get('updated_at'), 'deleted_at': share_instance.get('deleted_at'),
'deleted_at': share_replica.get('deleted_at'), 'created_at': share_instance.get('created_at'),
'created_at': share_replica.get('created_at'),
'share_server': kwargs.get('share_server'), 'share_server': kwargs.get('share_server'),
'access_rules_status': share_replica.get('access_rules_status'), 'access_rules_status': share_instance.get('access_rules_status'),
# Share details # Share details
'user_id': share_replica.get('user_id'), 'user_id': share_instance.get('user_id'),
'project_id': share_replica.get('project_id'), 'project_id': share_instance.get('project_id'),
'size': share_replica.get('size'), 'size': share_instance.get('size'),
'display_name': share_replica.get('display_name'), 'display_name': share_instance.get('display_name'),
'display_description': share_replica.get('display_description'), 'display_description': share_instance.get('display_description'),
'snapshot_id': share_replica.get('snapshot_id'), 'snapshot_id': share_instance.get('snapshot_id'),
'share_proto': share_replica.get('share_proto'), 'share_proto': share_instance.get('share_proto'),
'share_type_id': share_replica.get('share_type_id'), 'share_type_id': share_instance.get('share_type_id'),
'is_public': share_replica.get('is_public'), 'is_public': share_instance.get('is_public'),
'share_group_id': share_replica.get('share_group_id'), 'share_group_id': share_instance.get('share_group_id'),
'source_share_group_snapshot_member_id': share_replica.get( 'source_share_group_snapshot_member_id': share_instance.get(
'source_share_group_snapshot_member_id'), 'source_share_group_snapshot_member_id'),
'availability_zone': share_replica.get('availability_zone'), 'availability_zone': share_instance.get('availability_zone'),
'export_locations': share_instance.get('export_locations') or [],
} }
return share_replica_ref return share_instance_ref
def test_init_host_with_exception_on_ensure_shares(self): def test_init_host_with_exception_on_ensure_shares(self):
def raise_exception(*args, **kwargs): def raise_exception(*args, **kwargs):
@ -594,8 +601,10 @@ class ShareManagerTestCase(test.TestCase):
mock.Mock(side_effect=raise_exception)) mock.Mock(side_effect=raise_exception))
self.mock_object( self.mock_object(
self.share_manager, '_ensure_share_instance_has_pool') self.share_manager, '_ensure_share_instance_has_pool')
self.mock_object(db, 'share_server_get',
mock.Mock(return_value=fakes.fake_share_server_get()))
dict_instances = [self._get_share_replica_dict(instance) dict_instances = [self._get_share_instance_dict(instance)
for instance in instances] for instance in instances]
# call of 'init_host' method # call of 'init_host' method
@ -651,7 +660,7 @@ class ShareManagerTestCase(test.TestCase):
raise exception.ManilaException(message="Fake raise") raise exception.ManilaException(message="Fake raise")
instances, rules = self._setup_init_mocks() instances, rules = self._setup_init_mocks()
share_server = 'fake_share_server_does_not_matter' share_server = fakes.fake_share_server_get()
fake_update_instances = { fake_update_instances = {
instances[0]['id']: {'status': 'available'}, instances[0]['id']: {'status': 'available'},
instances[2]['id']: {'status': 'available'}, instances[2]['id']: {'status': 'available'},
@ -677,8 +686,10 @@ class ShareManagerTestCase(test.TestCase):
mock.Mock(return_value=rules)) mock.Mock(return_value=rules))
self.mock_object(smanager.access_helper, 'update_access_rules', self.mock_object(smanager.access_helper, 'update_access_rules',
mock.Mock(side_effect=raise_exception)) mock.Mock(side_effect=raise_exception))
self.mock_object(smanager, '_get_share_server_dict',
mock.Mock(return_value=share_server))
dict_instances = [self._get_share_replica_dict( dict_instances = [self._get_share_instance_dict(
instance, share_server=share_server) for instance in instances] instance, share_server=share_server) for instance in instances]
# call of 'init_host' method # call of 'init_host' method
@ -761,6 +772,73 @@ class ShareManagerTestCase(test.TestCase):
shr = db.share_get(self.context, share_id) shr = db.share_get(self.context, share_id)
self.assertEqual(constants.STATUS_ERROR, shr['status']) self.assertEqual(constants.STATUS_ERROR, shr['status'])
def test_create_share_instance_from_snapshot_status_creating(self):
"""Test share can be created from snapshot in asynchronous mode."""
share = db_utils.create_share()
share_id = share['id']
snapshot = db_utils.create_snapshot(share_id=share_id)
snapshot_id = snapshot['id']
create_from_snap_ret = {
'status': constants.STATUS_CREATING_FROM_SNAPSHOT,
}
driver_call = self.mock_object(
self.share_manager.driver, 'create_share_from_snapshot',
mock.Mock(return_value=create_from_snap_ret))
self.share_manager.create_share_instance(
self.context, share.instance['id'], snapshot_id=snapshot_id)
self.assertTrue(driver_call.called)
self.assertEqual(share_id, db.share_get(context.get_admin_context(),
share_id).id)
shr = db.share_get(self.context, share_id)
self.assertTrue(driver_call.called)
self.assertEqual(constants.STATUS_CREATING_FROM_SNAPSHOT,
shr['status'])
self.assertEqual(0, len(shr['export_locations']))
def test_create_share_instance_from_snapshot_invalid_status(self):
"""Test share can't be created from snapshot with 'creating' status."""
share = db_utils.create_share()
share_id = share['id']
snapshot = db_utils.create_snapshot(share_id=share_id)
snapshot_id = snapshot['id']
create_from_snap_ret = {
'status': constants.STATUS_CREATING,
}
driver_call = self.mock_object(
self.share_manager.driver, 'create_share_from_snapshot',
mock.Mock(return_value=create_from_snap_ret))
self.assertRaises(exception.InvalidShareInstance,
self.share_manager.create_share_instance,
self.context,
share.instance['id'],
snapshot_id=snapshot_id)
self.assertTrue(driver_call.called)
shr = db.share_get(self.context, share_id)
self.assertEqual(constants.STATUS_ERROR, shr['status'])
def test_create_share_instance_from_snapshot_export_locations_only(self):
"""Test share can be created from snapshot on old driver interface."""
share = db_utils.create_share()
share_id = share['id']
snapshot = db_utils.create_snapshot(share_id=share_id)
snapshot_id = snapshot['id']
create_from_snap_ret = ['/path/fake', '/path/fake2', '/path/fake3']
driver_call = self.mock_object(
self.share_manager.driver, 'create_share_from_snapshot',
mock.Mock(return_value=create_from_snap_ret))
self.share_manager.create_share_instance(
self.context, share.instance['id'], snapshot_id=snapshot_id)
self.assertTrue(driver_call.called)
self.assertEqual(share_id, db.share_get(context.get_admin_context(),
share_id).id)
shr = db.share_get(self.context, share_id)
self.assertEqual(constants.STATUS_AVAILABLE, shr['status'])
self.assertEqual(3, len(shr['export_locations']))
def test_create_share_instance_from_snapshot(self): def test_create_share_instance_from_snapshot(self):
"""Test share can be created from snapshot.""" """Test share can be created from snapshot."""
share = db_utils.create_share() share = db_utils.create_share()
@ -1027,7 +1105,8 @@ class ShareManagerTestCase(test.TestCase):
{'availability_zone': 'fake_az'}, with_share_data=True), {'availability_zone': 'fake_az'}, with_share_data=True),
mock.call(mock.ANY, replica['id'], mock.call(mock.ANY, replica['id'],
{'status': constants.STATUS_AVAILABLE, {'status': constants.STATUS_AVAILABLE,
'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC}), 'replica_state': constants.REPLICA_STATE_OUT_OF_SYNC,
'progress': '100%'}),
] ]
mock_export_locs_update_call = self.mock_object( mock_export_locs_update_call = self.mock_object(
db, 'share_export_locations_update') db, 'share_export_locations_update')
@ -1104,7 +1183,8 @@ class ShareManagerTestCase(test.TestCase):
mock_replica_update_call.assert_called_once_with( mock_replica_update_call.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), replica['id'], utils.IsAMatcher(context.RequestContext), replica['id'],
{'status': constants.STATUS_AVAILABLE, {'status': constants.STATUS_AVAILABLE,
'replica_state': constants.REPLICA_STATE_IN_SYNC}) 'replica_state': constants.REPLICA_STATE_IN_SYNC,
'progress': '100%'})
mock_share_replica_access_update.assert_called_once_with( mock_share_replica_access_update.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), utils.IsAMatcher(context.RequestContext),
share_instance_id=replica['id'], share_instance_id=replica['id'],
@ -1557,7 +1637,7 @@ class ShareManagerTestCase(test.TestCase):
self.mock_object(self.share_manager.db, 'share_replica_get', self.mock_object(self.share_manager.db, 'share_replica_get',
mock.Mock(return_value=replica)) mock.Mock(return_value=replica))
self.mock_object(db, 'share_server_get', self.mock_object(db, 'share_server_get',
mock.Mock(return_value='fake_share_server')) mock.Mock(return_value=fakes.fake_share_server_get()))
self.mock_object(self.share_manager.driver, 'update_replica_state', self.mock_object(self.share_manager.driver, 'update_replica_state',
mock.Mock(side_effect=exception.ManilaException)) mock.Mock(side_effect=exception.ManilaException))
mock_db_update_call = self.mock_object( mock_db_update_call = self.mock_object(
@ -1589,7 +1669,7 @@ class ShareManagerTestCase(test.TestCase):
self.mock_object(self.share_manager.db, 'share_replica_get', self.mock_object(self.share_manager.db, 'share_replica_get',
mock.Mock(return_value=replica)) mock.Mock(return_value=replica))
self.mock_object(db, 'share_server_get', self.mock_object(db, 'share_server_get',
mock.Mock(return_value='fake_share_server')) mock.Mock(return_value={}))
self.share_manager.host = replica['host'] self.share_manager.host = replica['host']
self.mock_object(self.share_manager.driver, 'update_replica_state', self.mock_object(self.share_manager.driver, 'update_replica_state',
mock.Mock(side_effect=exception.ManilaException)) mock.Mock(side_effect=exception.ManilaException))
@ -1658,7 +1738,7 @@ class ShareManagerTestCase(test.TestCase):
replica_states = [constants.REPLICA_STATE_IN_SYNC, replica_states = [constants.REPLICA_STATE_IN_SYNC,
constants.REPLICA_STATE_OUT_OF_SYNC] constants.REPLICA_STATE_OUT_OF_SYNC]
replica = fake_replica(replica_state=random.choice(replica_states), replica = fake_replica(replica_state=random.choice(replica_states),
share_server='fake_share_server') share_server=fakes.fake_share_server_get())
active_replica = fake_replica( active_replica = fake_replica(
id='fake2', replica_state=constants.STATUS_ACTIVE) id='fake2', replica_state=constants.STATUS_ACTIVE)
snapshots = [fakes.fake_snapshot( snapshots = [fakes.fake_snapshot(
@ -1671,7 +1751,7 @@ class ShareManagerTestCase(test.TestCase):
self.mock_object(db, 'share_replicas_get_all_by_share', self.mock_object(db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=[replica, active_replica])) mock.Mock(return_value=[replica, active_replica]))
self.mock_object(db, 'share_server_get', self.mock_object(db, 'share_server_get',
mock.Mock(return_value='fake_share_server')) mock.Mock(return_value=fakes.fake_share_server_get()))
mock_db_update_calls = [] mock_db_update_calls = []
self.mock_object(self.share_manager.db, 'share_replica_get', self.mock_object(self.share_manager.db, 'share_replica_get',
mock.Mock(return_value=replica)) mock.Mock(return_value=replica))
@ -4099,8 +4179,15 @@ class ShareManagerTestCase(test.TestCase):
} }
) )
def test_create_share_group_from_sg_snapshot_with_share_update(self): @ddt.data(constants.STATUS_AVAILABLE,
constants.STATUS_CREATING_FROM_SNAPSHOT,
None)
def test_create_share_group_from_sg_snapshot_with_share_update_status(
self, share_status):
fake_share = {'id': 'fake_share_id'} fake_share = {'id': 'fake_share_id'}
# if share_status is not None:
# fake_share.update({'status': share_status})
fake_export_locations = ['my_export_location'] fake_export_locations = ['my_export_location']
fake_group = { fake_group = {
'id': 'fake_id', 'id': 'fake_id',
@ -4117,18 +4204,28 @@ class ShareManagerTestCase(test.TestCase):
self.mock_object(self.share_manager.db, 'share_instance_update') self.mock_object(self.share_manager.db, 'share_instance_update')
self.mock_object(self.share_manager.db, self.mock_object(self.share_manager.db,
'share_export_locations_update') 'share_export_locations_update')
fake_share_update_list = [{'id': fake_share['id'],
'foo': 'bar', fake_share_update = {'id': fake_share['id'],
'export_locations': fake_export_locations}] 'foo': 'bar',
'export_locations': fake_export_locations}
if share_status is not None:
fake_share_update.update({'status': share_status})
self.mock_object(self.share_manager.driver, self.mock_object(self.share_manager.driver,
'create_share_group_from_share_group_snapshot', 'create_share_group_from_share_group_snapshot',
mock.Mock( mock.Mock(return_value=(None, [fake_share_update])))
return_value=(None, fake_share_update_list)))
self.share_manager.create_share_group(self.context, "fake_id") self.share_manager.create_share_group(self.context, "fake_id")
exp_progress = (
'0%' if share_status == constants.STATUS_CREATING_FROM_SNAPSHOT
else '100%')
self.share_manager.db.share_instance_update.assert_any_call( self.share_manager.db.share_instance_update.assert_any_call(
mock.ANY, 'fake_share_id', {'foo': 'bar'}) mock.ANY,
'fake_share_id',
{'foo': 'bar',
'status': share_status or constants.STATUS_AVAILABLE,
'progress': exp_progress})
self.share_manager.db.share_export_locations_update.assert_any_call( self.share_manager.db.share_export_locations_update.assert_any_call(
mock.ANY, 'fake_share_id', fake_export_locations) mock.ANY, 'fake_share_id', fake_export_locations)
self.share_manager.db.share_group_update.assert_any_call( self.share_manager.db.share_group_update.assert_any_call(
@ -4173,6 +4270,47 @@ class ShareManagerTestCase(test.TestCase):
} }
) )
def test_create_share_group_from_sg_snapshot_with_invalid_status(self):
fake_share = {'id': 'fake_share_id',
'status': constants.STATUS_CREATING}
fake_export_locations = ['my_export_location']
fake_group = {
'id': 'fake_id',
'source_share_group_snapshot_id': 'fake_snap_id',
'shares': [fake_share],
'availability_zone_id': 'fake_az',
}
fake_snap = {'id': 'fake_snap_id', 'share_group_snapshot_members': []}
self.mock_object(self.share_manager.db, 'share_group_get',
mock.Mock(return_value=fake_group))
self.mock_object(self.share_manager.db, 'share_group_snapshot_get',
mock.Mock(return_value=fake_snap))
self.mock_object(self.share_manager.db,
'share_instances_get_all_by_share_group_id',
mock.Mock(return_value=[]))
self.mock_object(self.share_manager.db, 'share_group_update',
mock.Mock(return_value=fake_group))
fake_share_update_list = [{'id': fake_share['id'],
'status': fake_share['status'],
'foo': 'bar',
'export_locations': fake_export_locations}]
self.mock_object(self.share_manager.driver,
'create_share_group_from_share_group_snapshot',
mock.Mock(
return_value=(None, fake_share_update_list)))
self.assertRaises(exception.InvalidShareInstance,
self.share_manager.create_share_group,
self.context, "fake_id")
self.share_manager.db.share_group_update.assert_called_once_with(
mock.ANY, 'fake_id', {
'status': constants.STATUS_ERROR,
'consistent_snapshot_support': None,
'availability_zone_id': fake_group['availability_zone_id'],
}
)
def test_create_share_group_from_sg_snapshot_with_share_error(self): def test_create_share_group_from_sg_snapshot_with_share_error(self):
fake_share = {'id': 'fake_share_id'} fake_share = {'id': 'fake_share_id'}
fake_group = { fake_group = {
@ -5291,7 +5429,8 @@ class ShareManagerTestCase(test.TestCase):
(self.share_manager.db.share_instance_update. (self.share_manager.db.share_instance_update.
assert_called_once_with( assert_called_once_with(
self.context, instance['id'], self.context, instance['id'],
{'status': constants.STATUS_AVAILABLE})) {'status': constants.STATUS_AVAILABLE,
'progress': '100%'}))
self.share_manager.db.share_update.assert_called_once_with( self.share_manager.db.share_update.assert_called_once_with(
self.context, share['id'], self.context, share['id'],
{'task_state': constants.TASK_STATE_MIGRATION_CANCELLED}) {'task_state': constants.TASK_STATE_MIGRATION_CANCELLED})
@ -5390,7 +5529,8 @@ class ShareManagerTestCase(test.TestCase):
self.context, src_instance['id']) self.context, src_instance['id'])
self.share_manager.db.share_instance_update.assert_has_calls([ self.share_manager.db.share_instance_update.assert_has_calls([
mock.call(self.context, dest_instance['id'], mock.call(self.context, dest_instance['id'],
{'status': constants.STATUS_AVAILABLE}), {'status': constants.STATUS_AVAILABLE,
'progress': '100%'}),
mock.call(self.context, src_instance['id'], mock.call(self.context, src_instance['id'],
{'status': constants.STATUS_INACTIVE})]) {'status': constants.STATUS_INACTIVE})])
self.share_manager.db.share_update.assert_called_once_with( self.share_manager.db.share_update.assert_called_once_with(
@ -5452,7 +5592,8 @@ class ShareManagerTestCase(test.TestCase):
self.share_manager.db.share_instance_update.assert_has_calls([ self.share_manager.db.share_instance_update.assert_has_calls([
mock.call(self.context, new_instance['id'], mock.call(self.context, new_instance['id'],
{'status': constants.STATUS_AVAILABLE}), {'status': constants.STATUS_AVAILABLE,
'progress': '100%'}),
mock.call(self.context, instance['id'], mock.call(self.context, instance['id'],
{'status': constants.STATUS_INACTIVE}) {'status': constants.STATUS_INACTIVE})
]) ])
@ -7348,6 +7489,92 @@ class ShareManagerTestCase(test.TestCase):
self.share_manager.update_share_usage_size(self.context) self.share_manager.update_share_usage_size(self.context)
self.assertTrue(mock_log_exception.called) self.assertTrue(mock_log_exception.called)
def test_periodic_share_status_update(self):
instances = self._setup_init_mocks(setup_access_rules=False)
instances_creating_from_snap = [
x for x in instances
if x['status'] == constants.STATUS_CREATING_FROM_SNAPSHOT
]
self.mock_object(self.share_manager, 'driver')
self.mock_object(self.share_manager.db,
'share_instances_get_all_by_host',
mock.Mock(return_value=instances_creating_from_snap))
mock_update_share_status = self.mock_object(
self.share_manager, '_update_share_status')
instances_dict = [
self.share_manager._get_share_instance_dict(self.context, si)
for si in instances_creating_from_snap]
self.share_manager.periodic_share_status_update(self.context)
mock_update_share_status.assert_has_calls([
mock.call(self.context, share_instance)
for share_instance in instances_dict
])
def test__update_share_status(self):
instances = self._setup_init_mocks(setup_access_rules=False)
fake_export_locations = ['fake/path/1', 'fake/path']
instance_model_update = {
'status': constants.STATUS_AVAILABLE,
'export_locations': fake_export_locations
}
expected_si_update_info = {
'status': constants.STATUS_AVAILABLE,
'progress': '100%'
}
driver_get_status = self.mock_object(
self.share_manager.driver, 'get_share_status',
mock.Mock(return_value=instance_model_update))
db_si_update = self.mock_object(self.share_manager.db,
'share_instance_update')
db_el_update = self.mock_object(self.share_manager.db,
'share_export_locations_update')
in_progress_instances = [x for x in instances
if x['status'] ==
constants.STATUS_CREATING_FROM_SNAPSHOT]
instance = self.share_manager.db.share_instance_get(
self.context, in_progress_instances[0]['id'], with_share_data=True)
self.share_manager._update_share_status(self.context, instance)
driver_get_status.assert_called_once_with(instance, None)
db_si_update.assert_called_once_with(self.context, instance['id'],
expected_si_update_info)
db_el_update.assert_called_once_with(self.context, instance['id'],
fake_export_locations)
@ddt.data(mock.Mock(return_value={'status': constants.STATUS_ERROR}),
mock.Mock(side_effect=exception.ShareBackendException))
def test__update_share_status_share_with_error_or_exception(self,
driver_error):
instances = self._setup_init_mocks(setup_access_rules=False)
expected_si_update_info = {
'status': constants.STATUS_ERROR,
'progress': None,
}
driver_get_status = self.mock_object(
self.share_manager.driver, 'get_share_status', driver_error)
db_si_update = self.mock_object(self.share_manager.db,
'share_instance_update')
in_progress_instances = [x for x in instances
if x['status'] ==
constants.STATUS_CREATING_FROM_SNAPSHOT]
instance = self.share_manager.db.share_instance_get(
self.context, in_progress_instances[0]['id'], with_share_data=True)
self.share_manager._update_share_status(self.context, instance)
driver_get_status.assert_called_once_with(instance, None)
db_si_update.assert_called_once_with(self.context, instance['id'],
expected_si_update_info)
self.share_manager.message_api.create.assert_called_once_with(
self.context,
message_field.Action.UPDATE,
instance['project_id'],
resource_type=message_field.Resource.SHARE,
resource_id=instance['share_id'],
detail=message_field.Detail.DRIVER_FAILED_CREATING_FROM_SNAP)
@ddt.ddt @ddt.ddt
class HookWrapperTestCase(test.TestCase): class HookWrapperTestCase(test.TestCase):

View File

@ -0,0 +1,14 @@
---
features:
- The scheduler was improved to select and weigh compatible back ends when
creating shares from snapshots. This change only affects the existing
behavior if the option ``use_scheduler_creating_share_from_snapshot`` is
enabled.
- A new share status `creating_from_snapshot` was added to inform the user
that a share creation from snapshot is in progress and may take some time
to be concluded. In order to quantify the share creation progress a new
field called ``progress`` was added to shares and share instances information,
to indicate the conclusion percentage of share create operation (0 to 100%).
fixes:
- The availability zone parameter is now being considered when creating
shares from snapshots.

View File

@ -51,6 +51,7 @@ manila.scheduler.filters =
JsonFilter = manila.scheduler.filters.json:JsonFilter JsonFilter = manila.scheduler.filters.json:JsonFilter
RetryFilter = manila.scheduler.filters.retry:RetryFilter RetryFilter = manila.scheduler.filters.retry:RetryFilter
ShareReplicationFilter = manila.scheduler.filters.share_replication:ShareReplicationFilter ShareReplicationFilter = manila.scheduler.filters.share_replication:ShareReplicationFilter
CreateFromSnapshotFilter = manila.scheduler.filters.create_from_snapshot:CreateFromSnapshotFilter
# Share Group filters # Share Group filters
ConsistentSnapshotFilter = manila.scheduler.filters.share_group_filters.consistent_snapshot:ConsistentSnapshotFilter ConsistentSnapshotFilter = manila.scheduler.filters.share_group_filters.consistent_snapshot:ConsistentSnapshotFilter
@ -58,6 +59,7 @@ manila.scheduler.weighers =
CapacityWeigher = manila.scheduler.weighers.capacity:CapacityWeigher CapacityWeigher = manila.scheduler.weighers.capacity:CapacityWeigher
GoodnessWeigher = manila.scheduler.weighers.goodness:GoodnessWeigher GoodnessWeigher = manila.scheduler.weighers.goodness:GoodnessWeigher
PoolWeigher = manila.scheduler.weighers.pool:PoolWeigher PoolWeigher = manila.scheduler.weighers.pool:PoolWeigher
HostAffinityWeigher = manila.scheduler.weighers.host_affinity:HostAffinityWeigher
# These are for backwards compat with Havana notification_driver configuration values # These are for backwards compat with Havana notification_driver configuration values
oslo_messaging.notify.drivers = oslo_messaging.notify.drivers =