~ubuntu-branches/ubuntu/trusty/cinder/trusty

« back to all changes in this revision

Viewing changes to cinder/tests/test_migrations.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2012-05-22 09:57:46 UTC
  • Revision ID: package-import@ubuntu.com-20120522095746-9lm71yvzltjybk4b
Tags: upstream-2012.2~f1~20120503.2
ImportĀ upstreamĀ versionĀ 2012.2~f1~20120503.2

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright 2010-2011 OpenStack, LLC
 
4
# All Rights Reserved.
 
5
#
 
6
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
7
#    not use this file except in compliance with the License. You may obtain
 
8
#    a copy of the License at
 
9
#
 
10
#         http://www.apache.org/licenses/LICENSE-2.0
 
11
#
 
12
#    Unless required by applicable law or agreed to in writing, software
 
13
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
14
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
15
#    License for the specific language governing permissions and limitations
 
16
#    under the License.
 
17
 
 
18
"""
 
19
Tests for database migrations. This test case reads the configuration
 
20
file test_migrations.conf for database connection settings
 
21
to use in the tests. For each connection found in the config file,
 
22
the test case runs a series of test cases to ensure that migrations work
 
23
properly both upgrading and downgrading, and that no data loss occurs
 
24
if possible.
 
25
"""
 
26
 
 
27
import ConfigParser
 
28
import commands
 
29
import os
 
30
import unittest
 
31
import urlparse
 
32
 
 
33
from migrate.versioning import repository
 
34
import sqlalchemy
 
35
 
 
36
import cinder.db.sqlalchemy.migrate_repo
 
37
from cinder.db.sqlalchemy.migration import versioning_api as migration_api
 
38
from cinder import log as logging
 
39
from cinder import test
 
40
 
 
41
LOG = logging.getLogger('cinder.tests.test_migrations')
 
42
 
 
43
 
 
44
def _mysql_get_connect_string(user="openstack_citest",
 
45
                              passwd="openstack_citest",
 
46
                              database="openstack_citest"):
 
47
    """
 
48
    Try to get a connection with a very specfic set of values, if we get
 
49
    these then we'll run the mysql tests, otherwise they are skipped
 
50
    """
 
51
    return "mysql://%(user)s:%(passwd)s@localhost/%(database)s" % locals()
 
52
 
 
53
 
 
54
def _is_mysql_avail(user="openstack_citest",
 
55
                    passwd="openstack_citest",
 
56
                    database="openstack_citest"):
 
57
    try:
 
58
        connect_uri = _mysql_get_connect_string(
 
59
            user=user, passwd=passwd, database=database)
 
60
        engine = sqlalchemy.create_engine(connect_uri)
 
61
        connection = engine.connect()
 
62
    except Exception:
 
63
        # intential catch all to handle exceptions even if we don't
 
64
        # have mysql code loaded at all.
 
65
        return False
 
66
    else:
 
67
        connection.close()
 
68
        return True
 
69
 
 
70
 
 
71
def _missing_mysql():
 
72
    if "NOVA_TEST_MYSQL_PRESENT" in os.environ:
 
73
        return True
 
74
    return not _is_mysql_avail()
 
75
 
 
76
 
 
77
class TestMigrations(test.TestCase):
 
78
    """Test sqlalchemy-migrate migrations"""
 
79
 
 
80
    TEST_DATABASES = {}
 
81
    DEFAULT_CONFIG_FILE = os.path.join(os.path.dirname(__file__),
 
82
                                       'test_migrations.conf')
 
83
    # Test machines can set the CINDER_TEST_MIGRATIONS_CONF variable
 
84
    # to override the location of the config file for migration testing
 
85
    CONFIG_FILE_PATH = os.environ.get('CINDER_TEST_MIGRATIONS_CONF',
 
86
                                      DEFAULT_CONFIG_FILE)
 
87
    MIGRATE_FILE = cinder.db.sqlalchemy.migrate_repo.__file__
 
88
    REPOSITORY = repository.Repository(
 
89
                                os.path.abspath(os.path.dirname(MIGRATE_FILE)))
 
90
 
 
91
    def setUp(self):
 
92
        super(TestMigrations, self).setUp()
 
93
 
 
94
        self.snake_walk = False
 
95
 
 
96
        # Load test databases from the config file. Only do this
 
97
        # once. No need to re-run this on each test...
 
98
        LOG.debug('config_path is %s' % TestMigrations.CONFIG_FILE_PATH)
 
99
        if not TestMigrations.TEST_DATABASES:
 
100
            if os.path.exists(TestMigrations.CONFIG_FILE_PATH):
 
101
                cp = ConfigParser.RawConfigParser()
 
102
                try:
 
103
                    cp.read(TestMigrations.CONFIG_FILE_PATH)
 
104
                    defaults = cp.defaults()
 
105
                    for key, value in defaults.items():
 
106
                        TestMigrations.TEST_DATABASES[key] = value
 
