~jcsackett/charmworld/bac-tag-constraints

145.1.1 by Abel Deuring
more copyright notcies added.
1
# Copyright 2012, 2013 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
4
"""Migrate data for the mongodb instance.
5
6
Loads migrations from the latest available in the versions directory.
7
8
"""
100.1.6 by Rick Harding
working on cli for migrate
9
import argparse
100.1.16 by Rick Harding
Update per code review
10
from datetime import datetime
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
11
import imp
12
from os import listdir
100.1.7 by Rick Harding
Update the migrate script with a cli that works
13
from os.path import abspath
100.1.6 by Rick Harding
working on cli for migrate
14
from os.path import dirname
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
15
from os.path import join
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
16
import re
17
128.1.1 by Rick Harding
Big chunk of reworking db
18
from charmworld.models import getconnection
100.1.7 by Rick Harding
Update the migrate script with a cli that works
19
from charmworld.models import getdb
100.1.6 by Rick Harding
working on cli for migrate
20
from charmworld.utils import get_ini
21
100.1.16 by Rick Harding
Update per code review
22
SCRIPT_TEMPLATE = """
100.1.3 by Rick Harding
Update to create a new version file
23
# {description}
24
25
def upgrade(db):
100.1.5 by Rick Harding
Lint
26
    \"\"\"Complete this function with work to be done for the migration/update.
100.1.3 by Rick Harding
Update to create a new version file
27
28
    db is the pymongo db instance for our datastore. Charms are in db.charms
29
    for instance.
30
    \"\"\"
31
32
33
"""
34
35
36
def str_to_filename(s):
37
    """Replaces spaces, quotes, and double underscores to underscores."""
100.1.5 by Rick Harding
Lint
38
    s = s.replace(' ', '_')\
39
         .replace('"', '_')\
40
         .replace("'", '_')\
41
         .replace(".", "_")
100.1.3 by Rick Harding
Update to create a new version file
42
    while '__' in s:
43
        s = s.replace('__', '_')
44
    return s
45
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
46
47
class DataStore(object):
48
    """Communicate with the data store to determine version status."""
49
50
    def __init__(self, db):
51
        """Talk to the data store
52
53
        :param db: The mongo db connection.
54
55
        """
56
        self.db = db
57
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
58
    @property
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
59
    def current_version(self):
60
        """Return the current version of the data store."""
89.2.15 by Rick Harding
Merge with trunk, update to use migration, change naming of questions
61
        found = self.db.migration_version.find_one({
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
62
            '_id': 'version'
63
        })
64
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
65
        if found:
66
            return found['version']
67
        else:
68
            return None
69
70
    def update_version(self, version):
71
        """Update the version number in the data store."""
89.2.15 by Rick Harding
Merge with trunk, update to use migration, change naming of questions
72
        self.db.migration_version.save({
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
73
            '_id': 'version',
100.1.16 by Rick Harding
Update per code review
74
            'version': version,
75
            'date': datetime.now(),
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
76
        })
77
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
78
    def version_datastore(self):
79
        """Init the data to track the current version in the datastore.
80
81
        The data store starts out with version 0.
82
83
        Raises exception if the store is already versioned.
84
        """
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
85
        if self.current_version is not None:
86
            raise Exception('Data store is already versioned: {0}'.format(
87
                self.current_version))
89.2.15 by Rick Harding
Merge with trunk, update to use migration, change naming of questions
88
        self.db.migration_version.insert({
100.1.16 by Rick Harding
Update per code review
89
            '_id': 'version',
90
            'version': 0,
91
            'date': datetime.now(),
92
        })
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
93
94
95
class Versions(object):
96
97
    def __init__(self, path):
98
        """Collect version scripts and store them in self.versions
99
100
        """
100.1.16 by Rick Harding
Update per code review
101
        FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*')
100.1.3 by Rick Harding
Update to create a new version file
102
        self.versions_dir = path
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
103
        # Create temporary list of files, allowing skipped version numbers.
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
104
        files = listdir(path)
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
105
        versions = {}
106
107
        for name in files:
