summaryrefslogtreecommitdiff
path: root/keystone/tests/unit/test_sql_banned_operations.py
blob: 2a9be1029d41b7e0b3ff301f65a656109bd2c656 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# Copyright 2016 Intel Corporation
#
# 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 os

import fixtures
from migrate.versioning import api as versioning_api
from migrate.versioning import repository
from oslo_db.sqlalchemy import enginefacade
from oslo_db.sqlalchemy import test_fixtures as db_fixtures
from oslo_db.sqlalchemy import test_migrations
from oslotest import base as test_base
import sqlalchemy
import testtools

from keystone.common.sql.legacy_migrations import contract_repo
from keystone.common.sql.legacy_migrations import data_migration_repo
from keystone.common.sql.legacy_migrations import expand_repo
from keystone.common.sql import upgrades


class DBOperationNotAllowed(Exception):
    pass


class BannedDBSchemaOperations(fixtures.Fixture):
    """Ban some operations for migrations."""

    def __init__(self, banned_ops, migration_repo):
        super().__init__()
        self._banned_ops = banned_ops or {}
        self._migration_repo = migration_repo

    @staticmethod
    def _explode(resource_op, repo):
        # Extract the repo name prior to the trailing '/__init__.py'
        repo_name = repo.split('/')[-2]
        raise DBOperationNotAllowed(
            'Operation %s() is not allowed in %s database migrations' % (
                resource_op, repo_name))

    def setUp(self):
        super().setUp()
        explode_lambda = {
            'Table.create': lambda *a, **k: self._explode(
                'Table.create', self._migration_repo),
            'Table.alter': lambda *a, **k: self._explode(
                'Table.alter', self._migration_repo),
            'Table.drop': lambda *a, **k: self._explode(
                'Table.drop', self._migration_repo),
            'Table.insert': lambda *a, **k: self._explode(
                'Table.insert', self._migration_repo),
            'Table.update': lambda *a, **k: self._explode(
                'Table.update', self._migration_repo),
            'Table.delete': lambda *a, **k: self._explode(
                'Table.delete', self._migration_repo),
            'Column.create': lambda *a, **k: self._explode(
                'Column.create', self._migration_repo),
            'Column.alter': lambda *a, **k: self._explode(
                'Column.alter', self._migration_repo),
            'Column.drop': lambda *a, **k: self._explode(
                'Column.drop', self._migration_repo)
        }
        for resource in self._banned_ops:
            for op in self._banned_ops[resource]:
                resource_op = '%(resource)s.%(op)s' % {
                    'resource': resource, 'op': op}
                self.useFixture(fixtures.MonkeyPatch(
                    'sqlalchemy.%s' % resource_op,
                    explode_lambda[resource_op]))


class TestBannedDBSchemaOperations(testtools.TestCase):
    """Test the BannedDBSchemaOperations fixture."""

    def test_column(self):
        """Test column operations raise DBOperationNotAllowed."""
        column = sqlalchemy.Column()
        with BannedDBSchemaOperations(
            banned_ops={'Column': ['create', 'alter', 'drop']},
            migration_repo=expand_repo.__file__,
        ):
            self.assertRaises(DBOperationNotAllowed, column.drop)
            self.assertRaises(DBOperationNotAllowed, column.alter)
            self.assertRaises(DBOperationNotAllowed, column.create)

    def test_table(self):
        """Test table operations raise DBOperationNotAllowed."""
        table = sqlalchemy.Table()
        with BannedDBSchemaOperations(
            banned_ops={'Table': ['create', 'alter', 'drop',
                                  'insert', 'update', 'delete']},
            migration_repo=expand_repo.__file__,
        ):
            self.assertRaises(DBOperationNotAllowed, table.drop)
            self.assertRaises(DBOperationNotAllowed, table.alter)
            self.assertRaises(DBOperationNotAllowed, table.create)
            self.assertRaises(DBOperationNotAllowed, table.insert)
            self.assertRaises(DBOperationNotAllowed, table.update)
            self.assertRaises(DBOperationNotAllowed, table.delete)