107
                    self.snake_walk = cp.getboolean('walk_style', 'snake_walk')
 
108
                except ConfigParser.ParsingError, e:
 
109
                    self.fail("Failed to read test_migrations.conf config "
 
110
                              "file. Got error: %s" % e)
 
111
            else:
 
112
                self.fail("Failed to find test_migrations.conf config "
 
113
                          "file.")
 
114
 
 
115
        self.engines = {}
 
116
        for key, value in TestMigrations.TEST_DATABASES.items():
 
117
            self.engines[key] = sqlalchemy.create_engine(value)
 
118
 
 
119
        # We start each test case with a completely blank slate.
 
120
        self._reset_databases()
 
121
 
 
122
    def tearDown(self):
 
123
 
 
124
        # We destroy the test data store between each test case,
 
125
        # and recreate it, which ensures that we have no side-effects
 
126
        # from the tests
 
127
        self._reset_databases()
 
128
 
 
129
        # remove these from the list so they aren't used in the migration tests
 
130
        if "mysqlcitest" in self.engines:
 
131
            del self.engines["mysqlcitest"]
 
132
        if "mysqlcitest" in TestMigrations.TEST_DATABASES:
 
133
            del TestMigrations.TEST_DATABASES["mysqlcitest"]
 
134
        super(TestMigrations, self).tearDown()
 
135
 
 
136
    def _reset_databases(self):
 
137
        def execute_cmd(cmd=None):
 
138
            status, output = commands.getstatusoutput(cmd)
 
139
            LOG.debug(output)
 
140
            self.assertEqual(0, status)
 
141
        for key, engine in self.engines.items():
 
142
            conn_string = TestMigrations.TEST_DATABASES[key]
 
143
            conn_pieces = urlparse.urlparse(conn_string)
 
144
            if conn_string.startswith('sqlite'):
 
145
                # We can just delete the SQLite database, which is
 
146
                # the easiest and cleanest solution
 
147
                db_path = conn_pieces.path.strip('/')
 
148
                if os.path.exists(db_path):
 
149
                    os.unlink(db_path)
 
150
                # No need to recreate the SQLite DB. SQLite will
 
151
                # create it for us if it's not there...
 
152
            elif conn_string.startswith('mysql'):
 
153
                # We can execute the MySQL client to destroy and re-create
 
154
                # the MYSQL database, which is easier and less error-prone
 
155
                # than using SQLAlchemy to do this via MetaData...trust me.
 
156
                database = conn_pieces.path.strip('/')
 
157
                loc_pieces = conn_pieces.netloc.split('@')
 
158
                host = loc_pieces[1]
 
159
                auth_pieces = loc_pieces[0].split(':')
 
160
                user = auth_pieces[0]
 
161
                password = ""
 
162
                if len(auth_pieces) > 1:
 
163
                    if auth_pieces[1].strip():
 
164
                        password = "-p\"%s\"" % auth_pieces[1]
 
165
                sql = ("drop database if exists %(database)s; "
 
166
                       "create database %(database)s;") % locals()
 
167
                cmd = ("mysql -u \"%(user)s\" %(password)s -h %(host)s "
 
168
                       "-e \"%(sql)s\"") % locals()
 
169
                execute_cmd(cmd)
 
170
            elif conn_string.startswith('postgresql'):
 
171
                database = conn_pieces.path.strip('/')
 
172
                loc_pieces = conn_pieces.netloc.split('@')
 
173
                host = loc_pieces[1]
 
174
                auth_pieces = loc_pieces[0].split(':')
 
175
                user = auth_pieces[0]
 
176
                password = ""
 
177
                if len(auth_pieces) > 1:
 
178
                    if auth_pieces[1].strip():
 
179
                        password = auth_pieces[1]
 
180
                cmd = ("touch ~/.pgpass;"
 
181
                       "chmod 0600 ~/.pgpass;"
 
182
                       "sed -i -e"
 
183
                       "'1{s/^.*$/\*:\*:\*:%(user)s:%(password)s/};"
 
184
                       "1!d' ~/.pgpass") % locals()
 
185
                execute_cmd(cmd)
 
186
                sql = ("UPDATE pg_catalog.pg_database SET datallowconn=false "
 
187
                       "WHERE datname='%(database)s';") % locals()
 
188
                cmd = ("psql -U%(user)s -h%(host)s -c\"%(sql)s\"") % locals()
 
189
                execute_cmd(cmd)
 
190
                sql = ("SELECT pg_catalog.pg_terminate_backend(procpid) "
 
191
                       "FROM pg_catalog.pg_stat_activity "
 
192
                       "WHERE datname='%(database)s';") % locals()
 
193
                cmd = ("psql -U%(user)s -h%(host)s -c\"%(sql)s\"") % locals()
 
194
                execute_cmd(cmd)
 
195
                sql = ("drop database if exists %(database)s;") % locals()
 
