Implement Instance Upgrade

Implments Instance Upgrade functionality to support upgrading the
image of a Trove datastore instance from datastore_version to a
newer datastore_version of the same datastore.

This functionality builds on the Nova rebuild API to upgrade
the image of an instance with minimal downtime.

Includes datastore implementation of the upgrade functionality
for the Mysql based datastores.

Change-Id: Ie6e48d78ac07df52f686f359ca7fdadaae6ad064
Implements: blueprint image-upgrade
Depends-On: I6ec2ebb78019c014f87ba5d8cbfd284686c64f30
This commit is contained in:
Morgan Jones 2016-06-06 13:45:44 -04:00
parent f1136f723a
commit 2478c0d1d4
28 changed files with 504 additions and 56 deletions

View File

@ -0,0 +1,4 @@
features:
- New instance upgrade API supports upgrading an instance of
a datastore to a new datastore version. Includes implementation
for MySQL family of databases.

View File

@ -370,6 +370,7 @@ instance = {
"replica_of": {},
"name": non_empty_string,
"configuration": configuration_id,
"datastore_version": non_empty_string,
}
}
}

View File

@ -347,6 +347,10 @@ class DBaaSAPINotification(object):
def server_type(self, server_type):
self.payload['server_type'] = server_type
@property
def request_id(self):
return self.payload['request_id']
def __init__(self, context, **kwargs):
self.context = context
self.needs_end_notification = True
@ -753,3 +757,14 @@ class DBaaSConfigurationEdit(DBaaSAPINotification):
@abc.abstractmethod
def required_start_traits(self):
return ['configuration_id']
class DBaaSInstanceUpgrade(DBaaSAPINotification):
@abc.abstractmethod
def event_type(self):
return 'upgrade'
@abc.abstractmethod
def required_start_traits(self):
return ['instance_id', 'datastore_version_id']

View File

@ -149,4 +149,6 @@ class Manager(periodic_task.PeriodicTasks):
message, exception):
notification = SerializableNotification.deserialize(
context, serialized_notification)
LOG.error(_("Guest exception on request %(req)s:\n%(exc)s")
% {'req': notification.request_id, 'exc': exception})
notification.notify_exc_info(message, exception)

View File

@ -336,6 +336,9 @@ class Datastore(object):
def __init__(self, db_info):
self.db_info = db_info
def __repr__(self, *args, **kwargs):
return "%s(%s)" % (self.name, self.id)
@classmethod
def load(cls, id_or_name):
try:
@ -387,6 +390,9 @@ class DatastoreVersion(object):
self.db_info = db_info
self._datastore_name = None
def __repr__(self, *args, **kwargs):
return "%s(%s)" % (self.name, self.id)
@classmethod
def load(cls, datastore, id_or_name):
try:

View File

@ -267,6 +267,17 @@ class API(object):
server.stop()
server.wait()
def pre_upgrade(self):
"""Prepare the guest for upgrade."""
LOG.debug("Sending the call to prepare the guest for upgrade.")
return self._call("pre_upgrade", AGENT_HIGH_TIMEOUT, self.version_cap)
def post_upgrade(self, upgrade_info):
"""Recover the guest after upgrading the guest's image."""
LOG.debug("Recover the guest after upgrading the guest's image.")
self._call("post_upgrade", AGENT_HIGH_TIMEOUT, self.version_cap,
upgrade_info=upgrade_info)
def restart(self):
"""Restart the database server."""
LOG.debug("Sending the call to restart the database process "

View File

@ -96,7 +96,7 @@ class ConfigurationManager(object):
"""Return the current value at a given key or 'default'.
"""
if self._value_cache is None:
self._refresh_cache()
self.refresh_cache()
return self._value_cache.get(key, default)
@ -139,7 +139,7 @@ class ConfigurationManager(object):
self._base_config_path, FileMode.ADD_READ_ALL,
as_root=self._requires_root)
self._refresh_cache()
self.refresh_cache()
def has_system_override(self, change_id):
"""Return whether a given 'system' change exists.
@ -178,7 +178,7 @@ class ConfigurationManager(object):
group_name, change_id, self._codec.deserialize(options))
else:
self._override_strategy.apply(group_name, change_id, options)
self._refresh_cache()
self.refresh_cache()
def remove_system_override(self, change_id=DEFAULT_CHANGE_ID):
"""Revert a 'system' configuration change.
@ -192,9 +192,9 @@ class ConfigurationManager(object):
def _remove_override(self, group_name, change_id):
self._override_strategy.remove(group_name, change_id)
self._refresh_cache()
self.refresh_cache()
def _refresh_cache(self):
def refresh_cache(self):
self._value_cache = self.parse_configuration()

