Browse Source

Merge "Add ability to upgrade db"

changes/56/682356/1
Zuul 1 month ago
parent
commit
c7f1ef0a93

+ 1
- 0
requirements.txt View File

@@ -3,6 +3,7 @@
3 3
 # process, which may cause wedges in the gate later.
4 4
 
5 5
 pbr>=3.1.1 # Apache-2.0
6
+alembic>=0.9.8  # MIT
6 7
 Babel>=2.5.3 # BSD
7 8
 lxml>=4.1.1 # BSD
8 9
 PyMySQL>=0.8.0 # MIT License

+ 2
- 0
setup.cfg View File

@@ -32,6 +32,8 @@ console_scripts =
32 32
     vitrage-persistor = vitrage.cli.persistor:main
33 33
     vitrage-ml = vitrage.cli.machine_learning:main
34 34
     vitrage-dbsync = vitrage.cli.storage:dbsync
35
+    vitrage-dbsync-revision = vitrage.cli.storage:revision
36
+    vitrage-dbsync-stamp = vitrage.cli.storage:stamp
35 37
     vitrage-purge-data = vitrage.cli.storage:purge_data
36 38
     vitrage-snmp-parsing = vitrage.cli.snmp_parsing:main
37 39
     vitrage-status = vitrage.cli.status:main

+ 34
- 1
vitrage/cli/storage.py View File

@@ -14,15 +14,48 @@
14 14
 
15 15
 import sys
16 16
 
17
+from oslo_config import cfg
18
+
17 19
 from vitrage.cli import VITRAGE_TITLE
18 20
 from vitrage.common import config
19 21
 from vitrage import storage
22
+from vitrage.storage.sqlalchemy import migration
23
+
24
+CONF = cfg.CONF
25
+CLI_OPTS = [
26
+    cfg.StrOpt('revision',
27
+               default='head',
28
+               help='Migration version')
29
+]
30
+REVISION_OPTS = [
31
+    cfg.StrOpt('message',
32
+               help='Text that will be used for migration title'),
33
+    cfg.BoolOpt('autogenerate',
34
+                default=False,
35
+                help='Generates diff based on current database state')
36
+]
37
+
38
+
39
+def stamp():
40
+    print(VITRAGE_TITLE)
41
+    CONF.register_cli_opts(CLI_OPTS)
42
+    config.parse_config(sys.argv)
43
+
44
+    migration.stamp(CONF.revision)
45
+
46
+
47
+def revision():
48
+    print(VITRAGE_TITLE)
49
+    CONF.register_cli_opts(REVISION_OPTS)
50
+    config.parse_config(sys.argv)
51
+    migration.revision(CONF.message, CONF.autogenerate)
20 52
 
21 53
 
22 54
 def dbsync():
23 55
     print(VITRAGE_TITLE)
56
+    CONF.register_cli_opts(CLI_OPTS)
24 57
     config.parse_config(sys.argv)
25
-    storage.get_connection_from_config().upgrade()
58
+    migration.upgrade(CONF.revision)
26 59
 
27 60
 
28 61
 def purge_data():

+ 0
- 4
vitrage/storage/base.py View File

@@ -55,10 +55,6 @@ class Connection(object):
55 55
     def changes(self):
56 56
         return None
57 57
 
58
-    @abc.abstractmethod
59
-    def upgrade(self, nocreate=False):
60
-        raise NotImplementedError('upgrade is not implemented')
61
-
62 58
     @abc.abstractmethod
63 59
     def disconnect(self):
64 60
         raise NotImplementedError('disconnect is not implemented')

+ 0
- 26
vitrage/storage/impl_sqlalchemy.py View File

@@ -106,32 +106,6 @@ class Connection(base.Connection):
106 106
             return str(url)
107 107
         return url
108 108
 
