ironic/ironic/tests/unit/cmd/test_dbsync.py
Grzegorz Grasza 54efd31239 Add new dbsync command with first online data migration
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>
2017-08-02 13:33:37 -04:00

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)