View File

@ -252,7 +252,7 @@ class Manager(periodic_task.PeriodicTasks):
return True
#################
# Prepare related
# Instance related
#################
def prepare(self, context, packages, databases, memory_mb, users,
device_path=None, mount_point=None, backup_info=None,
@ -389,6 +389,18 @@ class Manager(periodic_task.PeriodicTasks):
LOG.info(_('No post_prepare work has been defined.'))
pass
def pre_upgrade(self, context):
"""Prepares the guest for upgrade, returning a dict to be passed
to post_upgrade
"""
return {}
def post_upgrade(self, context, upgrade_info):
"""Recovers the guest after the image is upgraded using infomation
from the pre_upgrade step
"""
pass
#################
# Service related
#################
@ -407,11 +419,12 @@ class Manager(periodic_task.PeriodicTasks):
LOG.debug("Getting file system stats for '%s'" % mount_point)
return dbaas.get_filesystem_volume_stats(mount_point)
def mount_volume(self, context, device_path=None, mount_point=None):
def mount_volume(self, context, device_path=None, mount_point=None,
write_to_fstab=False):
LOG.debug("Mounting the device %s at the mount point %s." %
(device_path, mount_point))
device = volume.VolumeDevice(device_path)
device.mount(mount_point, write_to_fstab=False)
device.mount(mount_point, write_to_fstab=write_to_fstab)
def unmount_volume(self, context, device_path=None, mount_point=None):
LOG.debug("Unmounting the device %s from the mount point %s." %

View File

@ -242,6 +242,56 @@ class MySqlManager(manager.Manager):
if snapshot:
self.attach_replica(context, snapshot, snapshot['config'])
def pre_upgrade(self, context):
app = self.mysql_app(self.mysql_app_status.get())
data_dir = app.get_data_dir()
mount_point, _data = os.path.split(data_dir)
save_dir = "%s/etc_mysql" % mount_point
save_etc_dir = "%s/etc" % mount_point
home_save = "%s/trove_user" % mount_point
app.status.begin_restart()
app.stop_db()
if operating_system.exists("/etc/my.cnf", as_root=True):
operating_system.create_directory(save_etc_dir, as_root=True)
operating_system.copy("/etc/my.cnf", save_etc_dir,
preserve=True, as_root=True)
operating_system.copy("/etc/mysql/.", save_dir,
preserve=True, as_root=True)
operating_system.copy("%s/." % os.path.expanduser('~'), home_save,
preserve=True, as_root=True)
self.unmount_volume(context, mount_point=data_dir)
return {
'mount_point': mount_point,
'save_dir': save_dir,
'save_etc_dir': save_etc_dir,
'home_save': home_save
}
def post_upgrade(self, context, upgrade_info):
app = self.mysql_app(self.mysql_app_status.get())
app.stop_db()
if 'device' in upgrade_info:
self.mount_volume(context, mount_point=upgrade_info['mount_point'],
device_path=upgrade_info['device'],
write_to_fstab=True)
if operating_system.exists(upgrade_info['save_etc_dir'],
is_directory=True, as_root=True):
operating_system.copy("%s/." % upgrade_info['save_etc_dir'],
"/etc", preserve=True, as_root=True)
operating_system.copy("%s/." % upgrade_info['save_dir'], "/etc/mysql",
preserve=True, as_root=True)
operating_system.copy("%s/." % upgrade_info['home_save'],
os.path.expanduser('~'),
preserve=True, as_root=True)
app.start_mysql()
def restart(self, context):
app = self.mysql_app(self.mysql_app_status.get())
app.restart()

View File

@ -17,6 +17,7 @@
"""Model classes that form the core of instances functionality."""
from datetime import datetime
from datetime import timedelta
import os.path
import re
from novaclient import exceptions as nova_exceptions
@ -98,6 +99,7 @@ class InstanceStatus(object):
RESTART_REQUIRED = "RESTART_REQUIRED"
PROMOTE = "PROMOTE"
EJECT = "EJECT"
UPGRADE = "UPGRADE"
DETACH = "DETACH"
@ -129,7 +131,8 @@ def load_simple_instance_server_status(context, db_info):
# Invalid states to contact the agent
AGENT_INVALID_STATUSES = ["BUILD", "REBOOT", "RESIZE", "PROMOTE", "EJECT"]
AGENT_INVALID_STATUSES = ["BUILD", "REBOOT", "RESIZE", "PROMOTE", "EJECT",
"UPGRADE"]
class SimpleInstance(object):
@ -175,6 +178,9 @@ class SimpleInstance(object):
self.slave_list = None
def __repr__(self, *args, **kwargs):
return "%s(%s)" % (self.name, self.id)
@property
def addresses(self):
# TODO(tim.simpson): This code attaches two parts of the Nova server to
@ -296,6 +302,8 @@ class SimpleInstance(object):
return InstanceStatus.REBOOT
if 'RESIZING' == action:
return InstanceStatus.RESIZE
if 'UPGRADING' == action:
return InstanceStatus.UPGRADE
if 'RESTART_REQUIRED' == action:
return InstanceStatus.RESTART_REQUIRED
if InstanceTasks.PROMOTING.action == action:
@ -684,6 +692,32 @@ class BaseInstance(SimpleInstance):
self._server_group_loaded = True
return self._server_group
def get_injected_files(self, datastore_manager):
injected_config_location = CONF.get('injected_config_location')
guest_info = CONF.get('guest_info')
if ('/' in guest_info):
# Set guest_info_file to exactly guest_info from the conf file.
# This should be /etc/guest_info for pre-Kilo compatibility.
guest_info_file = guest_info
else:
guest_info_file = os.path.join(injected_config_location,
guest_info)
files = {guest_info_file: (
"[DEFAULT]\n"
"guest_id=%s\n"
"datastore_manager=%s\n"
"tenant_id=%s\n"
% (self.id, datastore_manager, self.tenant_id))}
if os.path.isfile(CONF.get('guest_config')):
with open(CONF.get('guest_config'), "r") as f:
files[os.path.join(injected_config_location,
"trove-guestagent.conf")] = f.read()
return files
class FreshInstance(BaseInstance):
@classmethod
@ -1230,6 +1264,12 @@ class Instance(BuiltInstance):
self.datastore_version, flavor, self.id)
return dict(config.render_dict())
def upgrade(self, datastore_version):
self.update_db(datastore_version_id=datastore_version.id,
task_status=InstanceTasks.UPGRADING)
task_api.API(self.context).upgrade(self.id,
datastore_version.id)
def create_server_list_matcher(server_list):
# Returns a method which finds a server from the given list.

View File

@ -312,15 +312,6 @@ class InstanceController(wsgi.Controller):
return configuration_id
def _modify_instance(self, context, req, instance, **kwargs):
"""Modifies the instance using the specified keyword arguments
'detach_replica': ignored if not present or False, if True,
specifies the instance is a replica that will be detached from
its master
'configuration_id': Ignored if not present, if None, detaches an
an attached configuration group, if not None, attaches the
specified configuration group
"""
if 'detach_replica' in kwargs and kwargs['detach_replica']:
LOG.debug("Detaching replica from source.")
context.notification = notification.DBaaSInstanceDetach(
@ -342,6 +333,14 @@ class InstanceController(wsgi.Controller):
request=req))
with StartNotification(context, instance_id=instance.id):
instance.unassign_configuration()
if 'datastore_version' in kwargs:
datastore_version = datastore_models.DatastoreVersion.load(
instance.datastore, kwargs['datastore_version'])
context.notification = (
notification.DBaaSInstanceUpgrade(context, request=req))
with StartNotification(context, instance_id=instance.id,
datastore_version_id=datastore_version.id):
instance.upgrade(datastore_version)
if kwargs:
instance.update_db(**kwargs)
@ -381,6 +380,10 @@ class InstanceController(wsgi.Controller):
args['name'] = body['instance']['name']
if 'configuration' in body['instance']:
args['configuration_id'] = self._configuration_parse(context, body)
if 'datastore_version' in body['instance']:
args['datastore_version'] = body['instance'].get(
'datastore_version')
self._modify_instance(context, req, instance, **args)
return wsgi.Result(None, 202)