109
-    def upgrade(self, nocreate=False):
110
-        engine = self._engine_facade.get_engine()
111
-        engine.connect()
112
-
113
-        # As the following tables were changed in Rocky, they are removed and
114
-        # created. This is fine for an upgrade from Queens, since data in these
115
-        # was anyway deleted in each restart.
116
-        # starting From Rocky, data in these tables should not be removed.
117
-
118
-        models.Base.metadata.drop_all(
119
-            engine, tables=[
120
-                models.ActiveAction.__table__,
121
-                models.Event.__table__,
122
-                models.GraphSnapshot.__table__])
123
-
124
-        models.Base.metadata.create_all(
125
-            engine, tables=[models.ActiveAction.__table__,
126
-                            models.Template.__table__,
127
-                            models.Webhooks.__table__,
128
-                            models.Event.__table__,
129
-                            models.GraphSnapshot.__table__,
130
-                            models.Alarm.__table__,
131
-                            models.Edge.__table__,
132
-                            models.Change.__table__])
133
-        # TODO(idan_hefetz) upgrade logic is missing
134
-
135 109
     def disconnect(self):
136 110
         self._engine_facade.get_engine().dispose()
137 111
 

+ 91
- 0
vitrage/storage/sqlalchemy/migration/__init__.py View File

@@ -0,0 +1,91 @@
1
+# Licensed under the Apache License, Version 2.0 (the "License");
2
+# you may not use this file except in compliance with the License.
3
+# You may obtain a copy of the License at
4
+#
5
+#    http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+# Unless required by applicable law or agreed to in writing, software
8
+# distributed under the License is distributed on an "AS IS" BASIS,
9
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10
+# implied.
11
+# See the License for the specific language governing permissions and
12
+# limitations under the License.
13
+
14
+import os
15
+
16
+from alembic import command as alembic_command
17
+from alembic import config as alembic_config
18
+from alembic import migration as alembic_migrations
19
+from oslo_config import cfg
20
+from oslo_db import exception as db_exc
21
+from oslo_db.sqlalchemy import enginefacade
22
+
23
+from vitrage.storage.sqlalchemy import models
24
+
25
+
26
+CONF = cfg.CONF
27
+
28
+
29
+def _alembic_config():
30
+    path = os.path.join(os.path.dirname(__file__), 'alembic.ini')
31
+    config = alembic_config.Config(path)
32
+    return config
33
+
34
+
35
+def create_schema(config=None, engine=None):
36
+    """Create database schema from models description.
37
+
38
+    Can be used for initial installation instead of upgrade('head').
39
+    """
40
+    if engine is None:
41
+        engine = enginefacade.writer.get_engine()
42
+
43
+    # NOTE(viktors): If we will use metadata.create_all() for non empty db
44
+    #                schema, it will only add the new tables, but leave
45
+    #                existing as is. So we should avoid of this situation.
46
+    if version(engine=engine) is not None:
47
+        raise db_exc.DBMigrationError("DB schema is already under version"
48
+                                      " control. Use upgrade() instead")
49
+
50
+    models.Base.metadata.create_all(engine)
51
+    stamp('head', config=config)
52
+
53
+
54
+def revision(message=None, autogenerate=False, config=None):
55
+    """Creates template for migration.
56
+
57
+    :param message: Text that will be used for migration title
58
+    :type message: string
59
+    :param autogenerate: If True - generates diff based on current database
60
+                         state
61
+    :type autogenerate: bool
62
+    """
63
+    config = config or _alembic_config()
64
+    return alembic_command.revision(config, message=message,
65
+                                    autogenerate=autogenerate)
66
+
67
+
68
+def stamp(revision, config=None):
69
+    """Stamps database with provided revision.
70
+
71
+    Don't run any migrations.
72
+    :param revision: Should match one from repository or head - to stamp
73
+                     database with most recent revision
74
+    :type revision: string
75
+    """
76
+    config = config or _alembic_config()
77
+    return alembic_command.stamp(config, revision=revision)
78
+
79
+
80
+def upgrade(revision):
81
+    config = _alembic_config()
82
+
83
+    alembic_command.upgrade(config, revision)
84
+
85
+
86
+def version(config=None, engine=None):
87
+    if engine is None:
88
+        engine = enginefacade.writer.get_engine()
89
+    with engine.connect() as conn:
90
+        context = alembic_migrations.MigrationContext.configure(conn)
91
+        return context.get_current_revision()

