def _apply_migration(self, version, migration, skip=False): """ Persist and apply a migration First create an in-progress version entry, apply the script, then finalize the entry as succeeded, failed or skipped. When `skip` is True, do everything but actually run the script, for example, when baselining instead of migrating. """ self.logger.info('Advancing to version {}'.format(version)) if skip: statements = [] self.logger.info('Migration is marked for skipping, ' 'not actually running script') else: statements = CqlSplitter.split(migration.content) version_uuid = self._create_version(version, migration) new_state = Migration.State.FAILED result = None try: if statements: self.logger.info('Executing migration - ' '{} CQL statements'.format(len(statements))) # Set default keyspace so migrations don't need to refer to it # manually # Fixes https://github.com/Cobliteam/cassandra-migrate/issues/5 self.session.execute('USE {};'.format(self.config.keyspace)) for statement in statements: self.session.execute(statement) except Exception: self.logger.exception('Failed to execute migration') raise FailedMigration(version, migration.name) else: new_state = (Migration.State.SUCCEEDED if not skip else Migration.State.SKIPPED) finally: self.logger.info('Finalizing migration version with ' 'state {}'.format(new_state)) result = self._execute( self._q(FINALIZE_DB_VERSION), (new_state, version_uuid, Migration.State.IN_PROGRESS)) if not result or not result[0].applied: raise ConcurrentMigration(version, migration.name)
def _apply_python_migration(self, version, migration): """ Persist and apply a python migration First create an in-progress version entry, apply the script, then finalize the entry as succeeded, failed or skipped. """ self.logger.info('Applying python script') try: mod, _ = os.path.splitext(os.path.basename(migration.path)) migration_script = importlib.import_module(mod) migration_script.execute(self._session) except Exception: self.logger.exception('Failed to execute script') raise FailedMigration(version, migration.name)
def _cleanup_stuck_in_progress(self, cur_versions): if not cur_versions: return last_version = cur_versions[-1] if last_version.state != Migration.State.IN_PROGRESS: return self.logger.warn( 'Cleaning up previous migration stuck in progress ' '(version {}): {}'.format(last_version.version, last_version.name)) result = self._execute( self._q(DELETE_DB_VERSION), (last_version.id, Migration.State.IN_PROGRESS)) if not result[0].applied: raise FailedMigration(last_version.version, last_version.name)
def _apply_cql_migration(self, version, migration): """ Persist and apply a cql migration First create an in-progress version entry, apply the script, then finalize the entry as succeeded, failed or skipped. """ self.logger.info('Applying cql migration') statements = CqlSplitter.split(migration.content) try: if statements: self.logger.info('Executing migration with ' '{} CQL statements'.format(len(statements))) for statement in statements: self.session.execute(statement) except Exception: raise FailedMigration(version, migration.name)
def _apply_migration(self, version, migration, skip=False): """ Persist and apply a migration When `skip` is True, do everything but actually run the script, for example, when baselining instead of migrating. """ self.logger.info('Advancing to version {}'.format(version)) version_uuid = self._create_version(version, migration) new_state = Migration.State.FAILED sys.path.append(self.config.migrations_path) result = None try: if skip: self.logger.info('Migration is marked for skipping, ' 'not actually running script') else: if migration.is_python: self._apply_python_migration(version, migration) else: self._apply_cql_migration(version, migration) except Exception: self.logger.exception('Failed to execute migration') raise FailedMigration(version, migration.name) else: new_state = (Migration.State.SUCCEEDED if not skip else Migration.State.SKIPPED) finally: self.logger.info('Finalizing migration version with ' 'state {}'.format(new_state)) result = self._execute( self._q(FINALIZE_DB_VERSION), (new_state, version_uuid, Migration.State.IN_PROGRESS)) if not result or not result[0].applied: raise ConcurrentMigration(version, migration.name)
def _verify_migrations(self, migrations, ignore_failed=False, ignore_concurrent=False): """Verify if the version history persisted in C* matches the migrations Migrations with corresponding DB versions must have the same content and name. Every DB version must have a corresponding migration. Migrations without corresponding DB versions are considered pending, and returned as a result. Returns a list of tuples of (version, migration), with version starting from 1 and incrementing linearly. """ # Load all the currently existing versions and sort them by version # number, as Cassandra can only sort it for us by partition. cur_versions = self._execute( self._q('SELECT * FROM "{keyspace}"."{table}"')) cur_versions = sorted(cur_versions, key=lambda v: v.version) last_version = None version_pairs = zip_longest(cur_versions, migrations) # Work through ordered pairs of (existing version, migration), so that # stored versions and expected migrations can be compared for any # differences. for i, (version, migration) in enumerate(version_pairs, 1): # If version is empty, the migration has not yet been applied. # Keep track of the first such version, and append it to the # pending migrations list. if not version: break # If migration is empty, we have a version in the database with # no corresponding file. That might mean we're running the wrong # migration or have an out-of-date state, so we must fail. if not migration: raise UnknownMigration(version.version, version.name) # A migration was previously run and failed. if version.state == Migration.State.FAILED: if ignore_failed: break raise FailedMigration(version.version, version.name) last_version = version.version # A stored version's migrations differs from the one in the FS. if version.content != migration.content or \ version.name != migration.name or \ bytearray(version.checksum) != bytearray(migration.checksum): raise InconsistentState(migration, version) if not last_version: pending_migrations = list(migrations) else: pending_migrations = list(migrations)[last_version:] if not pending_migrations: self.logger.info('Database is already up-to-date') else: self.logger.info('Pending migrations found. Current version: {}, ' 'Latest version: {}'.format( last_version, len(migrations))) pending_migrations = enumerate(pending_migrations, (last_version or 0) + 1) return last_version, cur_versions, list(pending_migrations)