From 287ca3ac4fb3e32cacf382ebaf16f383022b06ba Mon Sep 17 00:00:00 2001 From: Conrad Weidenkeller Date: Wed, 16 Oct 2013 07:35:07 -0500 Subject: [PATCH] Added Redis Crud Operations blueprint redis-support Change-Id: I741b3a39e1ec24ee81d441788be72f72726eda10 --- trove/guestagent/__init__.py | 16 -- trove/guestagent/datastore/__init__.py | 16 -- trove/guestagent/datastore/redis/__init__.py | 0 trove/guestagent/datastore/redis/manager.py | 124 ++++++++ trove/guestagent/datastore/redis/service.py | 269 ++++++++++++++++++ trove/guestagent/datastore/redis/system.py | 39 +++ trove/guestagent/dbaas.py | 2 +- trove/templates/redis/config.template | 41 +++ .../tests/unittests/guestagent/test_dbaas.py | 200 ++++++++++++- .../unittests/guestagent/test_manager.py | 96 ++++++- 10 files changed, 765 insertions(+), 38 deletions(-) create mode 100644 trove/guestagent/datastore/redis/__init__.py create mode 100644 trove/guestagent/datastore/redis/manager.py create mode 100644 trove/guestagent/datastore/redis/service.py create mode 100644 trove/guestagent/datastore/redis/system.py create mode 100644 trove/templates/redis/config.template diff --git a/trove/guestagent/__init__.py b/trove/guestagent/__init__.py index f0f23d6a20..e69de29bb2 100644 --- a/trove/guestagent/__init__.py +++ b/trove/guestagent/__init__.py @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 OpenStack Foundation -# 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. diff --git a/trove/guestagent/datastore/__init__.py b/trove/guestagent/datastore/__init__.py index f0f23d6a20..e69de29bb2 100644 --- a/trove/guestagent/datastore/__init__.py +++ b/trove/guestagent/datastore/__init__.py @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 OpenStack Foundation -# 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. diff --git a/trove/guestagent/datastore/redis/__init__.py b/trove/guestagent/datastore/redis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/guestagent/datastore/redis/manager.py b/trove/guestagent/datastore/redis/manager.py new file mode 100644 index 0000000000..f5840893d2 --- /dev/null +++ b/trove/guestagent/datastore/redis/manager.py @@ -0,0 +1,124 @@ +# Copyright (c) 2013 Rackspace +# 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 import cfg +from trove.guestagent import dbaas +from trove.guestagent import volume +from trove.guestagent.datastore.redis.service import RedisAppStatus +from trove.guestagent.datastore.redis.service import RedisApp +from trove.guestagent.datastore.redis import system +from trove.openstack.common import log as logging +from trove.openstack.common.gettextutils import _ +from trove.openstack.common import periodic_task + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class Manager(periodic_task.PeriodicTasks): + """ + This is the Redis manager class. It is dynamically loaded + based off of the service_type of the trove instance + """ + + @periodic_task.periodic_task(ticks_between_runs=3) + def update_status(self, context): + """ + Updates the redis trove instance. It is decorated with + perodic task so it is automatically called every 3 ticks. + """ + RedisAppStatus.get().update() + + def change_passwords(self, context, users): + """ + Changes the redis instance password, + it is currently not not implemented. + """ + raise NotImplemented() + + def reset_configuration(self, context, configuration): + """ + Resets to the default configuration, + currently this does nothing. + """ + app = RedisApp(RedisAppStatus.get()) + app.reset_configuration(configuration) + + def _perform_restore(self, backup_info, context, restore_location, app): + """ + Perform a restore on this instance, + currently it is not implemented. + """ + raise NotImplemented() + + def prepare(self, context, packages, databases, memory_mb, users, + device_path=None, mount_point=None, backup_info=None, + config_contents=None, root_password=None): + """ + This is called when the trove instance first comes online. + It is the first rpc message passed from the task manager. + prepare handles all the base configuration of the redis instance. + """ + app = RedisApp(RedisAppStatus.get()) + RedisAppStatus.get().begin_install() + if device_path: + device = volume.VolumeDevice(device_path) + device.format() + device.mount(system.REDIS_BASE_DIR) + LOG.debug(_('Mounted the volume.')) + app.install_if_needed(packages) + LOG.info(_('Securing redis now.')) + app.write_config(config_contents) + app.complete_install_or_restart() + LOG.info(_('"prepare" redis call has finished.')) + + def restart(self, context): + """ + Restart this redis instance. + This method is called when the guest agent + gets a restart message from the taskmanager. + """ + app = RedisApp(RedisAppStatus.get()) + app.restart() + + def start_db_with_conf_changes(self, context, config_contents): + """ + Start this redis instance with new conf changes. + Right now this does nothing. + """ + raise NotImplemented() + + def stop_db(self, context, do_not_start_on_reboot=False): + """ + Stop this redis instance. + This method is called when the guest agent + gets a stop message from the taskmanager. + """ + app = RedisApp(RedisAppStatus.get()) + app.stop_db(do_not_start_on_reboot=do_not_start_on_reboot) + + def get_filesystem_stats(self, context, fs_path): + """ + Gets file system stats from the provided fs_path. + """ + return dbaas.get_filesystem_volume_stats(fs_path) + + def create_backup(self, context, backup_info): + """ + This will eventually create a backup. Right now + it does nothing. + """ + raise NotImplemented() diff --git a/trove/guestagent/datastore/redis/service.py b/trove/guestagent/datastore/redis/service.py new file mode 100644 index 0000000000..7d99f56f58 --- /dev/null +++ b/trove/guestagent/datastore/redis/service.py @@ -0,0 +1,269 @@ +# Copyright (c) 2013 Rackspace +# 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 os + +from trove.common import cfg +from trove.common import utils as utils +from trove.common import exception +from trove.common import instance as rd_instance +from trove.guestagent import pkg +from trove.guestagent.datastore import service +from trove.guestagent.datastore.redis import system +from trove.openstack.common import log as logging +from trove.openstack.common.gettextutils import _ + +LOG = logging.getLogger(__name__) +TMP_REDIS_CONF = '/tmp/redis.conf.tmp' +TIME_OUT = 1200 +CONF = cfg.CONF +packager = pkg.Package() + + +def _load_redis_options(): + """ + Reads the redis config file for all redis options. + Right now this does not do any smart parsing and returns only key + value pairs as a str, str. + So: 'foo bar baz' becomes {'foo' : 'bar baz'} + """ + options = {} + with open(system.REDIS_CONFIG, 'r') as fd: + for opt in fd.readlines(): + opt = opt.rstrip().split(' ') + options.update({opt[0]: ' '.join(opt[1:])}) + return options + + +class RedisAppStatus(service.BaseDbStatus): + """ + Handles all of the status updating for the redis guest agent. + """ + @classmethod + def get(cls): + """ + Gets an instance of the RedisAppStatus class. + """ + if not cls._instance: + cls._instance = RedisAppStatus() + return cls._instance + + def _get_actual_db_status(self): + """ + Gets the actual status of the Redis instance + First it attempts to make a connection to the redis instance + by making a PING request. + If PING does not return PONG we do a ps + to see if the process is blocked or hung. + This implementation stinks but redis-cli only returns 0 + at this time. + http://redis.googlecode.com/svn/trunk/redis-cli.c + If we raise another exception.ProcessExecutionError while + running ps. + We attempt to locate the PID file and see if the process + is crashed or shutdown. + Remeber by default execute_with_timeout raises this exception + if a non 0 status code is returned from the cmd called. + """ + options = _load_redis_options() + out = "" + err = "" + try: + if 'requirepass' in options: + LOG.info(_('Password is set running ping with password')) + out, err = utils.execute_with_timeout( + system.REDIS_CLI, + '-a', + options['requirepass'], + 'PING', + run_as_root=True, + root_helper='sudo') + else: + LOG.info(_('Password not set running ping without password')) + out, err = utils.execute_with_timeout( + system.REDIS_CLI, + 'PING', + run_as_root=True, + root_helper='sudo') + LOG.info(_('Redis is RUNNING.')) + return rd_instance.ServiceStatuses.RUNNING + except exception.ProcessExecutionError: + LOG.error(_('Process execution error on redis-cli')) + if 'PONG' not in out: + try: + out, err = utils.execute_with_timeout('/bin/ps', '-C', + 'redis-server', 'h') + pid = out.split()[0] + msg = _('Redis pid: %s') % (pid) + LOG.info(msg) + LOG.info(_('Service Status is BLOCKED.')) + return rd_instance.ServiceStatuses.BLOCKED + except exception.ProcessExecutionError: + pid_file = options.get('pidfile', + '/var/run/redis/redis-server.pid') + if os.path.exists(pid_file): + LOG.info(_('Service Status is CRASHED.')) + return rd_instance.ServiceStatuses.CRASHED + else: + LOG.info(_('Service Status is SHUTDOWN.')) + return rd_instance.ServiceStatuses.SHUTDOWN + + +class RedisApp(object): + """ + Handles installation and configuration of redis + on a trove instance. + """ + + def __init__(self, status, state_change_wait_time=None): + """ + Sets default status and state_change_wait_time + """ + if state_change_wait_time: + self.state_change_wait_time = state_change_wait_time + else: + self.state_change_wait_time = CONF.state_change_wait_time + self.status = status + + def install_if_needed(self, packages): + """ + Install redis if needed do nothing if it is already installed. + """ + LOG.info(_('Preparing Guest as Redis Server')) + if not packager.pkg_is_installed(packages): + LOG.info(_('Installing Redis')) + self._install_redis(packages) + LOG.info(_('Dbaas install_if_needed complete')) + + def complete_install_or_restart(self): + """ + finalize status updates for install or restart. + """ + self.status.end_install_or_restart() + + def _install_redis(self, packages): + """ + Install the redis server. + """ + LOG.debug(_('Installing redis server')) + msg = _("Creating %s") % (system.REDIS_CONF_DIR) + LOG.debug(msg) + utils.execute_with_timeout('mkdir', + '-p', + system.REDIS_CONF_DIR, + run_as_root=True, + root_helper='sudo') + pkg_opts = {} + packager.pkg_install(packages, pkg_opts, TIME_OUT) + self.start_redis() + LOG.debug(_('Finished installing redis server')) + + def _enable_redis_on_boot(self): + """ + Enables redis on boot. + """ + LOG.info(_('Enabling redis on boot.')) + if os.path.isfile(system.REDIS_INIT): + LOG.info(_("OS Using Upstart")) + cmd = "sudo sed -i '/^manual$/d' %s" % (system.REDIS_INIT) + utils.execute_with_timeout(cmd, + shell=True) + else: + cmd = 'sudo %s' % (system.REDIS_CMD_ENABLE) + utils.execute_with_timeout(cmd, + shell=True) + + def _disable_redis_on_boot(self): + """ + Disables redis on boot. + """ + LOG.info(_('Disabling redis on boot.')) + if os.path.isfile(system.REDIS_INIT): + LOG.info(_("OS Using Upstart")) + utils.execute_with_timeout('echo', + "'manual'", + '>>', + system.REDIS_INIT, + run_as_root=True, + root_helper='sudo') + else: + cmd = 'sudo %s' % (system.REDIS_CMD_DISABLE) + utils.execute_with_timeout(cmd, + shell=True) + + def stop_db(self, update_db=False, do_not_start_on_reboot=False): + """ + Stops the redis application on the trove instance. + """ + LOG.info(_('Stopping redis...')) + if do_not_start_on_reboot: + self._disable_redis_on_boot() + cmd = 'sudo %s' % (system.REDIS_CMD_STOP) + utils.execute_with_timeout(cmd, + shell=True) + if not self.status.wait_for_real_status_to_change_to( + rd_instance.ServiceStatuses.SHUTDOWN, + self.state_change_wait_time, update_db): + LOG.error(_('Could not stop Redis!')) + self.status.end_install_or_restart() + + def restart(self): + """ + Restarts the redis daemon. + """ + try: + self.status.begin_restart() + self.stop_db() + self.start_redis() + finally: + self.status.end_install_or_restart() + + def write_config(self, config_contents): + """ + Write the redis config. + """ + with open(TMP_REDIS_CONF, 'w') as fd: + fd.write(config_contents) + utils.execute_with_timeout('mv', + TMP_REDIS_CONF, + system.REDIS_CONFIG, + run_as_root=True, + root_helper='sudo') + + def start_redis(self, update_db=False): + """ + Start the redis daemon. + """ + LOG.info(_("Starting redis...")) + self._enable_redis_on_boot() + try: + cmd = 'sudo %s' % (system.REDIS_CMD_START) + utils.execute_with_timeout(cmd, + shell=True) + except exception.ProcessExecutionError: + pass + if not self.status.wait_for_real_status_to_change_to( + rd_instance.ServiceStatuses.RUNNING, + self.state_change_wait_time, update_db): + LOG.error(_("Start up of redis failed!")) + try: + utils.execute_with_timeout('pkill', '-9', + 'redis-server', + run_as_root=True, + root_helper='sudo') + except exception.ProcessExecutionError as p: + LOG.error('Error killing stalled redis start command.') + LOG.error(p) + self.status.end_install_or_restart() diff --git a/trove/guestagent/datastore/redis/system.py b/trove/guestagent/datastore/redis/system.py new file mode 100644 index 0000000000..92aef64501 --- /dev/null +++ b/trove/guestagent/datastore/redis/system.py @@ -0,0 +1,39 @@ +# Copyright (c) 2013 Rackspace +# 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. + +""" +Determines operating system version and os depended commands. +""" +from trove.guestagent.common.operating_system import get_os + +OS = get_os() +REDIS_CONFIG = '/etc/redis/redis.conf' +REDIS_CONF_DIR = '/etc/redis' +REDIS_INIT = '/etc/init/redis-server.conf' +REDIS_CLI = '/usr/bin/redis-cli' +REDIS_BIN = '/usr/bin/redis-server' +REDIS_CMD_ENABLE = 'update-rc.d redis-server enable' +REDIS_CMD_DISABLE = 'update-rc.d redis-server disable' +REDIS_CMD_START = 'service redis-server start || /bin/true' +REDIS_CMD_STOP = 'service redis-server stop || /bin/true' +REDIS_PACKAGE = 'redis-server' +REDIS_BASE_DIR = '/var/lib/redis' + +if OS is 'redhat': + REDIS_BIN = '/usr/libexec/redis-server' + REDIS_CMD_ENABLE = 'chkconfig redis-server on' + REDIS_CMD_DISABLE = 'chkconfig redis-server off' + REDIS_CMD_START = 'service redis-server start' + REDIS_CMD_STOP = 'service redis-server stop' diff --git a/trove/guestagent/dbaas.py b/trove/guestagent/dbaas.py index 4c82509f45..9368d14e97 100644 --- a/trove/guestagent/dbaas.py +++ b/trove/guestagent/dbaas.py @@ -36,8 +36,8 @@ LOG = log.getLogger(__name__) defaults = { 'mysql': 'trove.guestagent.datastore.mysql.manager.Manager', 'percona': 'trove.guestagent.datastore.mysql.manager.Manager', + 'redis': 'trove.guestagent.datastore.redis.manager.Manager', } - CONF = cfg.CONF diff --git a/trove/templates/redis/config.template b/trove/templates/redis/config.template new file mode 100644 index 0000000000..225496b87a --- /dev/null +++ b/trove/templates/redis/config.template @@ -0,0 +1,41 @@ +daemonize yes +pidfile /var/run/redis/redis-server.pid +port 6379 +timeout 0 +loglevel notice +databases 16 +save 900 1 +save 300 10 +save 60 10000 +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir /var/lib/redis +slave-serve-stale-data yes +slave-read-only yes +slave-priority 100 +rename-command CONFIG "" +maxclients 10000 +maxmemory 1073741824 +maxmemory-policy volatile-lru +maxmemory-samples 3 +appendonly yes +appendfilename appendonly.aof +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +lua-time-limit 5000 +slowlog-max-len 128 +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 +list-max-ziplist-entries 512 +list-max-ziplist-value 64 +set-max-intset-entries 512 +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 +activerehashing yes +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 diff --git a/trove/tests/unittests/guestagent/test_dbaas.py b/trove/tests/unittests/guestagent/test_dbaas.py index c2cb0b0105..0c81aa9830 100644 --- a/trove/tests/unittests/guestagent/test_dbaas.py +++ b/trove/tests/unittests/guestagent/test_dbaas.py @@ -31,7 +31,6 @@ import testtools from testtools.matchers import Is from testtools.matchers import Equals from testtools.matchers import Not - from trove.extensions.mysql.models import RootHistory import trove from trove.common.context import TroveContext @@ -44,6 +43,9 @@ from trove.guestagent import pkg from trove.guestagent.dbaas import to_gb from trove.guestagent.dbaas import get_filesystem_volume_stats from trove.guestagent.datastore.service import BaseDbStatus +from trove.guestagent.datastore.redis import service as rservice +from trove.guestagent.datastore.redis.service import RedisApp +from trove.guestagent.datastore.redis import system as RedisSystem from trove.guestagent.datastore.mysql.service import MySqlAdmin from trove.guestagent.datastore.mysql.service import MySqlRootAccess from trove.guestagent.datastore.mysql.service import MySqlApp @@ -931,7 +933,7 @@ class ServiceRegistryTest(testtools.TestCase): dbaas_sr.get_custom_managers = Mock(return_value= datastore_registry_ext_test) test_dict = dbaas_sr.datastore_registry() - self.assertEqual(3, len(test_dict)) + self.assertEqual(4, len(test_dict)) self.assertEqual(test_dict.get('test'), datastore_registry_ext_test.get('test', None)) self.assertEqual(test_dict.get('mysql'), @@ -940,6 +942,9 @@ class ServiceRegistryTest(testtools.TestCase): self.assertEqual(test_dict.get('percona'), 'trove.guestagent.datastore.mysql.' 'manager.Manager') + self.assertEqual(test_dict.get('redis'), + 'trove.guestagent.datastore.redis.' + 'manager.Manager') def test_datastore_registry_with_existing_manager(self): datastore_registry_ext_test = { @@ -949,26 +954,30 @@ class ServiceRegistryTest(testtools.TestCase): dbaas_sr.get_custom_managers = Mock(return_value= datastore_registry_ext_test) test_dict = dbaas_sr.datastore_registry() - self.assertEqual(2, len(test_dict)) + self.assertEqual(3, len(test_dict)) self.assertEqual(test_dict.get('mysql'), 'trove.guestagent.datastore.mysql.' 'manager.Manager123') self.assertEqual(test_dict.get('percona'), 'trove.guestagent.datastore.mysql.' 'manager.Manager') + self.assertEqual(test_dict.get('redis'), + 'trove.guestagent.datastore.redis.manager.Manager') def test_datastore_registry_with_blank_dict(self): datastore_registry_ext_test = dict() dbaas_sr.get_custom_managers = Mock(return_value= datastore_registry_ext_test) test_dict = dbaas_sr.datastore_registry() - self.assertEqual(2, len(test_dict)) + self.assertEqual(3, len(test_dict)) self.assertEqual(test_dict.get('mysql'), 'trove.guestagent.datastore.mysql.' 'manager.Manager') self.assertEqual(test_dict.get('percona'), 'trove.guestagent.datastore.mysql.' 'manager.Manager') + self.assertEqual(test_dict.get('redis'), + 'trove.guestagent.datastore.redis.manager.Manager') class KeepAliveConnectionTest(testtools.TestCase): @@ -1194,3 +1203,186 @@ class MySqlAppStatusTest(testtools.TestCase): status = self.mySqlAppStatus._get_actual_db_status() self.assertEqual(rd_instance.ServiceStatuses.BLOCKED, status) + + +class TestRedisApp(testtools.TestCase): + + def setUp(self): + super(TestRedisApp, self).setUp() + self.FAKE_ID = 1000 + self.appStatus = FakeAppStatus(self.FAKE_ID, + rd_instance.ServiceStatuses.NEW) + self.app = RedisApp(self.appStatus) + self.orig_os_path_isfile = os.path.isfile + self.orig_utils_execute_with_timeout = utils.execute_with_timeout + utils.execute_with_timeout = Mock() + rservice.utils.execute_with_timeout = Mock() + + def tearDown(self): + super(TestRedisApp, self).tearDown() + self.app = None + os.path.isfile = self.orig_os_path_isfile + utils.execute_with_timeout = self.orig_utils_execute_with_timeout + rservice.utils.execute_with_timeout = \ + self.orig_utils_execute_with_timeout + unstub() + + def test_install_if_needed_installed(self): + when(pkg.Package).pkg_is_installed(any()).thenReturn(True) + when(RedisApp)._install_redis('bar').thenReturn(None) + self.app.install_if_needed('bar') + verify(pkg.Package).pkg_is_installed('bar') + verify(RedisApp, times=0)._install_redis('bar') + + def test_install_if_needed_not_installed(self): + when(pkg.Package).pkg_is_installed(any()).thenReturn(False) + when(RedisApp)._install_redis('asdf').thenReturn(None) + self.app.install_if_needed('asdf') + verify(pkg.Package).pkg_is_installed('asdf') + verify(RedisApp)._install_redis('asdf') + + def test_install_redis(self): + when(utils).execute_with_timeout(any()) + when(pkg.Package).pkg_install('redis', {}, 1200).thenReturn(None) + when(RedisApp).start_redis().thenReturn(None) + self.app._install_redis('redis') + verify(utils).execute_with_timeout(any()) + verify(pkg.Package).pkg_install('redis', {}, 1200) + verify(RedisApp).start_redis() + + def test_enable_redis_on_boot_without_upstart(self): + when(os.path).isfile(RedisSystem.REDIS_INIT).thenReturn(False) + when(utils).execute_with_timeout('sudo ' + + RedisSystem.REDIS_CMD_ENABLE, + shell=True).thenReturn(None) + self.app._enable_redis_on_boot() + verify(os.path).isfile(RedisSystem.REDIS_INIT) + verify(utils).execute_with_timeout('sudo ' + + RedisSystem.REDIS_CMD_ENABLE, + shell=True) + + def test_enable_redis_on_boot_with_upstart(self): + when(os.path).isfile(RedisSystem.REDIS_INIT).thenReturn(True) + when(utils).execute_with_timeout("sudo sed -i '/^manual$/d' " + + RedisSystem.REDIS_INIT, + shell=True).thenReturn(None) + self.app._enable_redis_on_boot() + verify(os.path).isfile(RedisSystem.REDIS_INIT) + verify(utils).execute_with_timeout("sudo sed -i '/^manual$/d' " + + RedisSystem.REDIS_INIT, + shell=True) + + def test_disable_redis_on_boot_with_upstart(self): + when(os.path).isfile(RedisSystem.REDIS_INIT).thenReturn(True) + when(utils).execute_with_timeout('echo', + "'manual'", + '>>', + RedisSystem.REDIS_INIT, + run_as_root=True, + root_helper='sudo').thenReturn(None) + self.app._disable_redis_on_boot() + verify(os.path).isfile(RedisSystem.REDIS_INIT) + verify(utils).execute_with_timeout('echo', + "'manual'", + '>>', + RedisSystem.REDIS_INIT, + run_as_root=True, + root_helper='sudo') + + def test_disable_redis_on_boot_without_upstart(self): + when(os.path).isfile(RedisSystem.REDIS_INIT).thenReturn(False) + when(utils).execute_with_timeout('sudo ' + + RedisSystem.REDIS_CMD_DISABLE, + shell=True).thenReturn(None) + self.app._disable_redis_on_boot() + verify(os.path).isfile(RedisSystem.REDIS_INIT) + verify(utils).execute_with_timeout('sudo ' + + RedisSystem.REDIS_CMD_DISABLE, + shell=True) + + def test_stop_db_without_fail(self): + mock_status = mock() + when(mock_status).wait_for_real_status_to_change_to( + any(), any(), any()).thenReturn(True) + app = RedisApp(mock_status, state_change_wait_time=0) + when(RedisApp)._disable_redis_on_boot().thenReturn(None) + when(utils).execute_with_timeout('sudo ' + RedisSystem.REDIS_CMD_STOP, + shell=True).thenReturn(None) + when(mock_status).wait_for_real_status_to_change_to( + any(), + any(), + any()).thenReturn(True) + app.stop_db(do_not_start_on_reboot=True) + verify(RedisApp)._disable_redis_on_boot() + verify(utils).execute_with_timeout('sudo ' + + RedisSystem.REDIS_CMD_STOP, + shell=True) + verify(mock_status).wait_for_real_status_to_change_to( + any(), + any(), + any()) + + def test_stop_db_with_failure(self): + mock_status = mock() + when(mock_status).wait_for_real_status_to_change_to( + any(), any(), any()).thenReturn(True) + app = RedisApp(mock_status, state_change_wait_time=0) + when(RedisApp)._disable_redis_on_boot().thenReturn(None) + when(utils).execute_with_timeout('sudo ' + RedisSystem.REDIS_CMD_STOP, + shell=True).thenReturn(None) + when(mock_status).wait_for_real_status_to_change_to( + any(), + any(), + any()).thenReturn(False) + app.stop_db(do_not_start_on_reboot=True) + verify(RedisApp)._disable_redis_on_boot() + verify(utils).execute_with_timeout('sudo ' + + RedisSystem.REDIS_CMD_STOP, + shell=True) + verify(mock_status).wait_for_real_status_to_change_to( + any(), + any(), + any()) + verify(mock_status).end_install_or_restart() + + def test_restart(self): + mock_status = mock() + app = RedisApp(mock_status, state_change_wait_time=0) + when(mock_status).begin_restart().thenReturn(None) + when(RedisApp).stop_db().thenReturn(None) + when(RedisApp).start_redis().thenReturn(None) + when(mock_status).end_install_or_restart().thenReturn(None) + app.restart() + verify(mock_status).begin_restart() + verify(RedisApp).stop_db() + verify(RedisApp).start_redis() + verify(mock_status).end_install_or_restart() + + def test_start_redis(self): + mock_status = mock() + app = RedisApp(mock_status, state_change_wait_time=0) + when(RedisApp)._enable_redis_on_boot().thenReturn(None) + when(utils).execute_with_timeout('sudo ' + RedisSystem.REDIS_CMD_START, + shell=True).thenReturn(None) + when(mock_status).wait_for_real_status_to_change_to(any(), + any(), + any()).thenReturn( + None) + when(utils).execute_with_timeout('pkill', '-9', + 'redis-server', + run_as_root=True, + root_helper='sudo').thenReturn(None) + when(mock_status).end_install_or_restart().thenReturn(None) + app.start_redis() + verify(RedisApp)._enable_redis_on_boot() + verify(utils).execute_with_timeout('sudo ' + + RedisSystem.REDIS_CMD_START, + shell=True) + verify(mock_status).wait_for_real_status_to_change_to(any(), + any(), + any()) + verify(utils).execute_with_timeout('pkill', '-9', + 'redis-server', + run_as_root=True, + root_helper='sudo') + verify(mock_status).end_install_or_restart() diff --git a/trove/tests/unittests/guestagent/test_manager.py b/trove/tests/unittests/guestagent/test_manager.py index 2368bf98a6..585527ce09 100644 --- a/trove/tests/unittests/guestagent/test_manager.py +++ b/trove/tests/unittests/guestagent/test_manager.py @@ -18,10 +18,13 @@ import testtools from mockito import verify, when, unstub, any, mock, never from testtools.matchers import Is, Equals, Not -from trove.guestagent import volume from trove.common.context import TroveContext +from trove.guestagent import volume from trove.guestagent.datastore.mysql.manager import Manager import trove.guestagent.datastore.mysql.service as dbaas +from trove.guestagent.datastore.redis.manager import Manager as RedisManager +import trove.guestagent.datastore.redis.service as redis_service +import trove.guestagent.datastore.redis.system as redis_system from trove.guestagent import backup from trove.guestagent.volume import VolumeDevice from trove.guestagent import pkg @@ -208,3 +211,94 @@ class GuestAgentManagerTest(testtools.TestCase): verify(dbaas.MySqlApp).secure_root(secure_remote_root=any()) verify(dbaas.MySqlAdmin, times=times_report).report_root_enabled( self.context) + + +class RedisGuestAgentManagerTest(testtools.TestCase): + + def setUp(self): + super(RedisGuestAgentManagerTest, self).setUp() + self.context = TroveContext() + self.manager = RedisManager() + self.packages = 'redis-server' + self.origin_RedisAppStatus = redis_service.RedisAppStatus + self.origin_os_path_exists = os.path.exists + self.origin_format = volume.VolumeDevice.format + self.origin_migrate_data = volume.VolumeDevice.migrate_data + self.origin_mount = volume.VolumeDevice.mount + self.origin_stop_redis = redis_service.RedisApp.stop_db + self.origin_start_redis = redis_service.RedisApp.start_redis + self.origin_install_redis = redis_service.RedisApp._install_redis + + def tearDown(self): + super(RedisGuestAgentManagerTest, self).tearDown() + redis_service.RedisAppStatus = self.origin_RedisAppStatus + os.path.exists = self.origin_os_path_exists + volume.VolumeDevice.format = self.origin_format + volume.VolumeDevice.migrate_data = self.origin_migrate_data + volume.VolumeDevice.mount = self.origin_mount + redis_service.RedisApp.stop_db = self.origin_stop_redis + redis_service.RedisApp.start_redis = self.origin_start_redis + redis_service.RedisApp._install_redis = self.origin_install_redis + unstub() + + def test_update_status(self): + mock_status = mock() + when(redis_service.RedisAppStatus).get().thenReturn(mock_status) + self.manager.update_status(self.context) + verify(redis_service.RedisAppStatus).get() + verify(mock_status).update() + + def test_prepare_device_path_true(self): + self._prepare_dynamic() + + def test_prepare_device_path_false(self): + self._prepare_dynamic(device_path=None) + + def test_prepare_redis_not_installed(self): + self._prepare_dynamic(is_redis_installed=False) + + def _prepare_dynamic(self, device_path='/dev/vdb', is_redis_installed=True, + backup_info=None, is_root_enabled=False): + + # covering all outcomes is starting to cause trouble here + dev_path = 1 if device_path else 0 + mock_status = mock() + when(redis_service.RedisAppStatus).get().thenReturn(mock_status) + when(mock_status).begin_install().thenReturn(None) + when(VolumeDevice).format().thenReturn(None) + when(VolumeDevice).mount().thenReturn(None) + when(redis_service.RedisApp).start_redis().thenReturn(None) + when(redis_service.RedisApp).install_if_needed().thenReturn(None) + when(backup).restore(self.context, backup_info).thenReturn(None) + when(redis_service.RedisApp).write_config(any()).thenReturn(None) + when(redis_service.RedisApp).complete_install_or_restart( + any()).thenReturn(None) + self.manager.prepare(self.context, self.packages, + None, '2048', + None, device_path=device_path, + mount_point='/var/lib/redis', + backup_info=backup_info) + verify(redis_service.RedisAppStatus, times=2).get() + verify(mock_status).begin_install() + verify(VolumeDevice, times=dev_path).format() + verify(VolumeDevice, times=dev_path).mount(redis_system.REDIS_BASE_DIR) + verify(redis_service.RedisApp).install_if_needed(self.packages) + verify(redis_service.RedisApp).write_config(None) + verify(redis_service.RedisApp).complete_install_or_restart() + + def test_restart(self): + mock_status = mock() + when(redis_service.RedisAppStatus).get().thenReturn(mock_status) + when(redis_service.RedisApp).restart().thenReturn(None) + self.manager.restart(self.context) + verify(redis_service.RedisAppStatus).get() + verify(redis_service.RedisApp).restart() + + def test_stop_db(self): + mock_status = mock() + when(redis_service.RedisAppStatus).get().thenReturn(mock_status) + when(redis_service.RedisApp).stop_db(do_not_start_on_reboot= + False).thenReturn(None) + self.manager.stop_db(self.context) + verify(redis_service.RedisAppStatus).get() + verify(redis_service.RedisApp).stop_db(do_not_start_on_reboot=False)