+ 58
- 0
vitrage/storage/sqlalchemy/migration/alembic.ini View File

@@ -0,0 +1,58 @@
1
+# A generic, single database configuration.
2
+
3
+[alembic]
4
+# path to migration scripts
5
+script_location = %(here)s/alembic_migrations
6
+
7
+# template used to generate migration files
8
+# file_template = %%(rev)s_%%(slug)s
9
+
10
+
11
+# max length of characters to apply to the
12
+# "slug" field
13
+#truncate_slug_length = 40
14
+
15
+# set to 'true' to run the environment during
16
+# the 'revision' command, regardless of autogenerate
17
+# revision_environment = false
18
+
19
+# set to 'true' to allow .pyc and .pyo files without
20
+# a source .py file to be detected as revisions in the
21
+# versions/ directory
22
+# sourceless = false
23
+
24
+
25
+# Logging configuration
26
+[loggers]
27
+keys = root,sqlalchemy,alembic
28
+
29
+[handlers]
30
+keys = console
31
+
32
+[formatters]
33
+keys = generic
34
+
35
+[logger_root]
36
+level = WARN
37
+handlers = console
38
+qualname =
39
+
40
+[logger_sqlalchemy]
41
+level = WARN
42
+handlers =
43
+qualname = sqlalchemy.engine
44
+
45
+[logger_alembic]
46
+level = INFO
47
+handlers =
48
+qualname = alembic
49
+
50
+[handler_console]
51
+class = StreamHandler
52
+args = (sys.stderr,)
53
+level = NOTSET
54
+formatter = generic
55
+
56
+[formatter_generic]
57
+format = %(levelname)-5.5s [%(name)s] %(message)s
58
+datefmt = %H:%M:%S

+ 18
- 0
vitrage/storage/sqlalchemy/migration/alembic_migrations/README View File

@@ -0,0 +1,18 @@
1
+The migrations in `alembic_migrations/versions` contain the changes needed to
2
+migrate between Vitrage database revisions. A migration occurs by executing a
3
+script that details the changes needed to upgrade the database. The migration
4
+scripts are ordered so that multiple scripts can run sequentially. The scripts
5
+are executed by Vitrage's migration wrapper which uses the Alembic library to
6
+manage the migration. Vitrage supports migration from Train release or later.
7
+
8
+Please see https://alembic.readthedocs.org/en/latest/index.html for general documentation
9
+
10
+To create alembic migrations use:
11
+$ vitrage-dbsync-revision --message --autogenerate
12
+
13
+Stamp db with most recent migration version, without actually running migrations
14
+$ vitrage-dbsync-stamp --revision head
15
+
16
+Upgrade can be performed by:
17
+$ vitrage-dbsync - for backward compatibility
18
+$ vitrage-dbsync --revision head

+ 0
- 0
vitrage/storage/sqlalchemy/migration/alembic_migrations/__init__.py View File


+ 76
- 0
vitrage/storage/sqlalchemy/migration/alembic_migrations/env.py View File

