54efd31239
This adds the new command 'ironic-dbsync online_data_migrations'. To limit downtime during upgrades, data migrations will be done online with migration scripts that could be run during normal operation of an ironic cluster, after the upgrade but before the next one. Each migration script should ensure that all related DB records are migrated to the new format. Scripts can detect the format based on the object version which is stored in the version column. The online data migration has one script; a function that backfills the new version column, using versions of objects from the release prior to this. This includes code to check the object versions for compatibility with an ironic release. However, the check is turned off (and will be turned on in Queens), since we need to boot-strap the new version column before we can turn the check on. To do this check, we need to keep a list of all supported versions for every object; release_mapping.RELEASE_MAPPING was modified so that the object versions is now a list instead of one value. Change-Id: I1a9fa829951ecf98cae6896d82ba20cf89062394 Closes-Bug: #1585141 Partial-bug: #1526283 Co-Authored-By: Ruby Loo <ruby.loo@intel.com>
185 lines
8.9 KiB
Python
185 lines
8.9 KiB
Python
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
# 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 mock
|
|
|
|
from ironic.cmd import dbsync
|
|
from ironic.common import context
|
|
from ironic.db import migration
|
|
from ironic.tests.unit.db import base as db_base
|
|
|
|
|
|
class DbSyncTestCase(db_base.DbTestCase):
|
|
|
|
def test_upgrade_and_version(self):
|
|
migration.upgrade('head')
|
|
v = migration.version()
|
|
self.assertTrue(v)
|
|
|
|
|
|
class OnlineMigrationTestCase(db_base.DbTestCase):
|
|
|
|
def setUp(self):
|
|
super(OnlineMigrationTestCase, self).setUp()
|
|
self.context = context.get_admin_context()
|
|
self.db_cmds = dbsync.DBCommand()
|
|
|
|
def test__check_versions(self):
|
|
with mock.patch.object(self.dbapi, 'check_versions',
|
|
autospec=True) as mock_check_versions:
|
|
mock_check_versions.return_value = True
|
|
self.db_cmds._check_versions()
|
|
mock_check_versions.assert_called_once_with()
|
|
|
|
def test__check_versions_bad(self):
|
|
with mock.patch.object(self.dbapi, 'check_versions',
|
|
autospec=True) as mock_check_versions:
|
|
mock_check_versions.return_value = False
|
|
exit = self.assertRaises(SystemExit, self.db_cmds._check_versions)
|
|
mock_check_versions.assert_called_once_with()
|
|
self.assertEqual(2, exit.code)
|
|
|
|
@mock.patch.object(dbsync, 'ONLINE_MIGRATIONS', autospec=True)
|
|
def test__run_migration_functions(self, mock_migrations):
|
|
mock_func = mock.MagicMock(side_effect=((15, 15),), __name__='foo')
|
|
mock_migrations.__iter__.return_value = (mock_func,)
|
|
self.assertTrue(
|
|
self.db_cmds._run_migration_functions(self.context, 50))
|
|
mock_func.assert_called_once_with(self.context, 50)
|
|
|
|
@mock.patch.object(dbsync, 'ONLINE_MIGRATIONS', autospec=True)
|
|
def test__run_migration_functions_none(self, mock_migrations):
|
|
# No migration functions to run
|
|
mock_migrations.__iter__.return_value = ()
|
|
self.assertTrue(
|
|
self.db_cmds._run_migration_functions(self.context, 50))
|
|
|
|
@mock.patch.object(dbsync, 'ONLINE_MIGRATIONS', autospec=True)
|
|
def test__run_migration_functions_exception(self, mock_migrations):
|
|
# Migration function raises exception
|
|
mock_func = mock.MagicMock(side_effect=TypeError("bar"),
|
|
__name__='foo')
|
|
mock_migrations.__iter__.return_value = (mock_func,)
|
|
self.assertRaises(TypeError, self.db_cmds._run_migration_functions,
|
|
self.context, 50)
|
|
mock_func.assert_called_once_with(self.context, 50)
|
|
|
|
@mock.patch.object(dbsync, 'ONLINE_MIGRATIONS', autospec=True)
|
|
def test__run_migration_functions_2(self, mock_migrations):
|
|
# 2 migration functions, migration completed
|
|
mock_func1 = mock.MagicMock(side_effect=((15, 15),), __name__='func1')
|
|
mock_func2 = mock.MagicMock(side_effect=((20, 20),), __name__='func2')
|
|
mock_migrations.__iter__.return_value = (mock_func1, mock_func2)
|
|
self.assertTrue(
|
|
self.db_cmds._run_migration_functions(self.context, 50))
|
|
mock_func1.assert_called_once_with(self.context, 50)
|
|
mock_func2.assert_called_once_with(self.context, 35)
|
|
|
|
@mock.patch.object(dbsync, 'ONLINE_MIGRATIONS', autospec=True)
|
|
def test__run_migration_functions_2_notdone(self, mock_migrations):
|
|
# 2 migration functions; only first function was run but not completed
|
|
mock_func1 = mock.MagicMock(side_effect=((15, 10),), __name__='func1')
|
|
mock_func2 = mock.MagicMock(side_effect=((20, 0),), __name__='func2')
|
|
mock_migrations.__iter__.return_value = (mock_func1, mock_func2)
|
|
self.assertFalse(
|
|
self.db_cmds._run_migration_functions(self.context, 10))
|
|
mock_func1.assert_called_once_with(self.context, 10)
|
|
self.assertFalse(mock_func2.called)
|
|
|
|
@mock.patch.object(dbsync, 'ONLINE_MIGRATIONS', autospec=True)
|
|
def test__run_migration_functions_2_onedone(self, mock_migrations):
|
|
# 2 migration functions; only first function was run and completed
|
|
mock_func1 = mock.MagicMock(side_effect=((10, 10),), __name__='func1')
|
|
mock_func2 = mock.MagicMock(side_effect=((20, 0),), __name__='func2')
|
|
mock_migrations.__iter__.return_value = (mock_func1, mock_func2)
|
|
self.assertFalse(
|
|
self.db_cmds._run_migration_functions(self.context, 10))
|
|
mock_func1.assert_called_once_with(self.context, 10)
|
|
self.assertFalse(mock_func2.called)
|
|
|
|
@mock.patch.object(dbsync, 'ONLINE_MIGRATIONS', autospec=True)
|
|
def test__run_migration_functions_2_done(self, mock_migrations):
|
|
# 2 migration functions; migrations completed
|
|
mock_func1 = mock.MagicMock(side_effect=((10, 10),), __name__='func1')
|
|
mock_func2 = mock.MagicMock(side_effect=((0, 0),), __name__='func2')
|
|
mock_migrations.__iter__.return_value = (mock_func1, mock_func2)
|
|
self.assertTrue(
|
|
self.db_cmds._run_migration_functions(self.context, 15))
|
|
mock_func1.assert_called_once_with(self.context, 15)
|
|
mock_func2.assert_called_once_with(self.context, 5)
|
|
|
|
@mock.patch.object(dbsync, 'ONLINE_MIGRATIONS', autospec=True)
|
|
def test__run_migration_functions_two_calls_done(self, mock_migrations):
|
|
# 2 migration functions; migrations completed after calling twice
|
|
mock_func1 = mock.MagicMock(side_effect=((10, 10), (0, 0)),
|
|
__name__='func1')
|
|
mock_func2 = mock.MagicMock(side_effect=((0, 0), (0, 0)),
|
|
__name__='func2')
|
|
mock_migrations.__iter__.return_value = (mock_func1, mock_func2)
|
|
self.assertFalse(
|
|
self.db_cmds._run_migration_functions(self.context, 10))
|
|
mock_func1.assert_called_once_with(self.context, 10)
|
|
self.assertFalse(mock_func2.called)
|
|
self.assertTrue(
|
|
self.db_cmds._run_migration_functions(self.context, 10))
|
|
mock_func1.assert_has_calls((mock.call(self.context, 10),) * 2)
|
|
mock_func2.assert_called_once_with(self.context, 10)
|
|
|
|
@mock.patch.object(dbsync.DBCommand, '_run_migration_functions',
|
|
autospec=True)
|
|
def test__run_online_data_migrations(self, mock_functions):
|
|
mock_functions.return_value = True
|
|
exit = self.assertRaises(SystemExit,
|
|
self.db_cmds._run_online_data_migrations)
|
|
self.assertEqual(0, exit.code)
|
|
mock_functions.assert_called_once_with(self.db_cmds, mock.ANY, 50)
|
|
|
|
@mock.patch.object(dbsync.DBCommand, '_run_migration_functions',
|
|
autospec=True)
|
|
def test__run_online_data_migrations_batches(self, mock_functions):
|
|
mock_functions.side_effect = (False, True)
|
|
exit = self.assertRaises(SystemExit,
|
|
self.db_cmds._run_online_data_migrations)
|
|
self.assertEqual(0, exit.code)
|
|
mock_functions.assert_has_calls(
|
|
(mock.call(self.db_cmds, mock.ANY, 50),) * 2)
|
|
|
|
@mock.patch.object(dbsync.DBCommand, '_run_migration_functions',
|
|
autospec=True)
|
|
def test__run_online_data_migrations_notdone(self, mock_functions):
|
|
mock_functions.return_value = False
|
|
exit = self.assertRaises(SystemExit,
|
|
self.db_cmds._run_online_data_migrations,
|
|
max_count=30)
|
|
self.assertEqual(1, exit.code)
|
|
mock_functions.assert_called_once_with(self.db_cmds, mock.ANY, 30)
|
|
|
|
@mock.patch.object(dbsync.DBCommand, '_run_migration_functions',
|
|
autospec=True)
|
|
def test__run_online_data_migrations_max_count_neg(self, mock_functions):
|
|
mock_functions.return_value = False
|
|
exit = self.assertRaises(SystemExit,
|
|
self.db_cmds._run_online_data_migrations,
|
|
max_count=-4)
|
|
self.assertEqual(127, exit.code)
|
|
self.assertFalse(mock_functions.called)
|
|
|
|
@mock.patch.object(dbsync.DBCommand, '_run_migration_functions',
|
|
autospec=True)
|
|
def test__run_online_data_migrations_exception(self, mock_functions):
|
|
mock_functions.side_effect = TypeError("yuck")
|
|
self.assertRaises(TypeError, self.db_cmds._run_online_data_migrations)
|
|
mock_functions.assert_called_once_with(self.db_cmds, mock.ANY, 50)
|