89.2.16 by Rick Harding
Fix the naming to not use a dot since that's mongo notation doh
108
            if not name.endswith('.py'):
109
                # Skip .pyc and the like
110
                continue
111
100.1.16 by Rick Harding
Update per code review
112
            match = FILENAME_WITH_VERSION.match(name)
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
113
            if match:
114
                num = int(match.group(1))
115
                versions[num] = name
116
            else:
117
                pass  # Must be a helper file or something, let's ignore it.
118
119
        self.versions = {}
120
        version_numbers = versions.keys()
121
        version_numbers.sort()
122
123
        self.version_indexes = version_numbers
124
        for idx in version_numbers:
125
            self.versions[idx] = versions[idx]
126
100.1.3 by Rick Harding
Update to create a new version file
127
    def create_new_version_file(self, description):
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
128
        """Create Python files for new version"""
100.1.3 by Rick Harding
Update to create a new version file
129
        version = self.latest + 1
130
131
        if not description:
132
            raise ValueError('Please provide a short migration description.')
133
134
        filename = "{0}_{1}.py".format(
135
            str(version).zfill(3),
136
            str_to_filename(description)
137
        )
138
139
        filepath = join(self.versions_dir, filename)
100.1.16 by Rick Harding
Update per code review
140
        with open(filepath, 'w') as new_migration:
141
            new_migration.write(
142
                SCRIPT_TEMPLATE.format(description=description))
100.1.3 by Rick Harding
Update to create a new version file
143
144
        # Update our current store of version data.
145
        self.versions[version] = filename
146
        self.version_indexes.append(version)
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
147
100.1.7 by Rick Harding
Update the migrate script with a cli that works
148
        return filename
149
100.1.1 by Rick Harding
Initial migrations code with a couple passing tests
150
    @property
151
    def latest(self):
152
        """:returns: Latest version in Collection"""
100.1.7 by Rick Harding
Update the migrate script with a cli that works
153
        return self.version_indexes[-1] if len(self.version_indexes) > 0 else 0
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
154
103.1.1 by Rick Harding
Add the init flag to upgrade and tests to support it
155
    def upgrade(self, datastore, init):
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
156
        """Run `upgrade` methods for required version files.
157
158
        :param datastore: An instance of DataStore
159
160
        """
161
        current_version = datastore.current_version
162
163
        if current_version is None:
103.1.1 by Rick Harding
Add the init flag to upgrade and tests to support it
164
            if init:
165
                # We want to auto init the database anyway.
166
                datastore.version_datastore()
167
                current_version = 0
168
            else:
169
                raise Exception('Data store is not versioned')
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
170
103.1.1 by Rick Harding
Add the init flag to upgrade and tests to support it
171
        if current_version >= self.latest:
100.1.16 by Rick Harding
Update per code review
172
            # Nothing to do here. All migrations processed already.
100.1.2 by Rick Harding
Add the upgade command, the ability to check versions, etc.
173
            return None
100.1.6 by Rick Harding
working on cli for migrate
174
100.1.16 by Rick Harding
Update per code review
175
        while current_version < self.latest:
176
            # Let's get processing.
177
            next_version = current_version + 1
178
            module_name = self.versions[next_version]
179
            module = imp.load_source(
180
                module_name.strip('.py'),
181
                join(self.versions_dir, module_name))
182
            getattr(module, 'upgrade')(datastore.db)
183
            current_version = next_version
184
            datastore.update_version(current_version)
185
        return current_version
186
100.1.6 by Rick Harding
working on cli for migrate
187
188
def parse_args():
189
    desc = "Mongo migration tool."
190
    parser = argparse.ArgumentParser(description=desc)
191
    subparsers = parser.add_subparsers(help='sub-command help')
192
193
    parser_current = subparsers.add_parser('current')
100.1.7 by Rick Harding
Update the migrate script with a cli that works
194
    parser_current.set_defaults(func=Commands.current)
195
100.1.6 by Rick Harding
working on cli for migrate
196
    parser_latest = subparsers.add_parser('latest')
100.1.7 by Rick Harding
Update the migrate script with a cli that works
197
    parser_latest.set_defaults(func=Commands.latest)
