~joetalbott/uci-engine/user_auth

« back to all changes in this revision

Viewing changes to nf-stats-service/nfss/db_migrate.py

  • Committer: Thomi Richards
  • Date: 2014-06-27 20:02:44 UTC
  • mto: (629.2.9 nfss)
  • mto: This revision was merged to the branch mainline in revision 636.
  • Revision ID: thomi.richards@canonical.com-20140627200244-zi7dwxnyw38ypr2f
Initial version.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python3
 
2
# Ubuntu CI Engine
 
3
# Copyright 2014 Canonical Ltd.
 
4
#
 
5
# This program is free software: you can redistribute it and/or modify it
 
6
# under the terms of the GNU Affero General Public License version 3, as
 
7
# published by the Free Software Foundation.
 
8
#
 
9
# This program is distributed in the hope that it will be useful, but
 
10
# WITHOUT ANY WARRANTY; without even the implied warranties of
 
11
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 
12
# PURPOSE.  See the GNU Affero General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Affero General Public License
 
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
16
 
 
17
"""Migration script for the Non Functional Stats Service.
 
18
 
 
19
This script is called when the service is installed, and any time the service
 
20
is upgraded. It follows a few simple steps to manage the database version:
 
21
 
 
22
1) Connecxt to the database, and read the version from the db_version table. If
 
23
    it doesn't exist, assume the version is -1.
 
24
 
 
25
2) Scan the db_patches directory for files that end in '.sql'. The files must
 
26
    be named 'NNN-{name}.sql'. The NNN is the numeric order of the patches,
 
27
    with leading '0's.
 
28
 
 
29
3) Apply patches in order. Each patch is applied in a DB transaction, and after
 
30
    each patch we incriment the database revision number in the db_version
 
31
    table. As a special case, if we just upgraded to version '0', we create
 
32
    the table.
 
33
 
 
34
"""
 
35
 
 
36
import glob
 
37
import os.path
 
38
import psycopg2
 
39
import re
 
40
import sys
 
41
 
 
42
from nfss.database import get_connection_for_request
 
43
 
 
44
 
 
45
def main():
 
46
    connection = get_connection_for_request()
 
47
    current_version = get_current_database_version(connection)
 
48
    maximum_version = get_maximum_version()
 
49
    print("Current database version is: %d" % current_version)
 
50
    if current_version != maximum_version:
 
51
        print(
 
52
            "Database schema can be updated to version: %d" % maximum_version
 
53
        )
 
54
        for new_version in range(current_version + 1, maximum_version + 1):
 
55
            upgrade_database_to_version(connection, new_version)
 
56
    connection.close()
 
57
 
 
58
 
 
59
def get_current_database_version(connection):
 
60
    cursor = connection.cursor()
 
61
    try:
 
62
        cursor.execute('SELECT version FROM db_version;')
 
63
    except psycopg2.ProgrammingError:
 
64
        # table doesn't exist, we have a blank database, and need to start from
 
65
        # scratch.
 
66
        connection.rollback()
 
67
        return -1
 
68
 
 
69
    data = cursor.fetchone()
 
70
    if data is None:
 
71
        # table exists, but contains no data - not sure what this means
 
72
        # as we should never get here. Let's assume version -1.
 
73
        return -1
 
74
    else:
 
75
        return int(data[0])
 
76
 
 
77
 
 
78
def report_error(message):
 
79
    # TODO: replace with proper logging!
 
80
    sys.stderr.write(message + '\n')
 
81
 
 
82
 
 
83
def get_maximum_version():
 
84
    """Return the maximum version we can patch the database to."""
 
85
    patches = get_all_patch_paths()
 
86
    return int(patches[-1][:3])
 
87
 
 
88
 
 
89
def get_all_patch_paths():
 
90
    patches_path = get_database_patch_dir()
 
91
    return sorted(
 
92
        filter(
 
93
            lambda f: re.match(r'\d{3}-.*.sql', f),
 
94
            os.listdir(patches_path)
 
95
        )
 
96
    )
 
97
 
 
98
 
 
99
def get_patch_file_path_for_version(version_number):
 
100
    """Given a database version number, get the path to a patch that upgrades
 
101
    the schema to that version.
 
102
 
 
103
    raise a RuntimeError if more than one file matches.
 
104
    """
 
105
    matches = glob.glob(
 
106
        os.path.join(
 
107
            get_database_patch_dir(),
 
108
            "%03d-*.sql" % version_number
 
109
        )
 
110
    )
 
111
    if len(matches) > 1:
 
112
        raise RuntimeError(
 
113
            "More than one file matched for version %d: %r" % (
 
114
                version_number, matches)
 
115
        )
 
116
    if not matches:
 
117
        raise RuntimeError(
 
118
            "No patch file found for version %d" % version_number
 
119
        )
 
120
    return matches[0]
 
121
 
 
122
 
 
123
def get_database_patch_dir():
 
124
    return os.path.abspath(
 
125
        os.path.join(
 
126
            os.path.dirname(__file__),
 
127
            '..',
 
128
            'db_patches'
 
129
        )
 
130
    )
 
131
 
 
132
 
 
133
def upgrade_database_to_version(connection, new_version):
 
134
    """Do the work to upgrade the database to version 'new_version'.
 
135
 
 
136
    This function starts a transaction on 'connection', and will either
 
137
    commit or rollback the transaction before the function ends.
 
138
 
 
139
    On success the function returns None, on failure the function raises an
 
140
    exception.
 
141
 
 
142
    Note: Only call this function if the database is at 'new_version' - 1.
 
143
 
 
144
    """
 
145
    patch_file = get_patch_file_path_for_version(new_version)
 
146
    print("Upgrading to version %s ..." % new_version)
 
147
    cursor = connection.cursor()
 
148
    try:
 
149
        with open(patch_file, 'r') as patch:
 
150
            cursor.execute(patch.read())
 
151
            if new_version == 0:
 
152
                cursor.execute('CREATE TABLE db_version (version integer);')
 
153
                cursor.execute('INSERT INTO db_version (version) VALUES (0);')
 
154
            else:
 
155
                cursor.execute('UPDATE db_version SET version=%d'
 
156
                               % new_version)
 
157
    except Exception as err:
 
158
        print("Error: %s" % err)
 
159
        connection.rollback()
 
160
        raise
 
161
    print("Done!")
 
162
    connection.commit()