]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
add HEAD sentinel file that contains migration revision
authorMark McClain <mmcclain@yahoo-inc.com>
Fri, 7 Mar 2014 20:07:43 +0000 (15:07 -0500)
committerMark McClain <mmcclain@yahoo-inc.com>
Wed, 19 Mar 2014 16:40:29 +0000 (12:40 -0400)
This change wraps the command to generate a migration with code to
update a file called HEAD. The HEAD file will contain the revision ID
of the head of the migraton timeline. Additionally, check migrations has
been altered to verify the contents of this file against the timeline
head.

This file helps the OpenStack gate detect potential migration branches
without running alembic via git merge conflicts.

Closes-Bug:1288427
Change-Id: If382c57baea061753d3f4fcd6faec1a31fbfb7ed

neutron/db/migration/alembic_migrations/versions/HEAD [new file with mode: 0644]
neutron/db/migration/cli.py
neutron/tests/unit/test_db_migration.py
tox.ini

diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD
new file mode 100644 (file)
index 0000000..41a258e
--- /dev/null
@@ -0,0 +1 @@
+2447ad0e9585
index 3e7257c2d8dcdeb8ab6542e7623cc36cee11ca9a..5cede267cff44b0e2f70f4807e8f742bd8ecbced 100644 (file)
@@ -18,11 +18,14 @@ import os
 
 from alembic import command as alembic_command
 from alembic import config as alembic_config
+from alembic import script as alembic_script
 from alembic import util as alembic_util
 from oslo.config import cfg
 
 from neutron.common import legacy
 
