Index: charmworld/migrations/migrate.py |
=== modified file 'charmworld/migrations/migrate.py' |
--- charmworld/migrations/migrate.py 2013-12-19 19:45:36 +0000 |
+++ charmworld/migrations/migrate.py 2014-02-11 14:43:04 +0000 |
@@ -1,10 +1,24 @@ |
-# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the |
+# Copyright 2012-2014 Canonical Ltd. This software is licensed under the |
# GNU Affero General Public License version 3 (see the file LICENSE). |
"""Migrate data for the mongodb instance. |
Loads migrations from the latest available in the versions directory. |
+The versioning can take the form of migrations and exoduses. A migration is a |
+simple data transformation that is expected to apply to a populated database |
+in a short amount of time. Generally five minutes is the maximum amount of |
+time allowed for all migrations for a given version. Migrations transform the |
+data in place and may block other operations on the collection. |
+ |
+If a transformation is expected to take longer, it should be written as an |
+exodus. An exodus makes a temporary copy of the charm collection, applies the |
+transformation, and then replaces the original copy with the new version. |
+Other operations on the charm collection are not blocked with the exodus |
+runs. |
+ |
+For any given upgrade, there may be at most one exodus and it must be run |
+before any subsequent migrations. |
""" |
import argparse |
from collections import namedtuple |
@@ -16,6 +30,7 @@ |
from os.path import join |
import re |
+import pymongo |
from pymongo.errors import OperationFailure |
from charmworld.models import ( |
@@ -29,20 +44,31 @@ |
from charmworld.utils import configure_logging |
-SCRIPT_TEMPLATE = """ |
+SCRIPT_TEMPLATE = '''\ |
# {description} |
+ |
def upgrade(db, index_client): |
- \"\"\"Complete this function with work to be done for the migration/update. |
+ """Complete this function with work to be done for the migration/update. |
db is the pymongo db instance for our datastore. Charms are in db.charms |
for instance. |
index_client is the ElasticSearch client. |
- \"\"\" |
- |
- |
-""" |
+ """ |
+ pass |
+ |
+ |
+## def exodus_update(source, target, current_version): |
+## """Complete this function with work for an exodus. |
+ |
+## source is the original CharmSource. |
+## target is the CharmSource to be used after the exodus. |
+## current_version is the version of this exodus. |
+## """ |
+## pass |
+ |
+''' |
def str_to_filename(s): |
@@ -56,6 +82,36 @@ |
return s |
+def setup_qa_data(db): |
+ """Setup the QA questions.""" |
+ for category_dict in iter_categories(): |
+ db.qa.insert(category_dict) |
+ |
+ |
+def setup_mongo_indices(charms): |
+ """Setup indices for charms. |
+ |
+ This function must be idempotent as it is called by the initialization |
+ section of migrate and may be called by upgrade scripts. |
+ |
+ Returns the index if created. If the index already exists None is |
+ returned. |
+ """ |
+ |
+ # Use ensure_index as it will not recreate the index if it already |
+ # exists. |
+ return charms.ensure_index( |
+ [ |
+ ('owner', pymongo.DESCENDING), |
+ ('series', pymongo.DESCENDING), |
+ ('name', pymongo.DESCENDING), |
+ ('store_data.revision', pymongo.DESCENDING), |
+ ], |
+ unique=True, |
+ name='owner_series_name_revision', |
+ ) |
+ |
+ |
class MigrationBeforeExodus(Exception): |
pass |
@@ -147,14 +203,10 @@ |
else: |
pass # Must be a helper file or something, let's ignore it. |
- @classmethod |
- def setup_qa_data(cls, db): |
- for category_dict in iter_categories(): |
- db.qa.insert(category_dict) |
- |
def initialize(self, datastore): |
datastore.version_datastore() |
- self.setup_qa_data(datastore.db) |
+ setup_mongo_indices(datastore.db.charms) |
+ setup_qa_data(datastore.db) |
def ensure_initialized(self, datastore, init=False): |
if datastore.current_version is not None: |
@@ -170,7 +222,7 @@ |
def version_path(self, description, version): |
filename = "{0}_{1}.py".format( |
str(version).zfill(3), |
- str_to_filename(description) |
+ str_to_filename(description), |
) |
path = join(self.versions_dir, filename) |
return path, filename |
@@ -206,6 +258,13 @@ |
self.iter_versions(current)] |
def _check_exodus(self, version_info): |
+ """Check the rules for running an exodus. |
+ |
+ * There can be only one exodus in the set of tranformations to be run. |
+ * An exodus must be first followed by one or more migrations. |
+ |
+ May raise MigrationBeforeExodus or MultipleExodus. |
+ """ |
exodus_seen = False |
migration_seen = False |
for num, filename, is_exodus in version_info: |