deb-manila/manila/share/migration.py
Rodrigo Barbieri 0524ab8fc2 Add Share Migration feature
Share Migration allows a share to be migrated from
one host#pool to another host#pool through the
"manila migrate <share> <host#pool>" command. It first
calls the driver to perform it in an optimized way if
possible. If the driver returns that it did not migrate,
it performs a generic migration.

A new field has been added to "shares" table: task_state,
which tracks migration status.

For driver migration, the method migrate_share in driver
base class should be overridden.

For generic migration, drivers may use new config options
to achieve the necessary configuration:
- migration_mounting_backend_ip: If backend has additional
exports IP for admin network, specify it here.
- migration_data_copy_node_ip: IP of entity performing
migration between backends, such as manila node or
data copy service node. This may not apply for
DHSS = true drivers.
- migration_protocol_mount_command: specify mount command
with protocol and additional parameters. Advisable to restrict
protocols per backend. Defaults to "mount -t <share_proto>".

If additional customization is needed, drivers may override
certain methods:
- _mount_share: return the mount command.
- _umount_share: return the umount command.
- _get_access_rule_for_data_copy: return an access rule with
the IP address which will allow the manila node or data copy node
to mount the share after added permission through
allow-access API command.

Change-Id: I8dde892cb7c0180b2b56d8c7d680dfe2320c2ec7
Implements: blueprint share-migration
2015-09-06 22:52:41 -03:00

349 lines
14 KiB
Python

