From a57bf8816bd08c836cf2134c1da78ec940bd4d78 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Fri, 3 Nov 2017 09:58:27 +0800 Subject: [PATCH] Implementation of root-enable, root-disable in redis. Implement root-enable, root-disable for redis to manage redis authentication. Change-Id: If88092c24c51192a19eeec8312701e2c6d709db9 Implements: blueprint root-enable-in-redis Signed-off-by: Fan Zhang --- ...implement-redis-root-347b5ee0107debb5.yaml | 4 + trove/common/cfg.py | 2 +- trove/common/db/redis/__init__.py | 0 trove/common/db/redis/models.py | 28 +++ trove/common/exception.py | 6 + trove/extensions/redis/__init__.py | 0 trove/extensions/redis/models.py | 28 +++ trove/extensions/redis/service.py | 185 ++++++++++++++++ trove/extensions/redis/views.py | 30 +++ trove/guestagent/api.py | 9 + .../datastore/experimental/redis/manager.py | 16 ++ .../datastore/experimental/redis/service.py | 27 +++ trove/guestagent/datastore/manager.py | 5 + trove/tests/int_tests.py | 3 +- .../guestagent/test_redis_manager.py | 24 ++ trove/tests/unittests/redis/__init__.py | 0 .../redis/test_redis_root_controller.py | 205 ++++++++++++++++++ 17 files changed, 570 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/implement-redis-root-347b5ee0107debb5.yaml create mode 100644 trove/common/db/redis/__init__.py create mode 100644 trove/common/db/redis/models.py create mode 100644 trove/extensions/redis/__init__.py create mode 100644 trove/extensions/redis/models.py create mode 100644 trove/extensions/redis/service.py create mode 100644 trove/extensions/redis/views.py create mode 100644 trove/tests/unittests/redis/__init__.py create mode 100644 trove/tests/unittests/redis/test_redis_root_controller.py diff --git a/releasenotes/notes/implement-redis-root-347b5ee0107debb5.yaml b/releasenotes/notes/implement-redis-root-347b5ee0107debb5.yaml new file mode 100644 index 0000000000..f6de2560c2 --- /dev/null +++ b/releasenotes/notes/implement-redis-root-347b5ee0107debb5.yaml @@ -0,0 +1,4 @@ +--- +features: + - OpenStack Trove now supports enable or disable authentication for Redis + datastore via the root-enable and root-disable API's. diff --git a/trove/common/cfg.py b/trove/common/cfg.py index 45f12f463a..d5385a3a93 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -854,7 +854,7 @@ redis_opts = [ help='Class that implements datastore-specific Guest Agent API ' 'logic.'), cfg.StrOpt('root_controller', - default='trove.extensions.common.service.DefaultRootController', + default='trove.extensions.redis.service.RedisRootController', help='Root controller implementation for redis.'), cfg.StrOpt('guest_log_exposed_logs', default='', help='List of Guest Logs to expose for publishing.'), diff --git a/trove/common/db/redis/__init__.py b/trove/common/db/redis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/common/db/redis/models.py b/trove/common/db/redis/models.py new file mode 100644 index 0000000000..76cadd3c97 --- /dev/null +++ b/trove/common/db/redis/models.py @@ -0,0 +1,28 @@ +# Copyright 2017 Eayun, 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 trove.common.db import models + + +class RedisRootUser(models.DatastoreModelsBase): + + def verify_dict(self): + pass + + def __init__(self, password=None): + self._name = '-' + self._password = password + super(RedisRootUser, self).__init__() diff --git a/trove/common/exception.py b/trove/common/exception.py index a5074683ec..1448f52f58 100644 --- a/trove/common/exception.py +++ b/trove/common/exception.py @@ -671,3 +671,9 @@ class DatastoreVersionAlreadyExists(BadRequest): class LogAccessForbidden(Forbidden): message = _("You must be admin to %(action)s log '%(log)s'.") + + +class SlaveOperationNotSupported(TroveError): + + message = _("The '%(operation)s' operation is not supported for slaves in " + "replication.") diff --git a/trove/extensions/redis/__init__.py b/trove/extensions/redis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/extensions/redis/models.py b/trove/extensions/redis/models.py new file mode 100644 index 0000000000..62a35e2660 --- /dev/null +++ b/trove/extensions/redis/models.py @@ -0,0 +1,28 @@ +# Copyright 2017 Eayun, 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 trove.common.remote import create_guest_client +from trove.extensions.common.models import load_and_verify +from trove.extensions.common.models import Root + + +class RedisRoot(Root): + @classmethod + def get_auth_password(cls, context, instance_id): + load_and_verify(context, instance_id) + password = create_guest_client(context, + instance_id).get_root_password() + return password diff --git a/trove/extensions/redis/service.py b/trove/extensions/redis/service.py new file mode 100644 index 0000000000..6788a3de9d --- /dev/null +++ b/trove/extensions/redis/service.py @@ -0,0 +1,185 @@ +# Copyright 2017 Eayun, 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 as logging +from trove.common import cfg +from trove.common import exception +from trove.common.i18n import _ +from trove.common.i18n import _LE +from trove.common.i18n import _LI +from trove.common import wsgi +from trove.extensions.common.service import DefaultRootController +from trove.extensions.redis.models import RedisRoot +from trove.extensions.redis.views import RedisRootCreatedView +from trove.instance.models import DBInstance + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF +MANAGER = CONF.datastore_manager if CONF.datastore_manager else 'redis' + + +class RedisRootController(DefaultRootController): + def root_create(self, req, body, tenant_id, instance_id, is_cluster): + """Enable authentication for a redis instance and its replicas if any + """ + self._validate_can_perform_action(tenant_id, instance_id, is_cluster, + "enable_root") + password = DefaultRootController._get_password_from_body(body) + slave_instances = self._get_slaves(tenant_id, instance_id) + return self._instance_root_create(req, instance_id, password, + slave_instances) + + def root_delete(self, req, tenant_id, instance_id, is_cluster): + """Disable authentication for a redis instance and its replicas if any + """ + self._validate_can_perform_action(tenant_id, instance_id, is_cluster, + "disable_root") + slave_instances = self._get_slaves(tenant_id, instance_id) + return self._instance_root_delete(req, instance_id, slave_instances) + + def _instance_root_create(self, req, instance_id, password, + slave_instances=None): + LOG.info(_LI("Enabling authentication for instance '%s'."), + instance_id) + LOG.info(_LI("req : '%s'\n\n"), req) + context = req.environ[wsgi.CONTEXT_KEY] + user_name = context.user + + original_auth_password = self._get_original_auth_password( + context, instance_id) + + # Do root-enable and roll back once if operation fails. + try: + root = RedisRoot.create(context, instance_id, + user_name, password) + if not password: + password = root.password + except exception.TroveError: + self._rollback_once(req, instance_id, original_auth_password) + raise exception.TroveError( + _LE("Failed to do root-enable for instance " + "'%(instance_id)s'.") % {'instance_id': instance_id} + ) + + failed_slaves = [] + for slave_id in slave_instances: + try: + LOG.info(_LI("Enabling authentication for slave instance " + "'%s'."), slave_id) + RedisRoot.create(context, slave_id, user_name, password) + except exception.TroveError: + failed_slaves.append(slave_id) + + return wsgi.Result( + RedisRootCreatedView(root, failed_slaves).data(), 200) + + def _instance_root_delete(self, req, instance_id, slave_instances=None): + LOG.info(_LI("Disabling authentication for instance '%s'."), + instance_id) + LOG.info(_LI("req : '%s'\n\n"), req) + context = req.environ[wsgi.CONTEXT_KEY] + + original_auth_password = self._get_original_auth_password( + context, instance_id) + + # Do root-disable and roll back once if operation fails. + try: + RedisRoot.delete(context, instance_id) + except exception.TroveError: + self._rollback_once(req, instance_id, original_auth_password) + raise exception.TroveError( + _LE("Failed to do root-disable for instance " + "'%(instance_id)s'.") % {'instance_id': instance_id} + ) + + failed_slaves = [] + for slave_id in slave_instances: + try: + LOG.info(_LI("Disabling authentication for slave instance " + "'%s'."), slave_id) + RedisRoot.delete(context, slave_id) + except exception.TroveError: + failed_slaves.append(slave_id) + + if len(failed_slaves) > 0: + result = { + 'failed_slaves': failed_slaves + } + return wsgi.Result(result, 200) + + return wsgi.Result(None, 204) + + @staticmethod + def _rollback_once(req, instance_id, original_auth_password): + LOG.info(_LI("Rolling back enable/disable authentication " + "for instance '%s'."), instance_id) + context = req.environ[wsgi.CONTEXT_KEY] + user_name = context.user + try: + if not original_auth_password: + # Instance never did root-enable before. + RedisRoot.delete(context, instance_id) + else: + # Instance has done root-enable successfully before. + # So roll back with original password. + RedisRoot.create(context, instance_id, user_name, + original_auth_password) + except exception.TroveError: + LOG.exception(_("Rolling back failed for instance '%s'"), + instance_id) + + @staticmethod + def _is_slave(tenant_id, instance_id): + args = {'id': instance_id, 'tenant_id': tenant_id} + instance_info = DBInstance.find_by(**args) + return instance_info.slave_of_id + + @staticmethod + def _get_slaves(tenant_id, instance_or_cluster_id, deleted=False): + LOG.info(_LI("Getting non-deleted slaves of instance '%s', " + "if any."), instance_or_cluster_id) + args = {'slave_of_id': instance_or_cluster_id, 'tenant_id': tenant_id, + 'deleted': deleted} + db_infos = DBInstance.find_all(**args) + slaves = [] + for db_info in db_infos: + slaves.append(db_info.id) + return slaves + + @staticmethod + def _get_original_auth_password(context, instance_id): + # Check if instance did root-enable before and get original password. + password = None + if RedisRoot.load(context, instance_id): + try: + password = RedisRoot.get_auth_password(context, instance_id) + except exception.TroveError: + raise exception.TroveError( + _LE("Failed to get original auth password of instance " + "'%(instance_id)s'.") % {'instance_id': instance_id} + ) + return password + + def _validate_can_perform_action(self, tenant_id, instance_id, is_cluster, + operation): + if is_cluster: + raise exception.ClusterOperationNotSupported( + operation=operation) + + is_slave = self._is_slave(tenant_id, instance_id) + if is_slave: + raise exception.SlaveOperationNotSupported( + operation=operation) diff --git a/trove/extensions/redis/views.py b/trove/extensions/redis/views.py new file mode 100644 index 0000000000..9066ce6bc6 --- /dev/null +++ b/trove/extensions/redis/views.py @@ -0,0 +1,30 @@ +# Copyright 2017 Eayun, 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 trove.extensions.common.views import UserView + + +class RedisRootCreatedView(UserView): + def __init__(self, user, failed_slaves): + self.failed_slaves = failed_slaves + super(RedisRootCreatedView, self).__init__(user) + + def data(self): + user_dict = { + "name": self.user.name, + "password": self.user.password + } + return {"user": user_dict, "failed_slaves": self.failed_slaves} diff --git a/trove/guestagent/api.py b/trove/guestagent/api.py index c9942a7568..a654297da9 100644 --- a/trove/guestagent/api.py +++ b/trove/guestagent/api.py @@ -237,6 +237,15 @@ class API(object): self._cast("delete_database", version=version, database=database) + def get_root_password(self): + """Make a synchronous call to get root password of instance. + """ + LOG.debug("Get root password of instance %s.", self.id) + version = self.API_BASE_VERSION + + return self._call("get_root_password", self.agent_high_timeout, + version=version) + def enable_root(self): """Make a synchronous call to enable the root user for access from anywhere diff --git a/trove/guestagent/datastore/experimental/redis/manager.py b/trove/guestagent/datastore/experimental/redis/manager.py index 5b67783f0b..f3ecf84a77 100644 --- a/trove/guestagent/datastore/experimental/redis/manager.py +++ b/trove/guestagent/datastore/experimental/redis/manager.py @@ -267,3 +267,19 @@ class Manager(manager.Manager): LOG.debug("Executing cluster_addslots to assign hash slots %s-%s.", first_slot, last_slot) self._app.cluster_addslots(first_slot, last_slot) + + def enable_root(self, context): + LOG.debug("Enabling authentication.") + return self._app.enable_root() + + def enable_root_with_password(self, context, root_password=None): + LOG.debug("Enabling authentication with password.") + return self._app.enable_root(root_password) + + def disable_root(self, context): + LOG.debug("Disabling authentication.") + return self._app.disable_root() + + def get_root_password(self, context): + LOG.debug("Getting auth password.") + return self._app.get_auth_password() diff --git a/trove/guestagent/datastore/experimental/redis/service.py b/trove/guestagent/datastore/experimental/redis/service.py index 5e46112486..fc30703129 100644 --- a/trove/guestagent/datastore/experimental/redis/service.py +++ b/trove/guestagent/datastore/experimental/redis/service.py @@ -20,6 +20,7 @@ from redis.exceptions import BusyLoadingError, ConnectionError from oslo_log import log as logging from trove.common import cfg +from trove.common.db.redis.models import RedisRootUser from trove.common import exception from trove.common.i18n import _ from trove.common import instance as rd_instance @@ -37,6 +38,7 @@ LOG = logging.getLogger(__name__) TIME_OUT = 1200 CONF = cfg.CONF CLUSTER_CFG = 'clustering' +SYS_OVERRIDES_AUTH = 'auth_password' packager = pkg.Package() @@ -401,6 +403,31 @@ class RedisApp(object): LOG.exception(_('Error removing node from cluster.')) raise + def enable_root(self, password=None): + if not password: + password = utils.generate_random_password() + redis_password = RedisRootUser(password=password) + try: + self.configuration_manager.apply_system_override( + {'requirepass': password, 'masterauth': password}, + change_id=SYS_OVERRIDES_AUTH) + self.apply_overrides( + self.admin, {'requirepass': password, 'masterauth': password}) + except exception.TroveError: + LOG.exception(_('Error enabling authentication for instance.')) + raise + return redis_password.serialize() + + def disable_root(self): + try: + self.configuration_manager.remove_system_override( + change_id=SYS_OVERRIDES_AUTH) + self.apply_overrides(self.admin, + {'requirepass': '', 'masterauth': ''}) + except exception.TroveError: + LOG.exception(_('Error disabling authentication for instance.')) + raise + class RedisAdmin(object): """Handles administrative tasks on the Redis database. diff --git a/trove/guestagent/datastore/manager.py b/trove/guestagent/datastore/manager.py index 0baa7b3011..d6311c5bae 100644 --- a/trove/guestagent/datastore/manager.py +++ b/trove/guestagent/datastore/manager.py @@ -730,6 +730,11 @@ class Manager(periodic_task.PeriodicTasks): raise exception.DatastoreOperationNotSupported( operation='change_passwords', datastore=self.manager) + def get_root_password(self, context): + LOG.debug("Getting root password.") + raise exception.DatastoreOperationNotSupported( + operation='get_root_password', datastore=self.manager) + def enable_root(self, context): LOG.debug("Enabling root.") raise exception.DatastoreOperationNotSupported( diff --git a/trove/tests/int_tests.py b/trove/tests/int_tests.py index 2bc7775012..4ee7e1ca6f 100644 --- a/trove/tests/int_tests.py +++ b/trove/tests/int_tests.py @@ -404,7 +404,8 @@ register( ["redis_supported"], single=[common_groups, backup_groups, - configuration_groups, ], + configuration_groups, + root_actions_groups, ], multi=[replication_promote_groups, ] # multi=[cluster_actions_groups, # cluster_negative_actions_groups, diff --git a/trove/tests/unittests/guestagent/test_redis_manager.py b/trove/tests/unittests/guestagent/test_redis_manager.py index c76965b553..f86d36e4d9 100644 --- a/trove/tests/unittests/guestagent/test_redis_manager.py +++ b/trove/tests/unittests/guestagent/test_redis_manager.py @@ -14,6 +14,7 @@ from mock import DEFAULT, MagicMock, Mock, patch +from trove.common import utils as utils from trove.guestagent import backup from trove.guestagent.common import configuration from trove.guestagent.common.configuration import ImportOverrideStrategy @@ -329,3 +330,26 @@ class RedisGuestAgentManagerTest(DatastoreManagerTest): self.manager._get_repl_info = MagicMock(return_value=repl_info) self.manager.wait_for_txn(self.context, expected_txn_id) self.manager._get_repl_info.assert_any_call() + + @patch.object(configuration.ConfigurationManager, 'apply_system_override') + @patch.object(redis_service.RedisApp, 'apply_overrides') + @patch.object(utils, 'generate_random_password', + return_value='password') + def test_enable_root(self, *mock): + root_user = {'_name': '-', + '_password': 'password'} + + result = self.manager.enable_root(self.context) + self.assertEqual(root_user, result) + + @patch.object(redis_service.RedisApp, 'disable_root') + def test_disable_root(self, disable_root_mock): + self.manager.disable_root(self.context) + disable_root_mock.assert_any_call() + + @patch.object(redis_service.RedisApp, 'get_auth_password', + return_value="password") + def test_get_root_password(self, get_auth_password_mock): + result = self.manager.get_root_password(self.context) + self.assertTrue(get_auth_password_mock.called) + self.assertEqual('password', result) diff --git a/trove/tests/unittests/redis/__init__.py b/trove/tests/unittests/redis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/tests/unittests/redis/test_redis_root_controller.py b/trove/tests/unittests/redis/test_redis_root_controller.py new file mode 100644 index 0000000000..062b8260d2 --- /dev/null +++ b/trove/tests/unittests/redis/test_redis_root_controller.py @@ -0,0 +1,205 @@ +# Copyright 2017 Eayun, 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. +# +import uuid + +from mock import Mock, patch + +from trove.common import exception +from trove.datastore import models as datastore_models +from trove.extensions.common import models +from trove.extensions.redis.service import RedisRootController +from trove.instance import models as instance_models +from trove.instance.models import DBInstance +from trove.instance.tasks import InstanceTasks +from trove.taskmanager import api as task_api +from trove.tests.unittests import trove_testtools +from trove.tests.unittests.util import util + + +class TestRedisRootController(trove_testtools.TestCase): + + @patch.object(task_api.API, 'get_client', Mock(return_value=Mock())) + def setUp(self): + util.init_db() + self.context = trove_testtools.TroveTestContext(self, is_admin=True) + self.datastore = datastore_models.DBDatastore.create( + id=str(uuid.uuid4()), + name='redis' + str(uuid.uuid4()), + ) + self.datastore_version = ( + datastore_models.DBDatastoreVersion.create( + id=str(uuid.uuid4()), + datastore_id=self.datastore.id, + name="3.2" + str(uuid.uuid4()), + manager="redis", + image_id="image_id", + packages="", + active=True)) + self.tenant_id = "UUID" + self.single_db_info = DBInstance.create( + id="redis-single", + name="redis-single", + flavor_id=1, + datastore_version_id=self.datastore_version.id, + tenant_id=self.tenant_id, + volume_size=None, + task_status=InstanceTasks.NONE) + self.master_db_info = DBInstance.create( + id="redis-master", + name="redis-master", + flavor_id=1, + datastore_version_id=self.datastore_version.id, + tenant_id=self.tenant_id, + volume_size=None, + task_status=InstanceTasks.NONE) + self.slave_db_info = DBInstance.create( + id="redis-slave", + name="redis-slave", + flavor_id=1, + datastore_version_id=self.datastore_version.id, + tenant_id=self.tenant_id, + volume_size=None, + task_status=InstanceTasks.NONE, + slave_of_id=self.master_db_info.id) + + super(TestRedisRootController, self).setUp() + self.controller = RedisRootController() + + def tearDown(self): + self.datastore.delete() + self.datastore_version.delete() + self.master_db_info.delete() + self.slave_db_info.delete() + super(TestRedisRootController, self).tearDown() + + @patch.object(instance_models.Instance, "load") + @patch.object(models.Root, "create") + def test_root_create_on_single_instance(self, root_create, *args): + user = Mock() + context = Mock() + context.user = Mock() + context.user.__getitem__ = Mock(return_value=user) + req = Mock() + req.environ = Mock() + req.environ.__getitem__ = Mock(return_value=context) + tenant_id = self.tenant_id + instance_id = self.single_db_info.id + is_cluster = False + password = Mock() + body = {"password": password} + self.controller.root_create(req, body, tenant_id, + instance_id, is_cluster) + root_create.assert_called_with(context, instance_id, + context.user, password) + + @patch.object(instance_models.Instance, "load") + @patch.object(models.Root, "create") + def test_root_create_on_master_instance(self, root_create, *args): + user = Mock() + context = Mock() + context.user = Mock() + context.user.__getitem__ = Mock(return_value=user) + req = Mock() + req.environ = Mock() + req.environ.__getitem__ = Mock(return_value=context) + tenant_id = self.tenant_id + instance_id = self.master_db_info.id + slave_instance_id = self.slave_db_info.id + is_cluster = False + password = Mock() + body = {"password": password} + self.controller.root_create(req, body, tenant_id, + instance_id, is_cluster) + root_create.assert_called_with(context, slave_instance_id, + context.user, password) + + def test_root_create_on_slave(self): + user = Mock() + context = Mock() + context.user = Mock() + context.user.__getitem__ = Mock(return_value=user) + req = Mock() + req.environ = Mock() + req.environ.__getitem__ = Mock(return_value=context) + tenant_id = self.tenant_id + instance_id = self.slave_db_info.id + is_cluster = False + body = {} + self.assertRaises( + exception.SlaveOperationNotSupported, + self.controller.root_create, + req, body, tenant_id, instance_id, is_cluster) + + def test_root_create_with_cluster(self): + req = Mock() + tenant_id = self.tenant_id + instance_id = self.master_db_info.id + is_cluster = True + body = {} + self.assertRaises( + exception.ClusterOperationNotSupported, + self.controller.root_create, + req, body, tenant_id, instance_id, is_cluster) + + @patch.object(instance_models.Instance, "load") + @patch.object(models.Root, "delete") + def test_root_delete_on_single_instance(self, root_delete, *args): + context = Mock() + req = Mock() + req.environ = Mock() + req.environ.__getitem__ = Mock(return_value=context) + tenant_id = self.tenant_id + instance_id = self.single_db_info.id + is_cluster = False + self.controller.root_delete(req, tenant_id, instance_id, is_cluster) + root_delete.assert_called_with(context, instance_id) + + @patch.object(instance_models.Instance, "load") + @patch.object(models.Root, "delete") + def test_root_delete_on_master_instance(self, root_delete, *args): + context = Mock() + req = Mock() + req.environ = Mock() + req.environ.__getitem__ = Mock(return_value=context) + tenant_id = self.tenant_id + instance_id = self.master_db_info.id + slave_instance_id = self.slave_db_info.id + is_cluster = False + self.controller.root_delete(req, tenant_id, instance_id, is_cluster) + root_delete.assert_called_with(context, slave_instance_id) + + def test_root_delete_on_slave(self): + context = Mock() + req = Mock() + req.environ = Mock() + req.environ.__getitem__ = Mock(return_value=context) + tenant_id = self.tenant_id + instance_id = self.slave_db_info.id + is_cluster = False + self.assertRaises( + exception.SlaveOperationNotSupported, + self.controller.root_delete, + req, tenant_id, instance_id, is_cluster) + + def test_root_delete_with_cluster(self): + req = Mock() + tenant_id = self.tenant_id + instance_id = self.master_db_info.id + is_cluster = True + self.assertRaises( + exception.ClusterOperationNotSupported, + self.controller.root_delete, + req, tenant_id, instance_id, is_cluster)