@@ -0,0 +1,76 @@
1
+# Licensed under the Apache License, Version 2.0 (the "License");
2
+# you may not use this file except in compliance with the License.
3
+# You may obtain a copy of the License at
4
+#
5
+#    http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+# Unless required by applicable law or agreed to in writing, software
8
+# distributed under the License is distributed on an "AS IS" BASIS,
9
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10
+# implied.
11
+# See the License for the specific language governing permissions and
12
+# limitations under the License.
13
+
14
+from __future__ import with_statement
15
+from alembic import context
16
+from logging.config import fileConfig
17
+from oslo_db.sqlalchemy import enginefacade
18
+
19
+# this is the Alembic Config object, which provides
20
+# access to the values within the .ini file in use.
21
+config = context.config
22
+
23
+# Interpret the config file for Python logging.
24
+# This line sets up loggers basically.
25
+fileConfig(config.config_file_name)
26
+
27
+# add your model's MetaData object here
28
+# for 'autogenerate' support
29
+# from myapp import mymodel
30
+# target_metadata = mymodel.Base.metadata
31
+target_metadata = None
32
+
33
+# other values from the config, defined by the needs of env.py,
34
+# can be acquired:
35
+# my_important_option = config.get_main_option("my_important_option")
36
+# ... etc.
37
+
38
+
39
+def run_migrations_offline():
40
+    """Run migrations in 'offline' mode.
41
+
42
+    This configures the context with just a URL
43
+    and not an Engine, though an Engine is acceptable
44
+    here as well.  By skipping the Engine creation
45
+    we don't even need a DBAPI to be available.
46
+
47
+    Calls to context.execute() here emit the given string to the
48
+    script output.
49
+
50
+    """
51
+    url = config.get_main_option("sqlalchemy.url")
52
+    context.configure(
53
+        url=url, target_metadata=target_metadata, literal_binds=True)
54
+
55
+    with context.begin_transaction():
56
+        context.run_migrations()
57
+
58
+
59
+def run_migrations_online():
60
+    """Run migrations in 'online' mode.
61
+
62
+    In this scenario we need to create an Engine
63
+    and associate a connection with the context.
64
+
65
+    """
66
+    engine = enginefacade.writer.get_engine()
67
+    with engine.connect() as connection:
68
+        context.configure(connection=connection,
69
+                          target_metadata=target_metadata)
70
+        with context.begin_transaction():
71
+            context.run_migrations()
72
+
73
+if context.is_offline_mode():
74
+    run_migrations_offline()
75
+else:
76
+    run_migrations_online()

+ 32
- 0
vitrage/storage/sqlalchemy/migration/alembic_migrations/script.py.mako View File

@@ -0,0 +1,32 @@
1
+# Licensed under the Apache License, Version 2.0 (the "License");
2
+# you may not use this file except in compliance with the License.
3
+# You may obtain a copy of the License at
4
+#
5
+#    http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+# Unless required by applicable law or agreed to in writing, software
8
+# distributed under the License is distributed on an "AS IS" BASIS,
9
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10
+# implied.
11
+# See the License for the specific language governing permissions and
12
+# limitations under the License.
13
+
14
+from alembic import op
15
+import sqlalchemy as sa
16
+${imports if imports else ""}
17
+
18
+"""${message}
19
+
20
+Revision ID: ${up_revision}
21
+Revises: ${down_revision}
22
+Create Date: ${create_date}
23
+
24
+"""
25
+
26
+# revision identifiers, used by Alembic.
27
+revision = ${repr(up_revision)}
28
+down_revision = ${repr(down_revision)}
29
+
30
+
31
+def upgrade():
32
+    ${upgrades if upgrades else "pass"}

+ 260
- 0
vitrage/storage/sqlalchemy/migration/alembic_migrations/versions/4e44c9414dff_initial_migration.py View File

