diff --git a/manila/cmd/manage.py b/manila/cmd/manage.py index 61e8100fa1..cdfc2594d1 100644 --- a/manila/cmd/manage.py +++ b/manila/cmd/manage.py @@ -75,6 +75,16 @@ from manila import version CONF = cfg.CONF +HOST_UPDATE_HELP_MSG = ("A fully qualified host string is of the format " + "'HostA@BackendB#PoolC'. Provide only the host name " + "(ex: 'HostA') to update the hostname part of " + "the host string. Provide only the " + "host name and backend name (ex: 'HostA@BackendB') to " + "update the host and backend names.") +HOST_UPDATE_CURRENT_HOST_HELP = ("Current share host name. %s" % + HOST_UPDATE_HELP_MSG) +HOST_UPDATE_NEW_HOST_HELP = "New share host name. %s" % HOST_UPDATE_HELP_MSG + # Decorators for actions def args(*args, **kwargs): @@ -355,12 +365,47 @@ class ServiceCommands(object): )) +class ShareCommands(object): + + @staticmethod + def _validate_hosts(current_host, new_host): + err = None + if '@' in current_host: + if '#' in current_host and '#' not in new_host: + err = "%(chost)s specifies a pool but %(nhost)s does not." + elif '@' not in new_host: + err = "%(chost)s specifies a backend but %(nhost)s does not." + if err: + print(err % {'chost': current_host, 'nhost': new_host}) + sys.exit(1) + + @args('--currenthost', required=True, help=HOST_UPDATE_CURRENT_HOST_HELP) + @args('--newhost', required=True, help=HOST_UPDATE_NEW_HOST_HELP) + @args('--force', required=False, type=bool, default=False, + help="Ignore validations.") + def update_host(self, current_host, new_host, force=False): + """Modify the host name associated with a share. + + Particularly to recover from cases where one has moved + their Manila Share node, or modified their 'host' opt + or their backend section name in the manila configuration file. + """ + if not force: + self._validate_hosts(current_host, new_host) + ctxt = context.get_admin_context() + updated = db.share_instances_host_update(ctxt, current_host, new_host) + print("Updated host of %(count)s share instances on %(chost)s " + "to %(nhost)s." % {'count': updated, 'chost': current_host, + 'nhost': new_host}) + + CATEGORIES = { 'config': ConfigCommands, 'db': DbCommands, 'host': HostCommands, 'logs': GetLogCommands, 'service': ServiceCommands, + 'share': ShareCommands, 'shell': ShellCommands, 'version': VersionCommands } diff --git a/manila/db/api.py b/manila/db/api.py index cbc63fdcc0..20bdfca910 100644 --- a/manila/db/api.py +++ b/manila/db/api.py @@ -325,6 +325,11 @@ def share_instance_update(context, instance_id, values, with_share_data=False): with_share_data=with_share_data) +def share_instances_host_update(context, current_host, new_host): + """Update the host attr of all share instances that are on current_host.""" + return IMPL.share_instances_host_update(context, current_host, new_host) + + def share_instances_get_all(context, filters=None): """Returns all share instances.""" return IMPL.share_instances_get_all(context, filters=filters) diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py index 8d1aaf924f..d73289b66e 100644 --- a/manila/db/sqlalchemy/api.py +++ b/manila/db/sqlalchemy/api.py @@ -1339,6 +1339,20 @@ def _share_instance_create(context, share_id, values, session): session=session) +@require_admin_context +def share_instances_host_update(context, current_host, new_host): + session = get_session() + host_field = models.ShareInstance.host + with session.begin(): + query = model_query( + context, models.ShareInstance, session=session, read_deleted="no", + ).filter(host_field.like('{}%'.format(current_host))) + result = query.update( + {host_field: func.replace(host_field, current_host, new_host)}, + synchronize_session=False) + return result + + @require_context def share_instance_update(context, share_instance_id, values, with_share_data=False): diff --git a/manila/tests/cmd/test_manage.py b/manila/tests/cmd/test_manage.py index 0e1d012bf0..f50769fd33 100644 --- a/manila/tests/cmd/test_manage.py +++ b/manila/tests/cmd/test_manage.py @@ -45,6 +45,7 @@ class ManilaCmdManageTestCase(test.TestCase): self.config_commands = manila_manage.ConfigCommands() self.get_log_cmds = manila_manage.GetLogCommands() self.service_cmds = manila_manage.ServiceCommands() + self.share_cmds = manila_manage.ShareCommands() def test_param2id_is_uuid_like(self): obj_id = '12345678123456781234567812345678' @@ -376,3 +377,44 @@ class ManilaCmdManageTestCase(test.TestCase): def test_get_arg_string(self, arg): parsed_arg = manila_manage.get_arg_string(arg) self.assertEqual('bar', parsed_arg) + + @ddt.data({'current_host': 'controller-0@fancystore01#pool100', + 'new_host': 'controller-0@fancystore01'}, + {'current_host': 'controller-0@fancystore01', + 'new_host': 'controller-0'}) + @ddt.unpack + def test_share_update_host_fail_validation(self, current_host, new_host): + self.mock_object(context, 'get_admin_context', + mock.Mock(return_value='admin_ctxt')) + self.mock_object(db, 'share_instances_host_update') + + self.assertRaises(SystemExit, + self.share_cmds.update_host, + current_host, new_host) + + self.assertFalse(db.share_instances_host_update.called) + + @ddt.data({'current_host': 'controller-0@fancystore01#pool100', + 'new_host': 'controller-0@fancystore02#pool0'}, + {'current_host': 'controller-0@fancystore01', + 'new_host': 'controller-1@fancystore01'}, + {'current_host': 'controller-0', + 'new_host': 'controller-1'}, + {'current_host': 'controller-0@fancystore01#pool100', + 'new_host': 'controller-1@fancystore02', 'force': True}) + @ddt.unpack + def test_share_update_host(self, current_host, new_host, force=False): + self.mock_object(context, 'get_admin_context', + mock.Mock(return_value='admin_ctxt')) + self.mock_object(db, 'share_instances_host_update', + mock.Mock(return_value=20)) + + with mock.patch('sys.stdout', new=six.StringIO()) as intercepted_op: + self.share_cmds.update_host(current_host, new_host, force) + + expected_op = ("Updated host of 20 share instances on " + "%(chost)s to %(nhost)s." % + {'chost': current_host, 'nhost': new_host}) + self.assertEqual(expected_op, intercepted_op.getvalue().strip()) + db.share_instances_host_update.assert_called_once_with( + 'admin_ctxt', current_host, new_host) diff --git a/manila/tests/db/sqlalchemy/test_api.py b/manila/tests/db/sqlalchemy/test_api.py index 97ff7d9ac2..ba767e6fab 100644 --- a/manila/tests/db/sqlalchemy/test_api.py +++ b/manila/tests/db/sqlalchemy/test_api.py @@ -3133,3 +3133,91 @@ class BackendInfoDatabaseAPITestCase(test.TestCase): self.assertIsNone(initial_data) self.assertEqual(value_2, actual_data['info_hash']) self.assertEqual(host, actual_data['host']) + + +@ddt.ddt +class ShareInstancesTestCase(test.TestCase): + + def setUp(self): + super(ShareInstancesTestCase, self).setUp() + self.context = context.get_admin_context() + + @ddt.data('controller-100', 'controller-0@otherstore03', + 'controller-0@otherstore01#pool200') + def test_share_instances_host_update_no_matches(self, current_host): + share_id = uuidutils.generate_uuid() + if '@' in current_host: + if '#' in current_host: + new_host = 'new-controller-X@backendX#poolX' + else: + new_host = 'new-controller-X@backendX' + else: + new_host = 'new-controller-X' + instances = [ + db_utils.create_share_instance( + share_id=share_id, + host='controller-0@fancystore01#pool100', + status=constants.STATUS_AVAILABLE), + db_utils.create_share_instance( + share_id=share_id, + host='controller-0@otherstore02#pool100', + status=constants.STATUS_ERROR), + db_utils.create_share_instance( + share_id=share_id, + host='controller-2@beststore07#pool200', + status=constants.STATUS_DELETING), + ] + db_utils.create_share(id=share_id, instances=instances) + + updates = db_api.share_instances_host_update(self.context, + current_host, + new_host) + + share_instances = db_api.share_instances_get_all( + self.context, filters={'share_id': share_id}) + self.assertEqual(0, updates) + for share_instance in share_instances: + self.assertTrue(not share_instance['host'].startswith(new_host)) + + @ddt.data({'current_host': 'controller-2', 'expected_updates': 1}, + {'current_host': 'controller-0@fancystore01', + 'expected_updates': 2}, + {'current_host': 'controller-0@fancystore01#pool100', + 'expected_updates': 1}) + @ddt.unpack + def test_share_instance_host_update_partial_matches(self, current_host, + expected_updates): + share_id = uuidutils.generate_uuid() + if '@' in current_host: + if '#' in current_host: + new_host = 'new-controller-X@backendX#poolX' + else: + new_host = 'new-controller-X@backendX' + else: + new_host = 'new-controller-X' + instances = [ + db_utils.create_share_instance( + share_id=share_id, + host='controller-0@fancystore01#pool100', + status=constants.STATUS_AVAILABLE), + db_utils.create_share_instance( + share_id=share_id, + host='controller-0@fancystore01#pool200', + status=constants.STATUS_ERROR), + db_utils.create_share_instance( + share_id=share_id, + host='controller-2@beststore07#pool200', + status=constants.STATUS_DELETING), + ] + db_utils.create_share(id=share_id, instances=instances) + + actual_updates = db_api.share_instances_host_update( + self.context, current_host, new_host) + + share_instances = db_api.share_instances_get_all( + self.context, filters={'share_id': share_id}) + + host_updates = [si for si in share_instances if + si['host'].startswith(new_host)] + self.assertEqual(actual_updates, expected_updates) + self.assertEqual(expected_updates, len(host_updates)) diff --git a/releasenotes/notes/add-update-host-command-to-manila-manage-b32ad5017b564c9e.yaml b/releasenotes/notes/add-update-host-command-to-manila-manage-b32ad5017b564c9e.yaml new file mode 100644 index 0000000000..c3bb6c280c --- /dev/null +++ b/releasenotes/notes/add-update-host-command-to-manila-manage-b32ad5017b564c9e.yaml @@ -0,0 +1,7 @@ +--- +features: + - The ``manila-manage`` utility now has a new command to update the host + attribute of shares. This is useful when the share manager process + has been migrated to a different host, or if changes are made to the + ``host`` config option or the backend section name in ``manila.conf``. + Execute ``manila-manage share update_host -h`` to see usage instructions. \ No newline at end of file