+HEAD_FILENAME = 'HEAD'
+
 
 _core_opts = [
     cfg.StrOpt('core_plugin',
@@ -61,6 +64,7 @@ def do_alembic_command(config, cmd, *args, **kwargs):
 
 def do_check_migration(config, cmd):
     do_alembic_command(config, 'branches')
+    validate_head_file(config)
 
 
 def do_upgrade_downgrade(config, cmd):
@@ -89,6 +93,30 @@ def do_revision(config, cmd):
                        message=CONF.command.message,
                        autogenerate=CONF.command.autogenerate,
                        sql=CONF.command.sql)
+    update_head_file(config)
+
+
+def validate_head_file(config):
+    script = alembic_script.ScriptDirectory.from_config(config)
+    if len(script.get_heads()) > 1:
+        alembic_util.err(_('Timeline branches unable to generate timeline'))
+
+    head_path = os.path.join(script.versions, HEAD_FILENAME)
+    if (os.path.isfile(head_path) and
+        open(head_path).read().strip() == script.get_current_head()):
+        return
+    else:
+        alembic_util.err(_('HEAD file does not match migration timeline head'))
+
+
+def update_head_file(config):
+    script = alembic_script.ScriptDirectory.from_config(config)
+    if len(script.get_heads()) > 1:
+        alembic_util.err(_('Timeline branches unable to generate timeline'))
+
+    head_path = os.path.join(script.versions, HEAD_FILENAME)
+    with open(head_path, 'w+') as f:
+        f.write(script.get_current_head())
 
 
 def add_command_parsers(subparsers):
index 2c3a7928fc1ddea232f19129c9c51f5a40b8f758..be3265bea2619a96b48192ab3a71e4f521029c66 100644 (file)
@@ -40,6 +40,8 @@ class TestCli(base.BaseTestCase):
         super(TestCli, self).setUp()
         self.do_alembic_cmd_p = mock.patch.object(cli, 'do_alembic_command')
         self.do_alembic_cmd = self.do_alembic_cmd_p.start()
+        self.mock_alembic_err = mock.patch('alembic.util.err').start()
+        self.mock_alembic_err.side_effect = SystemExit
         self.addCleanup(self.do_alembic_cmd_p.stop)
         self.addCleanup(cli.CONF.reset)
 
@@ -72,22 +74,28 @@ class TestCli(base.BaseTestCase):
         self._main_test_helper(['prog', 'history'], 'history')
 
     def test_check_migration(self):
-        self._main_test_helper(['prog', 'check_migration'], 'branches')
+        with mock.patch.object(cli, 'validate_head_file') as validate:
+            self._main_test_helper(['prog', 'check_migration'], 'branches')
+            validate.assert_called_once_with(mock.ANY)
 
     def test_database_sync_revision(self):
-        self._main_test_helper(
-            ['prog', 'revision', '--autogenerate', '-m', 'message'],
-            'revision',
-            (),
-            {'message': 'message', 'sql': False, 'autogenerate': True}
-        )
-
-        self._main_test_helper(
-            ['prog', 'revision', '--sql', '-m', 'message'],
-            'revision',
-            (),
-            {'message': 'message', 'sql': True, 'autogenerate': False}
-        )
+        with mock.patch.object(cli, 'update_head_file') as update:
+            self._main_test_helper(
+                ['prog', 'revision', '--autogenerate', '-m', 'message'],
+                'revision',
+                (),
+                {'message': 'message', 'sql': False, 'autogenerate': True}
+            )
+            update.assert_called_once_with(mock.ANY)
+
+            update.reset_mock()
+            self._main_test_helper(
+                ['prog', 'revision', '--sql', '-m', 'message'],
+                'revision',
+                (),
+                {'message': 'message', 'sql': True, 'autogenerate': False}
+            )
+            update.assert_called_once_with(mock.ANY)
 
     def test_upgrade(self):
         self._main_test_helper(
@@ -118,3 +126,61 @@ class TestCli(base.BaseTestCase):
             ('-2',),
             {'sql': False}
         )
+
+    def _test_validate_head_file_helper(self, heads, file_content=None):
+        with mock.patch('alembic.script.ScriptDirectory.from_config') as fc:
+            fc.return_value.get_heads.return_value = heads
+            fc.return_value.get_current_head.return_value = heads[0]
+            with mock.patch('__builtin__.open') as mock_open:
+                mock_open.return_value.__enter__ = lambda s: s
+                mock_open.return_value.__exit__ = mock.Mock()
+                mock_open.return_value.read.return_value = file_content
+
+                with mock.patch('os.path.isfile') as is_file:
+                    is_file.return_value = file_content is not None
+
+                    if file_content in heads:
+                        cli.validate_head_file(mock.sentinel.config)
+                    else:
+                        self.assertRaises(
+                            SystemExit,
+                            cli.validate_head_file,
+                            mock.sentinel.config
+                        )
+                        self.mock_alembic_err.assert_called_once_with(mock.ANY)
+            fc.assert_called_once_with(mock.sentinel.config)
+
+    def test_validate_head_file_multiple_heads(self):
+        self._test_validate_head_file_helper(['a', 'b'])
+
+    def test_validate_head_file_missing_file(self):
+        self._test_validate_head_file_helper(['a'])
+
+    def test_validate_head_file_wrong_contents(self):
+        self._test_validate_head_file_helper(['a'], 'b')
+
+    def test_validate_head_success(self):
+        self._test_validate_head_file_helper(['a'], 'a')
+
+    def test_update_head_file_multiple_heads(self):
+        with mock.patch('alembic.script.ScriptDirectory.from_config') as fc:
+            fc.return_value.get_heads.return_value = ['a', 'b']
+            self.assertRaises(
+                SystemExit,
+                cli.update_head_file,
+                mock.sentinel.config
+            )
+            self.mock_alembic_err.assert_called_once_with(mock.ANY)
+            fc.assert_called_once_with(mock.sentinel.config)
+
+    def test_update_head_file_success(self):
+        with mock.patch('alembic.script.ScriptDirectory.from_config') as fc:
+            fc.return_value.get_heads.return_value = ['a']
+            fc.return_value.get_current_head.return_value = 'a'
+            with mock.patch('__builtin__.open') as mock_open:
+                mock_open.return_value.__enter__ = lambda s: s
+                mock_open.return_value.__exit__ = mock.Mock()
+
+                cli.update_head_file(mock.sentinel.config)
+                mock_open.write.called_once_with('a')
+            fc.assert_called_once_with(mock.sentinel.config)
diff --git a/tox.ini b/tox.ini
index cf4d950984903c1daa86faa89f1dab0accee8bc2..a5a6600d866039599d83b0a95636c1e767add161 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -25,6 +25,7 @@ downloadcache = ~/cache/pip
 [testenv:pep8]
 commands =
   flake8
+  neutron-db-manage check_migration
 
 [testenv:i18n]
 commands = python ./tools/check_i18n.py ./neutron ./tools/i18n_cfg.py