View File

@ -114,6 +114,7 @@ class InstanceTasks(object):
SHRINKING_ERROR = InstanceTask(0x58, 'SHRINKING',
'Shrinking Cluster Error.',
is_error=True)
UPGRADING = InstanceTask(0x59, 'UPGRADING', 'Upgrading the instance.')
# Dissuade further additions at run-time.
InstanceTask.__init__ = None

View File

@ -198,6 +198,14 @@ class API(object):
self._cast("delete_cluster", self.version_cap, cluster_id=cluster_id)
def upgrade(self, instance_id, datastore_version_id):
LOG.debug("Making async call to upgrade guest to datastore "
"version %s " % datastore_version_id)
cctxt = self.client.prepare(version=self.version_cap)
cctxt.cast(self.context, "upgrade", instance_id=instance_id,
datastore_version_id=datastore_version_id)
def load(context, manager=None):
if manager:

View File

@ -30,6 +30,7 @@ from trove.common import remote
import trove.common.rpc.version as rpc_version
from trove.common import server_group as srv_grp
from trove.common.strategies.cluster import strategy
from trove.datastore.models import DatastoreVersion
import trove.extensions.mgmt.instances.models as mgmtmodels
from trove.instance.tasks import InstanceTasks
from trove.taskmanager import models
@ -383,6 +384,12 @@ class Manager(periodic_task.PeriodicTasks):
cluster_config, volume_type, modules,
locality)
def upgrade(self, context, instance_id, datastore_version_id):
instance_tasks = models.BuiltInstanceTasks.load(context, instance_id)
datastore_version = DatastoreVersion.load_by_uuid(datastore_version_id)
with EndNotification(context):
instance_tasks.upgrade(datastore_version)
def update_overrides(self, context, instance_id, overrides):
instance_tasks = models.BuiltInstanceTasks.load(context, instance_id)
instance_tasks.update_overrides(overrides)