196
                cmd = ("psql -U%(user)s -h%(host)s -c\"%(sql)s\"") % locals()
 
197
                execute_cmd(cmd)
 
198
                sql = ("create database %(database)s;") % locals()
 
199
                cmd = ("psql -U%(user)s -h%(host)s -c\"%(sql)s\"") % locals()
 
200
                execute_cmd(cmd)
 
201
 
 
202
    def test_walk_versions(self):
 
203
        """
 
204
        Walks all version scripts for each tested database, ensuring
 
205
        that there are no errors in the version scripts for each engine
 
206
        """
 
207
        for key, engine in self.engines.items():
 
208
            self._walk_versions(engine, self.snake_walk)
 
209
 
 
210
    def test_mysql_connect_fail(self):
 
211
        """
 
212
        Test that we can trigger a mysql connection failure and we fail
 
213
        gracefully to ensure we don't break people without mysql
 
214
        """
 
215
        if _is_mysql_avail(user="openstack_cifail"):
 
216
            self.fail("Shouldn't have connected")
 
217
 
 
218
    @test.skip_if(_missing_mysql(), "mysql not available")
 
219
    def test_mysql_innodb(self):
 
220
        """
 
221
        Test that table creation on mysql only builds InnoDB tables
 
222
        """
 
223
        # add this to the global lists to make reset work with it, it's removed
 
224
        # automaticaly in tearDown so no need to clean it up here.
 
225
        connect_string = _mysql_get_connect_string()
 
226
        engine = sqlalchemy.create_engine(connect_string)
 
227
        self.engines["mysqlcitest"] = engine
 
228
        TestMigrations.TEST_DATABASES["mysqlcitest"] = connect_string
 
229
 
 
230
        # build a fully populated mysql database with all the tables
 
231
        self._reset_databases()
 
232
        self._walk_versions(engine, False, False)
 
233
 
 
234
        uri = self._mysql_get_connect_string(database="information_schema")
 
235
        connection = sqlalchemy.create_engine(uri).connect()
 
236
 
 
237
        # sanity check
 
238
        total = connection.execute("SELECT count(*) "
 
239
                                   "from information_schema.TABLES "
 
240
                                   "where TABLE_SCHEMA='openstack_citest'")
 
241
        self.assertTrue(total.scalar() > 0, "No tables found. Wrong schema?")
 
242
 
 
243
        noninnodb = connection.execute("SELECT count(*) "
 
244
                                       "from information_schema.TABLES "
 
245
                                       "where TABLE_SCHEMA='openstack_citest' "
 
246
                                       "and ENGINE!='InnoDB'")
 
247
        count = noninnodb.scalar()
 
248
        self.assertEqual(count, 0, "%d non InnoDB tables created" % count)
 
249
 
 
250
    def _walk_versions(self, engine=None, snake_walk=False, downgrade=True):
 
251
        # Determine latest version script from the repo, then
 
252
        # upgrade from 1 through to the latest, with no data
 
253
        # in the databases. This just checks that the schema itself
 
254
        # upgrades successfully.
 
255
 
 
256
        # Place the database under version control
 
257
        migration_api.version_control(engine, TestMigrations.REPOSITORY)
 
258
        self.assertEqual(0,
 
259
                migration_api.db_version(engine,
 
260
                                         TestMigrations.REPOSITORY))
 
261
 
 
262
        LOG.debug('latest version is %s' % TestMigrations.REPOSITORY.latest)
 
263
 
 
264
        for version in xrange(1, TestMigrations.REPOSITORY.latest + 1):
 
265
            # upgrade -> downgrade -> upgrade
 
266
            self._migrate_up(engine, version)
 
267
            if snake_walk:
 
268
                self._migrate_down(engine, version - 1)
 
269
                self._migrate_up(engine, version)
 
270
 
 
271
        if downgrade:
 
272
            # Now walk it back down to 0 from the latest, testing
 
273
            # the downgrade paths.
 
274
            for version in reversed(
 
275
                xrange(0, TestMigrations.REPOSITORY.latest)):
 
276
                # downgrade -> upgrade -> downgrade
 
277
                self._migrate_down(engine, version)
 
278
                if snake_walk:
 
279
                    self._migrate_up(engine, version + 1)
 
280
                    self._migrate_down(engine, version)
 
281
 
 
282
    def _migrate_down(self, engine, version):
 
283
        migration_api.downgrade(engine,
 
284
                                TestMigrations.REPOSITORY,
 
285
                                version)
 
286
        self.assertEqual(version,
 
287
                         migration_api.db_version(engine,
 
288
                                                  TestMigrations.REPOSITORY))
 
289
 
 
290
    def _migrate_up(self, engine, version):
 
291
        migration_api.upgrade(engine,
 
292
                              TestMigrations.REPOSITORY,
 
293
                              version)
 
294
        self.assertEqual(version,
 
295
                migration_api.db_version(engine,
 
296
                                         TestMigrations.REPOSITORY))