Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
remakemigrations.py 6.43 KiB
#!/usr/bin/env python3

# Copyright (C) 2018    ASTRON (Netherlands Institute for Radio Astronomy)
# P.O. Box 2, 7990 AA Dwingeloo, The Netherlands
#
# This file is part of the LOFAR software suite.
# The LOFAR software suite is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# The LOFAR software suite is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>.

# $Id:  $

# This script automates the procedure to replace the existing migrations on the source tree with initital migrations
# based on the current datamodel. Django offers a call 'makemigrations' through manage.py, which creates new migrations
# after the datamodel implementation has changed. These additional migrations apply those changes to an existing
# database reflecting the previous datamodel.
# This is a very nice feature for production, but there are a few downsides that this script tackles:
#
# 1. During development, where the datamodel constantly changes, whe typically don't want a ton of iterative migrations,
# but just have a clean start with a fresh initial database state without the whole provenance is perfectly fine. (We
# start up a fresh database anyway for every test or test deployment.) This can be achieved by removing all existing
# migrations prior to creating new ones.
# A difficulty with this approach is that we do have a manual migration to populate the database with fixtures.
# This migration needs to be restored or re-created after Django created fresh migrations for the database itself.
#
# 2. Since in settings.py we refer to the tmss app in the lofar environment, Django uses the build or installed version.
# A consequence is that the created migrations are placed in there and need to be copied to the source tree.
#
# This script requires a running postgres database instance to work against.
# To use specific database credentials, run e.g. ./remakemigrations.py -C b5f881c4-d41a-4f24-b9f5-23cd6a7f37d0


import os
from glob import glob
import subprocess as sp
import logging
import argparse
from shutil import copy
import lofar.sas.tmss

logger = logging.getLogger(__file__)

# set up paths
tmss_source_directory = os.path.dirname(__file__)
if tmss_source_directory == '':
    tmss_source_directory = '.'
tmss_env_directory = os.path.dirname(lofar.sas.tmss.__file__)
relative_migrations_directory = '/tmss/tmssapp/migrations/'

# template for manual changes and fixture (applied last):
template = """
#
# auto-generated by remakemigrations.py
#
# ! Please make sure to apply any changes to the template in that script !  
#
from django.db import migrations

from lofar.sas.tmss.tmss.tmssapp.populate import *

class Migration(migrations.Migration):

    dependencies = [
        ('tmssapp', '%s'),
    ]

    # Start SubTask id with 2 000 000 to avoid overlap with 'old' (test/production) OTDB
    operations = [ migrations.RunSQL('ALTER SEQUENCE tmssapp_SubTask_id_seq RESTART WITH 2000000;'),
                   migrations.RunPython(populate_choices),
                   migrations.RunPython(populate_settings),
                   migrations.RunPython(populate_misc),
                   migrations.RunPython(populate_resources),
                   migrations.RunPython(populate_cycles),
                   migrations.RunPython(populate_projects) ]
"""


def execute_and_log(cmd):

    logger.info('COMMAND: %s' % cmd)
    p = sp.Popen(cmd, shell=True, stdout=sp.PIPE, stderr=sp.PIPE)
    out, err = p.communicate()
    if out is not None:
        logger.info("STDOUT: %s" % out.decode('utf-8').strip())
    if err is not None:
        logger.info("STDERR: %s" % err.decode('utf-8').strip())


def delete_old_migrations():
    logger.info('Removing old migrations...')

    files = glob_migrations()
    for f in [path for path in files if ("initial" in path or "auto" in path or "populate" in path)]:
        logger.info('Deleting: %s' % f)
        os.remove(f)


def make_django_migrations(dbcredentials=None):

    logger.info('Making Django migrations...')
    if dbcredentials:
        os.environ['TMSS_DBCREDENTIALS'] = dbcredentials
    execute_and_log('/usr/bin/env python3 %s/manage.py makemigrations' % tmss_source_directory)


def make_populate_migration():

    logger.info('Making migration for populating database...')
    last_migration = determine_last_migration()
    migration = template % last_migration

    path = tmss_env_directory + relative_migrations_directory + '%s_populate.py' % str(int(last_migration.split('_')[0])+1).zfill(4)
    logger.info('Writing to: %s' % path)
    with open(path,'w') as f:
        f.write(migration)


def glob_migrations(directories=(tmss_source_directory, tmss_env_directory)):
    paths = []
    for directory in directories:
        paths += glob(directory + '/' + relative_migrations_directory + '0*_*')
    return paths


def copy_migrations_to_source():
    logger.info('Copying over migrations to source directory...')
    files = glob_migrations(directories=[tmss_env_directory])
    for file in files:
        logger.info('Copying %s to %s' % (file, tmss_source_directory + '/' + relative_migrations_directory))
        copy(file, tmss_source_directory + '/' + relative_migrations_directory)


def determine_last_migration():
    logger.info('Determining last migration...')
    files = glob_migrations()
    files = [os.path.basename(path) for path in files]
    f = max(files)
    last_migration = f.split('.py')[0]
    logger.info('Determined last migration: %s' % last_migration)
    return last_migration


def remake_migrations(dbcredentials=None):
    delete_old_migrations()
    make_django_migrations(dbcredentials)
    make_populate_migration()
    copy_migrations_to_source()


if __name__ == "__main__":

    logger.setLevel(logging.DEBUG)

    handler = logging.StreamHandler()
    handler.setLevel(logging.INFO)
    logger.addHandler(handler)

    parser = argparse.ArgumentParser()
    parser.add_argument("-C", action="store", dest="dbcredentials", help="use database specified in these dbcredentials")
    args = parser.parse_args()
    remake_migrations(args.dbcredentials)