100.1.6 by Rick Harding
working on cli for migrate
198
199
    parser_new = subparsers.add_parser('new')
200
    parser_new.add_argument(
201
        '-d', '--description', action='store',
100.1.7 by Rick Harding
Update the migrate script with a cli that works
202
        required=True,
100.1.6 by Rick Harding
working on cli for migrate
203
        help='The description of the new migration.')
100.1.7 by Rick Harding
Update the migrate script with a cli that works
204
    parser_new.set_defaults(func=Commands.new)
100.1.6 by Rick Harding
working on cli for migrate
205
206
    parser_upgrade = subparsers.add_parser('upgrade')
100.1.7 by Rick Harding
Update the migrate script with a cli that works
207
    parser_upgrade.set_defaults(func=Commands.upgrade)
103.1.1 by Rick Harding
Add the init flag to upgrade and tests to support it
208
    parser_upgrade.add_argument(
209
        '-i', '--init', action='store_true',
210
        default=False,
211
        help='Auto init the database if not already init.')
100.1.6 by Rick Harding
working on cli for migrate
212
213
    args = parser.parse_args()
214
    return args
215
216
100.1.7 by Rick Harding
Update the migrate script with a cli that works
217
class Commands(object):
218
    """Container for the various commands the cli can execute"""
219
220
    @staticmethod
100.1.8 by Rick Harding
Garden
221
    def get_datastore(ini):
128.1.1 by Rick Harding
Big chunk of reworking db
222
        connection = getconnection(ini)
223
        db = getdb(connection, ini.get('mongo.database'))
100.1.8 by Rick Harding
Garden
224
        ds = DataStore(db)
225
        return ds
226
227
    @classmethod
228
    def current(cls, ini, args):
100.1.7 by Rick Harding
Update the migrate script with a cli that works
229
        """Fetch the current migration version of the data store."""
100.1.8 by Rick Harding
Garden
230
        ds = cls.get_datastore(ini)
100.1.7 by Rick Harding
Update the migrate script with a cli that works
231
        version = ds.current_version
232
233
        if version is None:
234
            print 'Data store is not currently versioned.'
235
        else:
236
            print version
237
238
    @staticmethod
239
    def latest(ini, args):
240
        """Determine the latest migration version available."""
241
        migrations = Versions(ini['migrations'])
242
        print migrations.latest
243
244
    @staticmethod
245
    def new(ini, args):
246
        """Generate a new migration file to be completed."""
247
        migrations = Versions(ini['migrations'])
248
        filename = migrations.create_new_version_file(args.description)
249
        print "Created new migration: {0}".format(filename)
250
100.1.8 by Rick Harding
Garden
251
    @classmethod
252
    def upgrade(cls, ini, args):
100.1.7 by Rick Harding
Update the migrate script with a cli that works
253
        """Upgrade the data store to the latest available migration."""
100.1.8 by Rick Harding
Garden
254
        ds = cls.get_datastore(ini)
100.1.7 by Rick Harding
Update the migrate script with a cli that works
255
        migrations = Versions(ini['migrations'])
103.1.1 by Rick Harding
Add the init flag to upgrade and tests to support it
256
        new_version = migrations.upgrade(ds, args.init)
100.1.7 by Rick Harding
Update the migrate script with a cli that works
257
258
        if new_version is None:
259
            print 'There are no new migrations to apply'
260
        else:
261
            print "Updated the datastore to version: {0}".format(new_version)
262
263
100.1.17 by Rick Harding
Add migrations cli entry point, update docs
264
def main():
265
    """Target for the console entry point."""
100.1.6 by Rick Harding
working on cli for migrate
266
    args = parse_args()
100.1.7 by Rick Harding
Update the migrate script with a cli that works
267
    ini = get_ini()
268
269
    # Add the migration path to the ini.
270
    migration_path = join(abspath(dirname(__file__)), 'versions')
271
    ini['migrations'] = migration_path
272
273
    args.func(ini, args)
100.1.17 by Rick Harding
Add migrations cli entry point, update docs
274
275
276
if __name__ == '__main__':
277
    main()