# Copyright (c) 2015 Hitachi Data Systems.
# 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.
"""Helper class for Share Migration."""
import time
from oslo_log import log
import six
from manila.common import constants
from manila import exception
from manila.i18n import _
from manila.i18n import _LE
from manila.i18n import _LW
from manila.share import api as share_api
from manila import utils
LOG = log.getLogger(__name__)
class ShareMigrationHelper(object):
def __init__(self, context, db, create_delete_timeout, access_rule_timeout,
share):
self.db = db
self.share = share
self.context = context
self.api = share_api.API()
self.migration_create_delete_share_timeout = create_delete_timeout
self.migration_wait_access_rules_timeout = access_rule_timeout
def delete_instance_and_wait(self, context, share_instance):
self.api.delete_instance(context, share_instance, True)
# Wait for deletion.
starttime = time.time()
deadline = starttime + self.migration_create_delete_share_timeout
tries = -1
instance = "Something not None"
while instance:
try:
instance = self.db.share_instance_get(context,
share_instance['id'])
tries += 1
now = time.time()
if now > deadline:
msg = _("Timeout trying to delete instance "
"%s") % share_instance['id']
raise exception.ShareMigrationFailed(reason=msg)
except exception.NotFound:
instance = None
else:
time.sleep(tries ** 2)
def create_instance_and_wait(self, context, share, share_instance, host):
api = share_api.API()
new_share_instance = api.create_instance(
context, share, share_instance['share_network_id'], host['host'])
# Wait for new_share_instance to become ready
starttime = time.time()
deadline = starttime + self.migration_create_delete_share_timeout
new_share_instance = self.db.share_instance_get(
context, new_share_instance['id'], with_share_data=True)
tries = 0
while new_share_instance['status'] != constants.STATUS_AVAILABLE:
tries += 1
now = time.time()
if new_share_instance['status'] == constants.STATUS_ERROR:
msg = _("Failed to create new share instance"
" (from %(share_id)s) on "
"destination host %(host_name)s") % {
'share_id': share['id'], 'host_name': host['host']}
raise exception.ShareMigrationFailed(reason=msg)
elif now > deadline:
msg = _("Timeout creating new share instance "
"(from %(share_id)s) on "
"destination host %(host_name)s") % {
'share_id': share['id'], 'host_name': host['host']}
raise exception.ShareMigrationFailed(reason=msg)
else:
time.sleep(tries ** 2)
new_share_instance = self.db.share_instance_get(
context, new_share_instance['id'], with_share_data=True)
return new_share_instance
def deny_rules_and_wait(self, context, share, saved_rules):
api = share_api.API()
# Deny each one.
for instance in share.instances:
for access in saved_rules:
api.deny_access_to_instance(context, instance, access)
# Wait for all rules to be cleared.
starttime = time.time()
deadline = starttime + self.migration_wait_access_rules_timeout
tries = 0
rules = self.db.share_access_get_all_for_share(context, share['id'])
while len(rules) > 0:
tries += 1
now = time.time()
if now > deadline:
msg = _("Timeout removing access rules from share "
"%(share_id)s. Timeout was %(timeout)s seconds.") % {
'share_id': share['id'],
'timeout': self.migration_wait_access_rules_timeout}
raise exception.ShareMigrationFailed(reason=msg)
else:
time.sleep(tries ** 2)
rules = self.db.share_access_get_all_for_share(
context, share['id'])
def add_rules_and_wait(self, context, share, saved_rules,
access_level=None):
for access in saved_rules:
if access_level:
level = access_level
else:
level = access['access_level']
self.api.allow_access(context, share, access['access_type'],
access['access_to'], level)
starttime = time.time()
deadline = starttime + self.migration_wait_access_rules_timeout
rules_added = False
tries = 0
rules = self.db.share_access_get_all_for_share(context, share['id'])
while not rules_added:
rules_added = True
tries += 1
now = time.time()
for access in rules:
if access['state'] != constants.STATUS_ACTIVE:
rules_added = False
if rules_added:
break
if now > deadline:
msg = _("Timeout adding access rules for share "
"%(share_id)s. Timeout was %(timeout)s seconds.") % {
'share_id': share['id'],
'timeout': self.migration_wait_access_rules_timeout}
raise exception.ShareMigrationFailed(reason=msg)
else:
time.sleep(tries ** 2)
rules = self.db.share_access_get_all_for_share(
context, share['id'])
def wait_for_allow_access(self, access_ref):
starttime = time.time()
deadline = starttime + self.migration_wait_access_rules_timeout
tries = 0
rule = self.api.access_get(self.context, access_ref['id'])
while rule['state'] != constants.STATUS_ACTIVE:
tries += 1
now = time.time()
if rule['state'] == constants.STATUS_ERROR:
msg = _("Failed to allow access"
" on share %s") % self.share['id']
raise exception.ShareMigrationFailed(reason=msg)
elif now > deadline:
msg = _("Timeout trying to allow access"
" on share %(share_id)s. Timeout "
"was %(timeout)s seconds.") % {
'share_id': self.share['id'],
'timeout': self.migration_wait_access_rules_timeout}
raise exception.ShareMigrationFailed(reason=msg)
else:
time.sleep(tries ** 2)
rule = self.api.access_get(self.context, access_ref['id'])
return rule
def wait_for_deny_access(self, access_ref):
starttime = time.time()
deadline = starttime + self.migration_wait_access_rules_timeout
tries = -1
rule = "Something not None"
while rule:
try:
rule = self.api.access_get(self.context, access_ref['id'])
tries += 1
now = time.time()
if now > deadline:
msg = _("Timeout trying to deny access"
" on share %(share_id)s. Timeout "
"was %(timeout)s seconds.") % {
'share_id': self.share['id'],
'timeout': self.migration_wait_access_rules_timeout}
raise exception.ShareMigrationFailed(reason=msg)
except exception.NotFound:
rule = None
else:
time.sleep(tries ** 2)
def allow_migration_access(self, access):
allowed = False
access_ref = None
try:
access_ref = self.api.allow_access(
self.context, self.share,
access['access_type'], access['access_to'])
allowed = True
except exception.ShareAccessExists:
LOG.warning(_LW("Access rule already allowed. "
"Access %(access_to)s - Share "
"%(share_id)s") % {
'access_to': access['access_to'],
'share_id': self.share['id']})
access_list = self.api.access_get_all(self.context, self.share)
for access_item in access_list:
if access_item['access_to'] == access['access_to']:
access_ref = access_item
if access_ref and allowed:
access_ref = self.wait_for_allow_access(access_ref)
return access_ref
def deny_migration_access(self, access_ref, access, throw_not_found=True):
denied = False
if access_ref:
try:
# Update status
access_ref = self.api.access_get(
self.context, access_ref['id'])
except exception.NotFound:
access_ref = None
LOG.warning(_LW("Access rule not found. "
"Access %(access_to)s - Share "
"%(share_id)s") % {
'access_to': access['access_to'],
'share_id': self.share['id']})
else:
access_list = self.api.access_get_all(self.context, self.share)
for access_item in access_list:
if access_item['access_to'] == access['access_to']:
access_ref = access_item
break
if access_ref:
try:
self.api.deny_access(self.context, self.share, access_ref)
denied = True
except (exception.InvalidShareAccess, exception.NotFound) as e:
LOG.exception(six.text_type(e))
LOG.warning(_LW("Access rule not found. "
"Access %(access_to)s - Share "
"%(share_id)s") % {
'access_to': access['access_to'],
'share_id': self.share['id']})
if throw_not_found:
raise
if denied:
self.wait_for_deny_access(access_ref)
# NOTE(ganso): Cleanup methods do not throw exception, since the
# exceptions that should be thrown are the ones that call the cleanup
def cleanup_migration_access(self, access_ref, access):
try:
self.deny_migration_access(access_ref, access)
except Exception as mae:
LOG.exception(six.text_type(mae))
LOG.error(_LE("Could not cleanup access rule of share "
"%s") % self.share['id'])
def cleanup_temp_folder(self, instance, mount_path):
try:
utils.execute('rmdir', mount_path + instance['id'],
check_exit_code=False)
except Exception as tfe:
LOG.exception(six.text_type(tfe))
LOG.error(_LE("Could not cleanup instance %(instance_id)s "
"temporary folders for migration of "
"share %(share_id)s") % {
'instance_id': instance['id'],
'share_id': self.share['id']})
def cleanup_unmount_temp_folder(self, instance, migration_info):
try:
utils.execute(*migration_info['umount'], run_as_root=True)
except Exception as utfe:
LOG.exception(six.text_type(utfe))
LOG.error(_LE("Could not unmount folder of instance"
" %(instance_id)s for migration of "
"share %(share_id)s") % {
'instance_id': instance['id'],
'share_id': self.share['id']})
def change_to_read_only(self, readonly_support):
# NOTE(ganso): If the share does not allow readonly mode we
# should remove all access rules and prevent any access
saved_rules = self.db.share_access_get_all_for_share(
self.context, self.share['id'])
self.deny_rules_and_wait(self.context, self.share, saved_rules)
if readonly_support:
LOG.debug("Changing all of share %s access rules "
"to read-only." % self.share['id'])
self.add_rules_and_wait(self.context, self.share,
saved_rules, 'ro')
return saved_rules
def revert_access_rules(self, readonly_support, saved_rules):
if readonly_support:
readonly_rules = self.db.share_access_get_all_for_share(
self.context, self.share['id'])
LOG.debug("Removing all of share %s read-only "
"access rules." % self.share['id'])
self.deny_rules_and_wait(self.context, self.share, readonly_rules)
self.add_rules_and_wait(self.context, self.share, saved_rules)