class KeystoneMigrationsCheckers(test_migrations.WalkVersionsMixin):
    """Walk over and test all sqlalchemy-migrate migrations."""

    migrate_file = None
    first_version = 1
    # A mapping of entity (Table, Column, ...) to operation
    banned_ops = {}
    exceptions = [
        # NOTE(xek): Reviewers: DO NOT ALLOW THINGS TO BE ADDED HERE UNLESS
        # JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT CAUSE
        # PROBLEMS FOR ROLLING UPGRADES.
    ]

    @property
    def INIT_VERSION(self):
        return upgrades.INITIAL_VERSION

    @property
    def REPOSITORY(self):
        return repository.Repository(
            os.path.abspath(os.path.dirname(self.migrate_file))
        )

    @property
    def migration_api(self):
        temp = __import__('oslo_db.sqlalchemy.migration', globals(),
                          locals(), ['versioning_api'], 0)
        return temp.versioning_api

    @property
    def migrate_engine(self):
        return self.engine

    def migrate_fully(self, repo_name):
        abs_path = os.path.abspath(os.path.dirname(repo_name))
        init_version = upgrades.get_init_version(abs_path=abs_path)
        schema = versioning_api.ControlledSchema.create(
            self.migrate_engine, abs_path, init_version)
        max_version = schema.repository.version().version
        upgrade = True
        err = ''
        version = versioning_api._migrate_version(
            schema, max_version, upgrade, err)
        schema.upgrade(version)

    def migrate_up(self, version, with_data=False):
        """Check that migrations don't cause downtime.

        Schema migrations can be done online, allowing for rolling upgrades.
        """
        # NOTE(xek):
        # self.exceptions contains a list of migrations where we allow the
        # banned operations. Only Migrations which don't cause
        # incompatibilities are allowed, for example dropping an index or
        # constraint.
        #
        # Please follow the guidelines outlined at:
        # https://docs.openstack.org/keystone/latest/contributor/database-migrations.html

        if version >= self.first_version and version not in self.exceptions:
            banned_ops = self.banned_ops
        else:
            banned_ops = None
        with BannedDBSchemaOperations(banned_ops, self.migrate_file):
            super().migrate_up(version, with_data)

    snake_walk = False
    downgrade = False

    def test_walk_versions(self):
        self.walk_versions(self.snake_walk, self.downgrade)


class TestKeystoneExpandSchemaMigrations(KeystoneMigrationsCheckers):

    migrate_file = expand_repo.__file__
    first_version = 1
    # TODO(henry-nash): we should include Table update here as well, but this
    # causes the update of the migration version to appear as a banned
    # operation!
    banned_ops = {'Table': ['alter', 'drop', 'insert', 'delete'],
                  'Column': ['alter', 'drop']}
    exceptions = [
        # NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
        # HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
        # CAUSE PROBLEMS FOR ROLLING UPGRADES.

        # Migration 002 changes the column type, from datetime to timestamp in
        # the contract phase. Adding exception here to pass expand banned
        # tests, otherwise fails.
        2,
        # NOTE(lbragstad): The expand 003 migration alters the credential table
        # to make `blob` nullable. This allows the triggers added in 003 to
        # catch writes when the `blob` attribute isn't populated. We do this so
        # that the triggers aren't aware of the encryption implementation.
        3,
        # Migration 004 changes the password created_at column type, from
        # timestamp to datetime and updates the initial value in the contract
        # phase. Adding an exception here to pass expand banned tests,
        # otherwise fails.
        4,

        # Migration 79 changes a varchar column length, doesn't
        # convert the data within that column/table and doesn't rebuild
        # indexes.
        79
    ]

    def setUp(self):
        super(TestKeystoneExpandSchemaMigrations, self).setUp()


class TestKeystoneExpandSchemaMigrationsMySQL(
        db_fixtures.OpportunisticDBTestMixin,
        test_base.BaseTestCase,
        TestKeystoneExpandSchemaMigrations):
    FIXTURE = db_fixtures.MySQLOpportunisticFixture

    def setUp(self):
        super(TestKeystoneExpandSchemaMigrationsMySQL, self).setUp()
        self.engine = enginefacade.writer.get_engine()
        self.sessionmaker = enginefacade.writer.get_sessionmaker()


class TestKeystoneExpandSchemaMigrationsPostgreSQL(
        db_fixtures.OpportunisticDBTestMixin,
        test_base.BaseTestCase,
        TestKeystoneExpandSchemaMigrations):
    FIXTURE = db_fixtures.PostgresqlOpportunisticFixture

    def setUp(self):
        super(TestKeystoneExpandSchemaMigrationsPostgreSQL, self).setUp()
        self.engine = enginefacade.writer.get_engine()
        self.sessionmaker = enginefacade.writer.get_sessionmaker()


