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 <zh.f@outlook.com>
This commit is contained in:
parent
8aad3ee2e4
commit
a57bf8816b
@ -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.
|
@ -854,7 +854,7 @@ redis_opts = [
|
|||||||
help='Class that implements datastore-specific Guest Agent API '
|
help='Class that implements datastore-specific Guest Agent API '
|
||||||
'logic.'),
|
'logic.'),
|
||||||
cfg.StrOpt('root_controller',
|
cfg.StrOpt('root_controller',
|
||||||
default='trove.extensions.common.service.DefaultRootController',
|
default='trove.extensions.redis.service.RedisRootController',
|
||||||
help='Root controller implementation for redis.'),
|
help='Root controller implementation for redis.'),
|
||||||
cfg.StrOpt('guest_log_exposed_logs', default='',
|
cfg.StrOpt('guest_log_exposed_logs', default='',
|
||||||
help='List of Guest Logs to expose for publishing.'),
|
help='List of Guest Logs to expose for publishing.'),
|
||||||
|
0
trove/common/db/redis/__init__.py
Normal file
0
trove/common/db/redis/__init__.py
Normal file
28
trove/common/db/redis/models.py
Normal file
28
trove/common/db/redis/models.py
Normal file
@ -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__()
|
@ -671,3 +671,9 @@ class DatastoreVersionAlreadyExists(BadRequest):
|
|||||||
class LogAccessForbidden(Forbidden):
|
class LogAccessForbidden(Forbidden):
|
||||||
|
|
||||||
message = _("You must be admin to %(action)s log '%(log)s'.")
|
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.")
|
||||||
|
0
trove/extensions/redis/__init__.py
Normal file
0
trove/extensions/redis/__init__.py
Normal file
28
trove/extensions/redis/models.py
Normal file
28
trove/extensions/redis/models.py
Normal file
@ -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
|
185
trove/extensions/redis/service.py
Normal file
185
trove/extensions/redis/service.py
Normal file
@ -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)
|
30
trove/extensions/redis/views.py
Normal file
30
trove/extensions/redis/views.py
Normal file
@ -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}
|
@ -237,6 +237,15 @@ class API(object):
|
|||||||
|
|
||||||
self._cast("delete_database", version=version, database=database)
|
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):
|
def enable_root(self):
|
||||||
"""Make a synchronous call to enable the root user for
|
"""Make a synchronous call to enable the root user for
|
||||||
access from anywhere
|
access from anywhere
|
||||||
|
@ -267,3 +267,19 @@ class Manager(manager.Manager):
|
|||||||
LOG.debug("Executing cluster_addslots to assign hash slots %s-%s.",
|
LOG.debug("Executing cluster_addslots to assign hash slots %s-%s.",
|
||||||
first_slot, last_slot)
|
first_slot, last_slot)
|
||||||
self._app.cluster_addslots(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()
|
||||||
|
@ -20,6 +20,7 @@ from redis.exceptions import BusyLoadingError, ConnectionError
|
|||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
|
||||||
from trove.common import cfg
|
from trove.common import cfg
|
||||||
|
from trove.common.db.redis.models import RedisRootUser
|
||||||
from trove.common import exception
|
from trove.common import exception
|
||||||
from trove.common.i18n import _
|
from trove.common.i18n import _
|
||||||
from trove.common import instance as rd_instance
|
from trove.common import instance as rd_instance
|
||||||
@ -37,6 +38,7 @@ LOG = logging.getLogger(__name__)
|
|||||||
TIME_OUT = 1200
|
TIME_OUT = 1200
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CLUSTER_CFG = 'clustering'
|
CLUSTER_CFG = 'clustering'
|
||||||
|
SYS_OVERRIDES_AUTH = 'auth_password'
|
||||||
packager = pkg.Package()
|
packager = pkg.Package()
|
||||||
|
|
||||||
|
|
||||||
@ -401,6 +403,31 @@ class RedisApp(object):
|
|||||||
LOG.exception(_('Error removing node from cluster.'))
|
LOG.exception(_('Error removing node from cluster.'))
|
||||||
raise
|
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):
|
class RedisAdmin(object):
|
||||||
"""Handles administrative tasks on the Redis database.
|
"""Handles administrative tasks on the Redis database.
|
||||||
|
@ -730,6 +730,11 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
raise exception.DatastoreOperationNotSupported(
|
raise exception.DatastoreOperationNotSupported(
|
||||||
operation='change_passwords', datastore=self.manager)
|
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):
|
def enable_root(self, context):
|
||||||
LOG.debug("Enabling root.")
|
LOG.debug("Enabling root.")
|
||||||
raise exception.DatastoreOperationNotSupported(
|
raise exception.DatastoreOperationNotSupported(
|
||||||
|
@ -404,7 +404,8 @@ register(
|
|||||||
["redis_supported"],
|
["redis_supported"],
|
||||||
single=[common_groups,
|
single=[common_groups,
|
||||||
backup_groups,
|
backup_groups,
|
||||||
configuration_groups, ],
|
configuration_groups,
|
||||||
|
root_actions_groups, ],
|
||||||
multi=[replication_promote_groups, ]
|
multi=[replication_promote_groups, ]
|
||||||
# multi=[cluster_actions_groups,
|
# multi=[cluster_actions_groups,
|
||||||
# cluster_negative_actions_groups,
|
# cluster_negative_actions_groups,
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
from mock import DEFAULT, MagicMock, Mock, patch
|
from mock import DEFAULT, MagicMock, Mock, patch
|
||||||
|
|
||||||
|
from trove.common import utils as utils
|
||||||
from trove.guestagent import backup
|
from trove.guestagent import backup
|
||||||
from trove.guestagent.common import configuration
|
from trove.guestagent.common import configuration
|
||||||
from trove.guestagent.common.configuration import ImportOverrideStrategy
|
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._get_repl_info = MagicMock(return_value=repl_info)
|
||||||
self.manager.wait_for_txn(self.context, expected_txn_id)
|
self.manager.wait_for_txn(self.context, expected_txn_id)
|
||||||
self.manager._get_repl_info.assert_any_call()
|
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)
|
||||||
|
0
trove/tests/unittests/redis/__init__.py
Normal file
0
trove/tests/unittests/redis/__init__.py
Normal file
205
trove/tests/unittests/redis/test_redis_root_controller.py
Normal file
205
trove/tests/unittests/redis/test_redis_root_controller.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user