80
trove/taskmanager/models.py Normal file → Executable file
View File

@ -336,32 +336,6 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
LOG.debug("End _delete_resource for instance %s" % self.id)
def _get_injected_files(self, datastore_manager):
injected_config_location = CONF.get('injected_config_location')
guest_info = CONF.get('guest_info')
if ('/' in guest_info):
# Set guest_info_file to exactly guest_info from the conf file.
# This should be /etc/guest_info for pre-Kilo compatibility.
guest_info_file = guest_info
else:
guest_info_file = os.path.join(injected_config_location,
guest_info)
files = {guest_info_file: (
"[DEFAULT]\n"
"guest_id=%s\n"
"datastore_manager=%s\n"
"tenant_id=%s\n"
% (self.id, datastore_manager, self.tenant_id))}
if os.path.isfile(CONF.get('guest_config')):
with open(CONF.get('guest_config'), "r") as f:
files[os.path.join(injected_config_location,
"trove-guestagent.conf")] = f.read()
return files
def wait_for_instance(self, timeout, flavor):
# Make sure the service becomes active before sending a usage
# record to avoid over billing a customer for an instance that
@ -421,7 +395,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
LOG.debug("Successfully created security group for "
"instance: %s" % self.id)
files = self._get_injected_files(datastore_manager)
files = self.get_injected_files(datastore_manager)
cinder_volume_type = volume_type or CONF.cinder_volume_type
if use_heat:
volume_info = self._create_server_volume_heat(
@ -1420,6 +1394,58 @@ class BuiltInstanceTasks(BuiltInstance, NotifyMixin, ConfigurationMixin):
datastore_status.status = rd_instance.ServiceStatuses.PAUSED
datastore_status.save()
def upgrade(self, datastore_version):
LOG.debug("Upgrading instance %s to new datastore version %s",
self, datastore_version)
def server_finished_rebuilding():
self.refresh_compute_server_info()
return not self.server_status_matches(['REBUILD'])
try:
upgrade_info = self.guest.pre_upgrade()
if self.volume_id:
volume = self.volume_client.volumes.get(self.volume_id)
volume_device = self._fix_device_path(
volume.attachments[0]['device'])
injected_files = self.get_injected_files(
datastore_version.manager)
LOG.debug("Rebuilding instance %(instance)s with image %(image)s.",
{'instance': self, 'image': datastore_version.image_id})
self.server.rebuild(datastore_version.image_id,
files=injected_files)
utils.poll_until(
server_finished_rebuilding,
sleep_time=2, time_out=600)
if not self.server_status_matches(['ACTIVE']):
raise TroveError(_("Instance %(instance)s failed to "
"upgrade to %(datastore_version)s")
% {'instance': self,
'datastore_version': datastore_version})
if volume:
upgrade_info['device'] = volume_device
self.guest.post_upgrade(upgrade_info)
self.reset_task_status()
except Exception as e:
LOG.exception(e)
err = inst_models.InstanceTasks.BUILDING_ERROR_SERVER
self.update_db(task_status=err)
raise e
# Some cinder drivers appear to return "vdb" instead of "/dev/vdb".
# We need to account for that.
def _fix_device_path(self, device):
if device.startswith("/dev"):
return device
else:
return "/dev/%s" % device
class BackupTasks(object):
@classmethod

View File

@ -17,6 +17,7 @@ from novaclient import exceptions as nova_exceptions
from oslo_log import log as logging
from trove.common.exception import PollTimeOut
from trove.common.i18n import _
from trove.common import instance as rd_instance
from trove.tests.fakes.common import authorize

View File

@ -42,6 +42,7 @@ from trove.tests.scenario.groups import instance_actions_group
from trove.tests.scenario.groups import instance_create_group
from trove.tests.scenario.groups import instance_delete_group
from trove.tests.scenario.groups import instance_error_create_group
from trove.tests.scenario.groups import instance_upgrade_group
from trove.tests.scenario.groups import module_group
from trove.tests.scenario.groups import negative_cluster_actions_group
from trove.tests.scenario.groups import replication_group
@ -146,6 +147,9 @@ instance_create_groups.extend([instance_create_group.GROUP,
instance_error_create_groups = list(base_groups)
instance_error_create_groups.extend([instance_error_create_group.GROUP])
instance_upgrade_groups = list(instance_create_groups)
instance_upgrade_groups.extend([instance_upgrade_group.GROUP])
backup_groups = list(instance_create_groups)
backup_groups.extend([groups.BACKUP,
groups.BACKUP_INST])
@ -204,6 +208,7 @@ register(["guest_log"], guest_log_groups)
register(["instance", "instance_actions"], instance_actions_groups)
register(["instance_create"], instance_create_groups)
register(["instance_error_create"], instance_error_create_groups)
register(["instance_upgrade"], instance_upgrade_groups)
register(["module"], module_groups)
register(["module_create"], module_create_groups)
register(["replication"], replication_groups)
@ -228,8 +233,8 @@ register(["postgresql_supported"], common_groups,
backup_incremental_groups, replication_groups)
register(["mysql_supported", "percona_supported"], common_groups,
backup_groups, configuration_groups, database_actions_groups,
replication_promote_groups, root_actions_groups, user_actions_groups,
backup_incremental_groups)
replication_promote_groups, instance_upgrade_groups,
root_actions_groups, user_actions_groups, backup_incremental_groups)
register(["mariadb_supported"], common_groups,
backup_groups, cluster_actions_groups, configuration_groups,
database_actions_groups, replication_promote_groups,

View File

@ -64,6 +64,10 @@ INST_ACTIONS_RESIZE = "scenario.inst_actions_resize_grp"
INST_ACTIONS_RESIZE_WAIT = "scenario.inst_actions_resize_wait_grp"
# Instance Upgrade Group
INST_UPGRADE = "scenario.inst_upgrade_grp"
# Instance Create Group
INST_CREATE = "scenario.inst_create_grp"
INST_CREATE_WAIT = "scenario.inst_create_wait_grp"

View File

@ -235,9 +235,11 @@ class ConfigurationInstCreateGroup(TestGroup):
groups=[GROUP, groups.CFGGRP_INST,
groups.CFGGRP_INST_CREATE_WAIT],
runs_after_groups=[groups.INST_ACTIONS,
groups.INST_UPGRADE,
groups.MODULE_INST_CREATE_WAIT])
class ConfigurationInstCreateWaitGroup(TestGroup):
"""Test that Instance Configuration Group Create Completes."""
def __init__(self):
super(ConfigurationInstCreateWaitGroup, self).__init__(
ConfigurationRunnerFactory.instance())

View File

@ -54,6 +54,7 @@ class InstanceActionsGroup(TestGroup):
@test(depends_on_groups=[groups.INST_CREATE_WAIT],
groups=[GROUP, groups.INST_ACTIONS_RESIZE],
runs_after_groups=[groups.INST_ACTIONS,
groups.INST_UPGRADE,
groups.MODULE_INST_CREATE_WAIT,
groups.CFGGRP_INST_CREATE_WAIT,
groups.BACKUP_CREATE,

View File

@ -33,6 +33,7 @@ class InstanceDeleteRunnerFactory(test_runners.RunnerFactory):
groups=[GROUP, groups.INST_DELETE],
runs_after_groups=[groups.INST_INIT_DELETE,
groups.INST_ACTIONS,
groups.INST_UPGRADE,
groups.INST_ACTIONS_RESIZE_WAIT,
groups.BACKUP_INST_DELETE,
groups.BACKUP_INC_INST_DELETE,

View File

@ -0,0 +1,92 @@
# Copyright 2015 Tesora 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 proboscis import test
from trove.tests.scenario import groups
from trove.tests.scenario.groups.test_group import TestGroup
from trove.tests.scenario.runners import test_runners
GROUP = "scenario.instance_upgrade_group"
class InstanceUpgradeRunnerFactory(test_runners.RunnerFactory):
_runner_ns = 'instance_upgrade_runners'
_runner_cls = 'InstanceUpgradeRunner'
class UserActionsRunnerFactory(test_runners.RunnerFactory):
_runner_ns = 'user_actions_runners'
_runner_cls = 'UserActionsRunner'
class DatabaseActionsRunnerFactory(test_runners.RunnerFactory):
_runner_ns = 'database_actions_runners'
_runner_cls = 'DatabaseActionsRunner'
@test(depends_on_groups=[groups.INST_CREATE_WAIT],
groups=[GROUP, groups.INST_UPGRADE],
runs_after_groups=[groups.INST_ACTIONS])
class InstanceUpgradeGroup(TestGroup):
def __init__(self):
super(InstanceUpgradeGroup, self).__init__(
InstanceUpgradeRunnerFactory.instance())
self.database_actions_runner = DatabaseActionsRunnerFactory.instance()
self.user_actions_runner = UserActionsRunnerFactory.instance()
@test
def create_user_databases(self):
"""Create user databases on an existing instance."""
# These databases may be referenced by the users (below) so we need to
# create them first.
self.database_actions_runner.run_databases_create()
@test(runs_after=[create_user_databases])
def create_users(self):
"""Create users on an existing instance."""
self.user_actions_runner.run_users_create()
@test(runs_after=[create_users])
def instance_upgrade(self):
"""Upgrade an existing instance."""
self.test_runner.run_instance_upgrade()
@test(depends_on=[instance_upgrade])
def show_user(self):
"""Show created users."""
self.user_actions_runner.run_user_show()
@test(depends_on=[create_users],
runs_after=[show_user])
def list_users(self):
"""List the created users."""
self.user_actions_runner.run_users_list()
@test(depends_on=[create_users],
runs_after=[list_users])
def delete_user(self):
"""Delete the created users."""
self.user_actions_runner.run_user_delete()
@test(depends_on=[create_user_databases], runs_after=[delete_user])
def delete_user_databases(self):
"""Delete the user databases."""
self.database_actions_runner.run_database_delete()

View File

@ -374,7 +374,7 @@ class ModuleInstCreateGroup(TestGroup):
@test(depends_on_groups=[groups.MODULE_INST_CREATE],
groups=[GROUP, groups.MODULE_INST, groups.MODULE_INST_CREATE_WAIT],
runs_after_groups=[groups.INST_ACTIONS])
runs_after_groups=[groups.INST_ACTIONS, groups.INST_UPGRADE])
class ModuleInstCreateWaitGroup(TestGroup):
"""Test that Module Instance Create Completes."""

View File

@ -0,0 +1,33 @@
# Copyright 2015 Tesora 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.tests.scenario.runners.test_runners import TestRunner
class InstanceUpgradeRunner(TestRunner):
def __init__(self):
super(InstanceUpgradeRunner, self).__init__()
def run_instance_upgrade(
self, expected_states=['UPGRADE', 'ACTIVE'],
expected_http_code=202):
instance_id = self.instance_info.id
self.report.log("Testing upgrade on instance: %s" % instance_id)
target_version = self.instance_info.dbaas_datastore_version
self.auth_client.instances.upgrade(instance_id, target_version)
self.assert_instance_action(instance_id, expected_states,
expected_http_code)

View File

@ -1548,10 +1548,12 @@ class MySqlAppMockTest(trove_testtools.TestCase):
utils.execute_with_timeout = self.orig_utils_execute_with_timeout
super(MySqlAppMockTest, self).tearDown()
@patch('trove.guestagent.common.configuration.ConfigurationManager'
'.refresh_cache')
@patch.object(mysql_common_service, 'clear_expired_password')
@patch.object(utils, 'generate_random_password',
return_value='some_password')
def test_secure_keep_root(self, auth_pwd_mock, clear_pwd_mock):
def test_secure_keep_root(self, auth_pwd_mock, clear_pwd_mock, _):
with patch.object(self.mock_client,
'execute', return_value=None) as mock_execute:
utils.execute_with_timeout = MagicMock(return_value=None)
@ -1569,10 +1571,12 @@ class MySqlAppMockTest(trove_testtools.TestCase):
app._reset_configuration.assert_has_calls(reset_config_calls)
self.assertTrue(mock_execute.called)
@patch('trove.guestagent.common.configuration.ConfigurationManager'
'.refresh_cache')
@patch.object(mysql_common_service, 'clear_expired_password')
@patch.object(mysql_common_service.BaseMySqlApp,
'get_auth_password', return_value='some_password')
def test_secure_with_mycnf_error(self, auth_pwd_mock, clear_pwd_mock):
def test_secure_with_mycnf_error(self, *args):
with patch.object(self.mock_client,
'execute', return_value=None) as mock_execute:
with patch.object(operating_system, 'service_discovery',

View File

@ -250,6 +250,78 @@ class CreateInstanceTest(trove_testtools.TestCase):
self.assertIsNotNone(instance)
class TestInstanceUpgrade(trove_testtools.TestCase):
def setUp(self):
self.context = trove_testtools.TroveTestContext(self, is_admin=True)
util.init_db()
self.datastore = datastore_models.DBDatastore.create(
id=str(uuid.uuid4()),
name='test' + str(uuid.uuid4()),
default_version_id=str(uuid.uuid4()))
self.datastore_version1 = datastore_models.DBDatastoreVersion.create(
id=self.datastore.default_version_id,
name='name' + str(uuid.uuid4()),
image_id='old_image',
packages=str(uuid.uuid4()),
datastore_id=self.datastore.id,
manager='test',
active=1)
self.datastore_version2 = datastore_models.DBDatastoreVersion.create(
id=str(uuid.uuid4()),
name='name' + str(uuid.uuid4()),
image_id='new_image',
packages=str(uuid.uuid4()),
datastore_id=self.datastore.id,
manager='test',
active=1)
self.safe_nova_client = models.create_nova_client
models.create_nova_client = nova.fake_create_nova_client
super(TestInstanceUpgrade, self).setUp()
def tearDown(self):
self.datastore.delete()
self.datastore_version1.delete()
self.datastore_version2.delete()
models.create_nova_client = self.safe_nova_client
super(TestInstanceUpgrade, self).tearDown()
@patch.object(task_api.API, 'get_client', Mock(return_value=Mock()))
@patch.object(task_api.API, 'upgrade')
def test_upgrade(self, task_upgrade):
instance_model = DBInstance(
InstanceTasks.NONE,
id=str(uuid.uuid4()),
name="TestUpgradeInstance",
datastore_version_id=self.datastore_version1.id)
instance_model.set_task_status(InstanceTasks.NONE)
instance_model.save()
instance_status = InstanceServiceStatus(
ServiceStatuses.RUNNING,
id=str(uuid.uuid4()),
instance_id=instance_model.id)
instance_status.save()
self.assertIsNotNone(instance_model)
instance = models.load_instance(models.Instance, self.context,
instance_model.id)
try:
instance.upgrade(self.datastore_version2)
self.assertEqual(self.datastore_version2.id,
instance.db_info.datastore_version_id)
self.assertEqual(InstanceTasks.UPGRADING,
instance.db_info.task_status)
self.assertTrue(task_upgrade.called)
finally:
instance_status.delete()
instance_model.delete()
class TestReplication(trove_testtools.TestCase):
def setUp(self):

View File

@ -122,6 +122,14 @@ class ApiTest(trove_testtools.TestCase):
('Could not transform %s' % flavor),
self.api._transform_obj, flavor)
def test_upgrade(self):
self.api.upgrade('some-instance-id', 'some-datastore-version')
self._verify_rpc_prepare_before_cast()
self._verify_cast('upgrade',
instance_id='some-instance-id',
datastore_version_id='some-datastore-version')
class TestAPI(trove_testtools.TestCase):

View File

@ -18,6 +18,7 @@ import uuid
from cinderclient import exceptions as cinder_exceptions
import cinderclient.v2.client as cinderclient
from cinderclient.v2 import volumes as cinderclient_volumes
from mock import Mock, MagicMock, patch, PropertyMock, call
from novaclient import exceptions as nova_exceptions
import novaclient.v2.flavors
@ -217,6 +218,9 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
self.task_models_conf_patch = patch('trove.taskmanager.models.CONF')
self.task_models_conf_mock = self.task_models_conf_patch.start()
self.addCleanup(self.task_models_conf_patch.stop)
self.inst_models_conf_patch = patch('trove.instance.models.CONF')
self.inst_models_conf_mock = self.inst_models_conf_patch.start()
self.addCleanup(self.inst_models_conf_patch.stop)
def tearDown(self):
super(FreshInstanceTasksTest, self).tearDown()
@ -252,9 +256,9 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
else:
return ''
self.task_models_conf_mock.get.side_effect = fake_conf_getter
self.inst_models_conf_mock.get.side_effect = fake_conf_getter
# execute
files = self.freshinstancetasks._get_injected_files("test")
files = self.freshinstancetasks.get_injected_files("test")
# verify
self.assertTrue(
'/etc/trove/conf.d/guest_info.conf' in files)
@ -275,9 +279,9 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
else:
return ''
self.task_models_conf_mock.get.side_effect = fake_conf_getter
self.inst_models_conf_mock.get.side_effect = fake_conf_getter
# execute
files = self.freshinstancetasks._get_injected_files("test")
files = self.freshinstancetasks.get_injected_files("test")
# verify
self.assertTrue(
'/etc/guest_info' in files)
@ -396,7 +400,7 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
@patch.object(BaseInstance, 'update_db')
@patch.object(backup_models.Backup, 'get_by_id')
@patch.object(taskmanager_models.FreshInstanceTasks, 'report_root_enabled')
@patch.object(taskmanager_models.FreshInstanceTasks, '_get_injected_files')
@patch.object(taskmanager_models.FreshInstanceTasks, 'get_injected_files')
@patch.object(taskmanager_models.FreshInstanceTasks, '_create_secgroup')
@patch.object(taskmanager_models.FreshInstanceTasks, '_build_volume_info')
@patch.object(taskmanager_models.FreshInstanceTasks, '_create_server')
@ -417,7 +421,7 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
@patch.object(BaseInstance, 'update_db')
@patch.object(taskmanager_models.FreshInstanceTasks, '_create_dns_entry')
@patch.object(taskmanager_models.FreshInstanceTasks, '_get_injected_files')
@patch.object(taskmanager_models.FreshInstanceTasks, 'get_injected_files')
@patch.object(taskmanager_models.FreshInstanceTasks, '_create_server')
@patch.object(taskmanager_models.FreshInstanceTasks, '_create_secgroup')
@patch.object(taskmanager_models.FreshInstanceTasks, '_build_volume_info')
@ -691,6 +695,10 @@ class BuiltInstanceTasksTest(trove_testtools.TestCase):
True)
stub_flavor_manager.get = MagicMock(return_value=nova_flavor)
self.instance_task._volume_client = MagicMock(spec=cinderclient)
self.instance_task._volume_client.volumes = Mock(
spec=cinderclient_volumes.VolumeManager)
answers = (status for status in
self.get_inst_service_status('inst_stat-id',
[ServiceStatuses.SHUTDOWN,
@ -917,6 +925,36 @@ class BuiltInstanceTasksTest(trove_testtools.TestCase):
self.instance_task.demote_replication_master()
self.instance_task._guest.demote_replication_master.assert_any_call()
@patch.multiple(taskmanager_models.BuiltInstanceTasks,
get_injected_files=Mock(return_value="the-files"))
def test_upgrade(self, *args):
pre_rebuild_server = self.instance_task.server
dsv = Mock(image_id='foo_image')
mock_volume = Mock(attachments=[{'device': '/dev/mock_dev'}])
with patch.object(self.instance_task._volume_client.volumes, "get",
Mock(return_value=mock_volume)):
mock_server = Mock(status='ACTIVE')
with patch.object(self.instance_task._nova_client.servers,
'get', Mock(return_value=mock_server)):
with patch.multiple(self.instance_task._guest,
pre_upgrade=Mock(return_value={}),
post_upgrade=Mock()):
self.instance_task.upgrade(dsv)
self.instance_task._guest.pre_upgrade.assert_called_with()
pre_rebuild_server.rebuild.assert_called_with(
dsv.image_id, files="the-files")
self.instance_task._guest.post_upgrade.assert_called_with(
mock_volume.attachments[0])
def test_fix_device_path(self):
self.assertEqual("/dev/vdb", self.instance_task.
_fix_device_path("vdb"))
self.assertEqual("/dev/dev", self.instance_task.
_fix_device_path("dev"))
self.assertEqual("/dev/vdb/dev", self.instance_task.
_fix_device_path("vdb/dev"))
class BackupTasksTest(trove_testtools.TestCase):