@@ -0,0 +1,260 @@
1
+# Licensed under the Apache License, Version 2.0 (the "License");
2
+# you may not use this file except in compliance with the License.
3
+# You may obtain a copy of the License at
4
+#
5
+#    http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+# Unless required by applicable law or agreed to in writing, software
8
+# distributed under the License is distributed on an "AS IS" BASIS,
9
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10
+# implied.
11
+# See the License for the specific language governing permissions and
12
+# limitations under the License.
13
+
14
+from alembic import op
15
+from oslo_utils import timeutils
16
+import sqlalchemy as sa
17
+
18
+from vitrage.storage.sqlalchemy import models
19
+
20
+
21
+"""Initial migration"
22
+
23
+Revision ID: 4e44c9414dff
24
+Revises: None
25
+Create Date: 2019-09-04 15:35:01.086784
26
+
27
+"""
28
+
29
+# revision identifiers, used by Alembic.
30
+revision = '4e44c9414dff'
31
+down_revision = None
32
+
33
+
34
+def upgrade():
35
+    try:
36
+        op.create_table(
37
+            'alarms',
38
+            sa.Column('vitrage_id', sa.String(128), primary_key=True),
39
+            sa.Column('start_timestamp', sa.DateTime, nullable=False),
40
+            sa.Column('end_timestamp', sa.DateTime, nullable=False,
41
+                      default=models.DEFAULT_END_TIME),
42
+            sa.Column('name', sa.String(256), nullable=False),
43
+            sa.Column('vitrage_type', sa.String(64), nullable=False),
44
+            sa.Column('vitrage_aggregated_severity', sa.String(64),
45
+                      nullable=False),
46
+            sa.Column('vitrage_operational_severity', sa.String(64),
47
+                      nullable=False),
48
+            sa.Column('project_id', sa.String(64)),
49
+            sa.Column('vitrage_resource_type', sa.String(64)),
50
+            sa.Column('vitrage_resource_id', sa.String(64)),
51
+            sa.Column('vitrage_resource_project_id', sa.String(64)),
52
+            sa.Column('payload', models.JSONEncodedDict),
53
+
54
+            sa.Column('created_at', sa.DateTime,
55
+                      default=lambda: timeutils.utcnow()),
56
+            sa.Column('updated_at', sa.DateTime,
57
+                      onupdate=lambda: timeutils.utcnow()),
58
+            mysql_charset='utf8',
59
+            mysql_engine='InnoDB'
60
+        )
61
+
62
+        op.create_table(
63
+            'edges',
64
+            sa.Column('source_id', sa.String(128), primary_key=True),
65
+            sa.Column('target_id', sa.String(128), primary_key=True),
66
+            sa.Column('label', sa.String(64), nullable=False),
67
+            sa.Column('start_timestamp', sa.DateTime, nullable=False),
68
+            sa.Column('end_timestamp', sa.DateTime, nullable=False,
69
+                      default=models.DEFAULT_END_TIME),
70
+            sa.Column('payload', models.JSONEncodedDict),
71
+
72
+            sa.Column('created_at', sa.DateTime,
73
+                      default=lambda: timeutils.utcnow()),
74
+            sa.Column('updated_at', sa.DateTime,
75
+                      onupdate=lambda: timeutils.utcnow()),
76
+            sa.ForeignKeyConstraint(['source_id'], ['alarms.vitrage_id'],
77
+                                    ondelete='CASCADE'),
78
+            sa.ForeignKeyConstraint(['target_id'], ['alarms.vitrage_id'],
79
+                                    ondelete='CASCADE'),
80
+            mysql_charset='utf8',
81
+            mysql_engine='InnoDB'
82
+        )
83
+
84
+        op.create_table(
85
+            'changes',
86
+            sa.Column('id', models.MagicBigInt, primary_key=True,
87
+                      autoincrement=True),
88
+            sa.Column('vitrage_id', sa.String(128), nullable=False),
89
+            sa.Column('timestamp', sa.DateTime, nullable=False),
90
+            sa.Column('severity', sa.String(64), nullable=False),
91
+            sa.Column('payload', models.JSONEncodedDict),
92
+
93
+            sa.Column('created_at', sa.DateTime,
94
+                      default=lambda: timeutils.utcnow()),
95
+            sa.Column('updated_at', sa.DateTime,
96
+                      onupdate=lambda: timeutils.utcnow()),
97
+            sa.ForeignKeyConstraint(['vitrage_id'], ['alarms.vitrage_id'],
98
+                                    ondelete='CASCADE'),
99
+            mysql_charset='utf8',
100
+            mysql_engine='InnoDB'
101
+        )
102
+
103
+        op.create_table(
104
+            'active_actions',
105
+            sa.Column('action_type', sa.String(128)),
106
+            sa.Column('extra_info', sa.String(128)),
107
+            sa.Column('source_vertex_id', sa.String(128)),
108
+            sa.Column('target_vertex_id', sa.String(128)),
109
+            sa.Column('action_id', sa.String(128), primary_key=True),
110
+            sa.Column('score', sa.SmallInteger()),
111
+            sa.Column('trigger', sa.String(128), primary_key=True),
112
+            sa.Column('created_at', sa.DateTime,
113
+                      default=lambda: timeutils.utcnow()),
114
+            sa.Column('updated_at', sa.DateTime,
115
+                      onupdate=lambda: timeutils.utcnow()),
116
+            mysql_charset='utf8',
117
+            mysql_engine='InnoDB'
118
+        )
119
+        op.create_table(
120
+            'templates',
121
+            sa.Column('id', sa.String(64), primary_key=True, nullable=False),
122
+            sa.Column('status', sa.String(16)),
123
+            sa.Column('status_details', sa.String(128)),
124
+            sa.Column('name', sa.String(128), nullable=False),
125
+            sa.Column('file_content', models.JSONEncodedDict, nullable=False),
126
+            sa.Column("type", sa.String(64), default='standard'),
127
+            sa.Column('created_at', sa.DateTime,
128
+                      default=lambda: timeutils.utcnow()),
129
+            sa.Column('updated_at', sa.DateTime,
130
+                      onupdate=lambda: timeutils.utcnow()),
131
+            mysql_charset='utf8',
132
+            mysql_engine='InnoDB'
133
+        )
134
+        op.create_table(
135
+            'webhooks',
136
+            sa.Column('id', sa.String(128), primary_key=True),
137
+            sa.Column('project_id', sa.String(128), nullable=False),
138
+            sa.Column('is_admin_webhook', sa.Boolean, nullable=False),
139
+            sa.Column('url', sa.String(256), nullable=False),
140
+            sa.Column('headers', sa.String(1024)),
141
+            sa.Column('regex_filter', sa.String(512)),
142
+            sa.Column('created_at', sa.DateTime,
143
+                      default=lambda: timeutils.utcnow()),
144
+            sa.Column('updated_at', sa.DateTime,
145
+                      onupdate=lambda: timeutils.utcnow()),
146
+            mysql_charset='utf8',
147
+            mysql_engine='InnoDB'
148
+        )
149
+        op.create_index(
150
+            'ix_active_action',
151
+            'active_actions',
152
+            [
153
+                'action_type', 'extra_info', 'source_vertex_id',
154
+                'target_vertex_id'
155
+            ]
156
+        )
157
+
158
+        op.create_table(
159
+            'events',
160
+            sa.Column("id", sa.BigInteger, primary_key=True, nullable=False,
161
+                      autoincrement=True),
162
+            sa.Column('payload', models.JSONEncodedDict(), nullable=False),
163
+            sa.Column('is_vertex', sa.Boolean, nullable=False),
164
+            sa.Column('created_at', sa.DateTime,
165
+                      default=lambda: timeutils.utcnow()),
166
+            sa.Column('updated_at', sa.DateTime,
167
+                      onupdate=lambda: timeutils.utcnow()),
168
+            mysql_charset='utf8',
169
+            mysql_engine='InnoDB'
170
+        )
171
+        op.create_table(
172
+            'graph_snapshots',
173
+            sa.Column('id', sa.Integer, primary_key=True,
174
+                      nullable=False),
175
+            sa.Column('event_id', sa.BigInteger, nullable=False),
176
+            sa.Column('graph_snapshot', models.CompressedBinary((2 ** 32) - 1),
177
+                      nullable=False),
178
+            sa.Column('created_at', sa.DateTime,
179
+                      default=lambda: timeutils.utcnow()),
180
+            sa.Column('updated_at', sa.DateTime,
181
+                      onupdate=lambda: timeutils.utcnow()),
182
+            mysql_charset='utf8',
183
+            mysql_engine='InnoDB'
184
+        )
185
+
186
+        op.create_index(
187
+            'ix_alarms_end_timestamp',
188
+            'alarms',
189
+            [
190
+                'end_timestamp'
191
+            ]
192
+        )
193
+
194
+        op.create_index(
195
+            'ix_alarms_project_id',
196
+            'alarms',
197
+            [
198
+                'project_id'
199
+            ]
200
+        )
201
+
202
+        op.create_index(
203
+            'ix_alarms_start_timestamp',
204
+            'alarms',
205
+            [
206
+                'start_timestamp'
207
+            ]
208
+        )
209
+
210
+        op.create_index(
211
+            'ix_alarms_vitrage_aggregated_severity',
212
+            'alarms',
213
+            [
214
+                'vitrage_aggregated_severity'
215
+            ]
216
+        )
217
+
218
+        op.create_index(
219
+            'ix_alarms_vitrage_operational_severity',
220
+            'alarms',
221
+            [
222
+                'vitrage_operational_severity'
223
+            ]
224
+        )
225
+
226
+        op.create_index(
227
+            'ix_alarms_vitrage_resource_project_id',
228
+            'alarms',
229
+            [
230
+                'vitrage_resource_project_id'
231
+            ]
232
+        )
233
+
234
+        op.create_index(
235
+            'ix_changes_severity',
236
+            'changes',
237
+            [
238
+                'severity'
239
+            ]
240
+        )
241
+        op.create_index(
242
+            'ix_changes_timestamp',
243
+            'changes',
244
+            [
245
+                'timestamp'
246
+            ]
247
+        )
248
+
249
+        op.create_index(
250
+            'ix_changes_vitrage_id',
251
+            'changes',
252
+            [
253
+                'vitrage_id'
254
+            ]
255
+        )
256
+    except Exception:
257
+        # TODO(e0ne): figure out more specific exception here to handle a case
258
+        # when migration is applied over Queens release and tables are already
259
+        # exists
260
+        pass