class TestKeystoneDataMigrations(
        KeystoneMigrationsCheckers):

    migrate_file = data_migration_repo.__file__
    first_version = 1
    banned_ops = {'Table': ['create', 'alter', 'drop'],
                  'Column': ['create', 'alter', 'drop']}
    exceptions = [
        # NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
        # HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
        # CAUSE PROBLEMS FOR ROLLING UPGRADES.

        # Migration 002 changes the column type, from datetime to timestamp in
        # the contract phase. Adding exception here to pass banned data
        # migration tests. Fails otherwise.
        2,
        # Migration 004 changes the password created_at column type, from
        # timestamp to datetime and updates the initial value in the contract
        # phase. Adding an exception here to pass data migrations banned tests,
        # otherwise fails.
        4
    ]

    def setUp(self):
        super(TestKeystoneDataMigrations, self).setUp()
        self.migrate_fully(expand_repo.__file__)


class TestKeystoneDataMigrationsMySQL(
        TestKeystoneDataMigrations,
        db_fixtures.OpportunisticDBTestMixin):
    FIXTURE = db_fixtures.MySQLOpportunisticFixture


class TestKeystoneDataMigrationsPostgreSQL(
        TestKeystoneDataMigrations,
        db_fixtures.OpportunisticDBTestMixin):
    FIXTURE = db_fixtures.PostgresqlOpportunisticFixture


class TestKeystoneDataMigrationsSQLite(
        TestKeystoneDataMigrations,
        db_fixtures.OpportunisticDBTestMixin):
    pass


class TestKeystoneContractSchemaMigrations(
        KeystoneMigrationsCheckers):

    migrate_file = contract_repo.__file__
    first_version = 1
    # TODO(henry-nash): we should include Table update here as well, but this
    # causes the update of the migration version to appear as a banned
    # operation!
    banned_ops = {'Table': ['create', 'insert', 'delete'],
                  'Column': ['create']}
    exceptions = [
        # NOTE(xek, henry-nash): Reviewers: DO NOT ALLOW THINGS TO BE ADDED
        # HERE UNLESS JUSTIFICATION CAN BE PROVIDED AS TO WHY THIS WILL NOT
        # CAUSE PROBLEMS FOR ROLLING UPGRADES.

        # Migration 002 changes the column type, from datetime to timestamp.
        # To do this, the column is first dropped and recreated. This should
        # not have any negative impact on a rolling upgrade deployment.
        2,
        # Migration 004 changes the password created_at column type, from
        # timestamp to datetime and updates the created_at value. This is
        # likely not going to impact a rolling upgrade as the contract repo is
        # executed once the code has been updated; thus the created_at column
        # would be populated for any password changes. That being said, there
        # could be a performance issue for existing large password tables, as
        # the migration is not batched. However, it's a compromise and not
        # likely going to be a problem for operators.
        4,
        # Migration 013 updates a foreign key constraint at the federated_user
        # table. It is a composite key pointing to the procotol.id and
        # protocol.idp_id columns. Since we can't create a new foreign key
        # before dropping the old one and the operations happens in the same
        # upgrade phase, adding an exception here to pass the contract
        # banned tests.
        13
    ]

    def setUp(self):
        super(TestKeystoneContractSchemaMigrations, self).setUp()
        self.migrate_fully(expand_repo.__file__)
        self.migrate_fully(data_migration_repo.__file__)


class TestKeystoneContractSchemaMigrationsMySQL(
        TestKeystoneContractSchemaMigrations,
        db_fixtures.OpportunisticDBTestMixin):
    FIXTURE = db_fixtures.MySQLOpportunisticFixture


class TestKeystoneContractSchemaMigrationsPostgreSQL(
        TestKeystoneContractSchemaMigrations,
        db_fixtures.OpportunisticDBTestMixin):
    FIXTURE = db_fixtures.PostgresqlOpportunisticFixture


class TestKeystoneContractSchemaMigrationsSQLite(
        TestKeystoneContractSchemaMigrations,
        db_fixtures.OpportunisticDBTestMixin):
    # In Sqlite an alter will appear as a create, so if we check for creates
    # we will get false positives.
    def setUp(self):
        super(TestKeystoneContractSchemaMigrationsSQLite, self).setUp()
        self.banned_ops['Table'].remove('create')