+ 0
- 0
vitrage/storage/sqlalchemy/migration/alembic_migrations/versions/__init__.py View File


+ 4
- 2
vitrage/storage/sqlalchemy/models.py View File

@@ -110,7 +110,7 @@ class Event(Base):
110 110
             )
111 111
 
112 112
 
113
-class ActiveAction(Base, models.TimestampMixin):
113
+class ActiveAction(Base):
114 114
     __tablename__ = 'active_actions'
115 115
     __table_args__ = (
116 116
         # Index 'ix_active_action' on fields:
@@ -170,7 +170,7 @@ class GraphSnapshot(Base):
170 170
             )
171 171
 
172 172
 
173
-class Template(Base, models.TimestampMixin):
173
+class Template(Base):
174 174
     __tablename__ = 'templates'
175 175
 
176 176
     uuid = Column("id", String(64), primary_key=True, nullable=False)
@@ -210,6 +210,7 @@ class Webhooks(Base):
210 210
             "<Webhook(" \
211 211
             "id='%s', " \
212 212
             "created_at='%s', " \
213
+            "updated_at='%s', " \
213 214
             "project_id='%s', " \
214 215
             "is_admin_webhook='%s', " \
215 216
             "url='%s', " \
@@ -218,6 +219,7 @@ class Webhooks(Base):
218 219
             (
219 220
                 self.id,
220 221
                 self.created_at,
222
+                self.updated_at,
221 223
                 self.project_id,
222 224
                 self.is_admin_webhook,
223 225
                 self.url,

+ 0
- 0
vitrage/tests/unit/storage/__init__.py View File


+ 121
- 0
vitrage/tests/unit/storage/test_migrations.py View File

@@ -0,0 +1,121 @@
1
+# Licensed under the Apache License, Version 2.0 (the "License");
2
+# you may not use this file except in compliance with the License.
3
+# You may obtain a copy of the License at
4
+#
5
+#    http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+# Unless required by applicable law or agreed to in writing, software
8
+# distributed under the License is distributed on an "AS IS" BASIS,
9
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
10
+# implied.
11
+# See the License for the specific language governing permissions and
12
+# limitations under the License.
13
+
14
+from alembic import script
15
+import contextlib
16
+import mock
17
+from oslo_db.sqlalchemy import enginefacade
18
+from oslo_db.sqlalchemy import test_fixtures
19
+from oslo_db.sqlalchemy import test_migrations
20
+from oslo_log import log as logging
21
+from oslo_utils import excutils
22
+from oslotest import base as test_base
23
+
24
+from vitrage.storage.sqlalchemy import migration
25
+from vitrage.storage.sqlalchemy import models
26
+
27
+
28
+LOG = logging.getLogger(__name__)
29
+
30
+
31
+@contextlib.contextmanager
32
+def patch_with_engine(engine):
33
+    with mock.patch.object(enginefacade.writer, 'get_engine') as patch_engine:
34
+        patch_engine.return_value = engine
35
+        yield
36
+
37
+
38
+class WalkWersionsMixin(object):
39
+    def _walk_versions(self, engine=None, alembic_cfg=None):
40
+        with patch_with_engine(engine):
41
+
42
+            script_directory = script.ScriptDirectory.from_config(alembic_cfg)
43
+
44
+            self.assertIsNone(self.migration_api.version(alembic_cfg))
45
+            versions = [ver for ver in script_directory.walk_revisions()]
46
+
47
+            for version in reversed(versions):
48
+                self._migrate_up(engine, alembic_cfg,
49
+                                 version.revision, with_data=True)
50
+
51
+    def _migrate_up(self, engine, config, version, with_data=False):
52
+        """migrate up to a new version of the db.
53
+
54
+        We allow for data insertion and post checks at every
55
+        migration version with special _pre_upgrade_### and
56
+        _check_### functions in the main test.
57
+        """
58
+
59
+        try:
60
+            if with_data:
61
+                data = None
62
+                pre_upgrade = getattr(
63
+                    self, "_pre_upgrade_%s" % version, None)
64
+                if pre_upgrade:
65
+                    data = pre_upgrade(engine)
66
+
67
+            self.migration_api.upgrade(version, config=config)
68
+            self.assertEqual(version, self.migration_api.version(config))
69
+
70
+            if with_data:
71
+                check = getattr(self, '_check_%s' % version, None)
72
+                if check:
73
+                    check(engine, data)
74
+        except Exception:
75
+            excutils.save_and_reraise_exception(logger=LOG)
76
+
77
+
78
+class MigrationCheckersMixin(object):
79
+    def setUp(self):
80
+        super(MigrationCheckersMixin, self).setUp()
81
+        self.engine = enginefacade.writer.get_engine()
82
+        self.config = migration._alembic_config()
83
+        self.migration_api = migration
84
+
85
+    def test_walk_versions(self):
86
+        self._walk_versions(self.engine, self.config)
87
+
88
+    def test_upgrade_and_version(self):
89
+        with patch_with_engine(self.engine):
90
+            self.migration_api.upgrade('head')
91
+            self.assertIsNotNone(self.migration_api.version())
92
+
93
+
94
+class TestMigrationsMySQL(MigrationCheckersMixin,
95
+                          WalkWersionsMixin,
96
+                          test_fixtures.OpportunisticDBTestMixin):
97
+    FIXTURE = test_fixtures.MySQLOpportunisticFixture
98
+
99
+
100
+class ModelsMigrationSyncMixin(object):
101
+
102
+    def setUp(self):
103
+        super(ModelsMigrationSyncMixin, self).setUp()
104
+        self.engine = enginefacade.writer.get_engine()
105
+
106
+    def get_metadata(self):
107
+        return models.Base.metadata
108
+
109
+    def get_engine(self):
110
+        return self.engine
111
+
112
+    def db_sync(self, engine):
113
+        with patch_with_engine(engine):
114
+            migration.upgrade('head')
115
+
116
+
117
+class ModelsMigrationsMySQL(ModelsMigrationSyncMixin,
118
+                            test_migrations.ModelsMigrationsSync,
119
+                            test_fixtures.OpportunisticDBTestMixin,
120
+                            test_base.BaseTestCase):
121
+    FIXTURE = test_fixtures.MySQLOpportunisticFixture

Loading…
Cancel
Save