[SEBA-461][WIP] Adding xos-migrate to the XOS Toolchain
Change-Id: I3a6e2a86b804efe207e7a71109763b11ba9acdaa
diff --git a/lib/xos-migrate/.gitignore b/lib/xos-migrate/.gitignore
new file mode 100644
index 0000000..ed8b98a
--- /dev/null
+++ b/lib/xos-migrate/.gitignore
@@ -0,0 +1,6 @@
+build
+dist
+XosMigrate.egg-info
+.coverage
+coverage.xml
+cover
diff --git a/lib/xos-migrate/MANIFEST.in b/lib/xos-migrate/MANIFEST.in
new file mode 100644
index 0000000..a38c6ba
--- /dev/null
+++ b/lib/xos-migrate/MANIFEST.in
@@ -0,0 +1,2 @@
+include xosmigrate/migration_cfg_schema.yaml
+include xosmigrate/migration_cfg.yaml
diff --git a/lib/xos-migrate/bin/xos-migrate b/lib/xos-migrate/bin/xos-migrate
new file mode 100644
index 0000000..574bc40
--- /dev/null
+++ b/lib/xos-migrate/bin/xos-migrate
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+# from xosmigrate import run
+from xosmigrate.main import run
+run()
diff --git a/lib/xos-migrate/setup.py b/lib/xos-migrate/setup.py
new file mode 100644
index 0000000..fe795de
--- /dev/null
+++ b/lib/xos-migrate/setup.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+try:
+ from xosutil.autoversion_setup import setup_with_auto_version as setup
+except ImportError:
+ # xosutil is not installed. Expect this to happen when we build an egg, in which case xosgenx.version will
+ # automatically have the right version.
+ from setuptools import setup
+
+from xosgenx.version import __version__
+
+setup(
+ name="XosMigrate",
+ version=__version__,
+ description="XOS Migrations Toolkit",
+ author="Matteo Scandolo",
+ author_email="teo@opennetworking.org",
+ packages=["xosmigrate"],
+ scripts=["bin/xos-migrate"],
+ include_package_data=True,
+ # TODO add all deps to the install_requires section
+ install_requires=[],
+)
diff --git a/lib/xos-migrate/xosmigrate/__init__.py b/lib/xos-migrate/xosmigrate/__init__.py
new file mode 100644
index 0000000..0612f4f
--- /dev/null
+++ b/lib/xos-migrate/xosmigrate/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import main
diff --git a/lib/xos-migrate/xosmigrate/main.py b/lib/xos-migrate/xosmigrate/main.py
new file mode 100644
index 0000000..e6901e2
--- /dev/null
+++ b/lib/xos-migrate/xosmigrate/main.py
@@ -0,0 +1,354 @@
+#!/usr/bin/python
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# TO RUN
+# source scripts/setup_venv.sh
+# xos-migrate [-s <service-name>] [-r ~/cord]
+# eg: xos-migrate -r ~/Sites/cord -s core -s fabric
+
+# TODO
+# - add support for services that are in the cord/orchestration/profiles folders
+# - add support to specify a name to be given to the generated migration (--name parameter in django makemigrations)
+# - add support to generate empty migrations (needed for data-only migrations)
+
+import os
+import sys
+import argparse
+import yaml
+import shutil
+from xosgenx.generator import XOSProcessor, XOSProcessorArgs
+from xosconfig import Config
+from multistructlog import create_logger
+
+
+def get_abs_path(dir_):
+ if os.path.isabs(dir_):
+ return dir_
+ if dir_[0] == '~' and not os.path.exists(dir_):
+ dir_ = os.path.expanduser(dir_)
+ return os.path.abspath(dir_)
+ return os.path.dirname(os.path.realpath(__file__)) + "/" + dir_
+
+
+def print_banner(root):
+ log.info(r"---------------------------------------------------------------")
+ log.info(r" _ __ ")
+ log.info(r" _ ______ _____ ____ ___ (_)___ __________ _/ /____ ")
+ log.info(r" | |/_/ __ \/ ___/_____/ __ `__ \/ / __ `/ ___/ __ `/ __/ _ \ ")
+ log.info(r" _> </ /_/ (__ )_____/ / / / / / / /_/ / / / /_/ / /_/ __/ ")
+ log.info(r"/_/|_|\____/____/ /_/ /_/ /_/_/\__, /_/ \__,_/\__/\___/ ")
+ log.info(r" /____/ ")
+ log.info(r"---------------------------------------------------------------")
+ log.debug("CORD repo root", root=root)
+ log.debug("Storing logs in: %s" % os.environ["LOG_FILE"])
+ # log.debug("Config schema: %s" % os.environ['XOS_CONFIG_SCHEMA'])
+ # log.debug("Config: %s" % os.environ['XOS_CONFIG_FILE'])
+ log.debug(r"---------------------------------------------------------------")
+
+
+def generate_core_models(core_dir):
+ core_xproto = os.path.join(core_dir, "core.xproto")
+
+ args = XOSProcessorArgs(
+ output=core_dir,
+ target="django.xtarget",
+ dest_extension="py",
+ write_to_file="model",
+ files=[core_xproto],
+ )
+ XOSProcessor.process(args)
+
+ security_args = XOSProcessorArgs(
+ output=core_dir,
+ target="django-security.xtarget",
+ dest_file="security.py",
+ write_to_file="single",
+ files=[core_xproto],
+ )
+
+ XOSProcessor.process(security_args)
+
+ init_args = XOSProcessorArgs(
+ output=core_dir,
+ target="init.xtarget",
+ dest_file="__init__.py",
+ write_to_file="single",
+ files=[core_xproto],
+ )
+ XOSProcessor.process(init_args)
+
+
+def find_xproto_in_folder(path):
+ """
+ Recursively iterate a folder tree to look for any xProto file.
+ We use this function in case that the name of the xProto is different from the name of the folder (eg: olt-service)
+ :param path: the root folder to start the search
+ :return: [string]
+ """
+ xprotos = []
+ for fn in os.listdir(path):
+ # skip hidden files and folders. plus other useless things
+ if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
+ continue
+ full_path = os.path.join(path, fn)
+ if fn.endswith(".xproto"):
+ xprotos.append(full_path)
+ elif os.path.isdir(full_path):
+ xprotos = xprotos + find_xproto_in_folder(full_path)
+ return xprotos
+
+
+def find_decls_models(path):
+ """
+ Recursively iterate a folder tree to look for any models.py file.
+ This files contain the base model for _decl generated models.
+ :param path: the root folder to start the search
+ :return: [string]
+ """
+ decls = []
+ for fn in os.listdir(path):
+ # skip hidden files and folders. plus other useless things
+ if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
+ continue
+ full_path = os.path.join(path, fn)
+ if fn == "models.py":
+ decls.append(full_path)
+ elif os.path.isdir(full_path):
+ decls = decls + find_decls_models(full_path)
+ return decls
+
+
+def get_service_name_from_config(path):
+ """
+ Given a service folder look for the config.yaml file and find the name
+ :param path: the root folder to start the search
+ :return: string
+ """
+ config = os.path.join(path, "xos/synchronizer/config.yaml")
+ if not os.path.isfile(config):
+ raise Exception("Config file not found at: %s" % config)
+
+ cfg_file = open(config)
+ cfg = yaml.load(cfg_file)
+ return cfg['name']
+
+
+def generate_service_models(service_dir, service_dest_dir, service_name):
+ """
+ Generate the django code starting from xProto for a given service.
+ :param service_dir: string (path to the folder)
+ :param service_name: string (name of the service)
+ :return: void
+ """
+ xprotos = find_xproto_in_folder(service_dir)
+ decls = find_decls_models(service_dir)
+ log.debug("Generating models for %s from files %s" % (service_name, ", ".join(xprotos)))
+ out_dir = os.path.join(service_dest_dir, service_name)
+ if not os.path.isdir(out_dir):
+ os.mkdir(out_dir)
+
+ args = XOSProcessorArgs(
+ output=out_dir,
+ files=xprotos,
+ target="service.xtarget",
+ write_to_file="target",
+ )
+ XOSProcessor.process(args)
+
+ security_args = XOSProcessorArgs(
+ output=out_dir,
+ target="django-security.xtarget",
+ dest_file="security.py",
+ write_to_file="single",
+ files=xprotos,
+ )
+
+ XOSProcessor.process(security_args)
+
+ init_py_filename = os.path.join(out_dir, "__init__.py")
+ if not os.path.exists(init_py_filename):
+ open(init_py_filename, "w").write("# created by dynamicbuild")
+
+ # copy over models.py files from the service
+ if len(decls) > 0:
+ for file in decls:
+ fn = os.path.basename(file)
+ src_fn = file
+ dest_fn = os.path.join(out_dir, fn)
+ log.debug("Copying models.py from %s to %s" % (src_fn, dest_fn))
+ shutil.copyfile(src_fn, dest_fn)
+
+ # copy existing migrations from the service, otherwise they won't be incremental
+ src_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
+ if os.path.isdir(src_dir):
+ dest_dir = os.path.join(out_dir, "migrations")
+ if os.path.isdir(dest_dir):
+ shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
+ shutil.copytree(src_dir, dest_dir)
+
+
+def copy_service_migrations(service_dir, service_dest_dir, service_name):
+ """
+ Once the migrations are generated, copy them in the correct location
+ :param service_dir: string (path to the folder)
+ :param service_name: string (name of the service)
+ :return: void
+ """
+ # FIXME Django eats this message
+ log.debug("Copying %s migrations to %s" % (service_name, service_dir))
+ migration_dir = os.path.join(service_dest_dir, service_name, "migrations")
+ dest_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
+ if os.path.isdir(dest_dir):
+ shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
+ shutil.copytree(migration_dir, dest_dir)
+
+
+def monkey_patch_migration_template():
+ import django
+ django.setup()
+
+ import django.db.migrations.writer as dj
+ dj.MIGRATION_TEMPLATE = """\
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# -*- coding: utf-8 -*-
+# Generated by Django %(version)s on %(timestamp)s
+from __future__ import unicode_literals
+
+%(imports)s
+
+class Migration(migrations.Migration):
+%(replaces_str)s%(initial_str)s
+ dependencies = [
+%(dependencies)s\
+ ]
+
+ operations = [
+%(operations)s\
+ ]
+"""
+
+# SETTING ENV
+os.environ['LOG_FILE'] = get_abs_path("django.log")
+os.environ['XOS_CONFIG_SCHEMA'] = get_abs_path("migration_cfg_schema.yaml")
+os.environ['XOS_CONFIG_FILE'] = get_abs_path("migration_cfg.yaml")
+os.environ["MIGRATIONS"] = "true"
+# this is populated in case we generate migrations for services and it's used in settings.py
+os.environ["INSTALLED_APPS"] = ""
+
+# PARAMS
+parser = argparse.ArgumentParser(description="XOS Migrations")
+required = parser.add_argument_group('required arguments')
+
+required.add_argument(
+ '-s',
+ '--service',
+ action='append',
+ required=True,
+ dest="service_names",
+ help='The name of the folder containing the service in cord/orchestration/xos_services'
+)
+
+parser.add_argument(
+ '-r',
+ '--repo',
+ default=get_abs_path("~/cord"),
+ dest="repo_root",
+ help='The location of the folder containing the CORD repo root (default to ~/cord)'
+)
+
+# FIXME this is not working with multistructlog
+parser.add_argument(
+ "-v",
+ "--verbose",
+ help="increase log verbosity",
+ action="store_true"
+)
+
+# INITIALIZING LOGGER
+Config.init()
+log = create_logger(Config().get('logging'))
+
+
+def run():
+
+ args = parser.parse_args()
+
+ print_banner(args.repo_root)
+
+ # find absolute path to the code
+ xos_path = get_abs_path(os.path.join(args.repo_root, "orchestration/xos/xos/"))
+ core_dir = get_abs_path(os.path.join(xos_path, "core/models/"))
+ service_base_dir = get_abs_path(os.path.join(xos_path, "../../xos_services/"))
+ service_dest_dir = get_abs_path(os.path.join(xos_path, "services/"))
+
+ # we need to append the xos folder to sys.path
+ original_sys_path = sys.path
+ sys.path.append(xos_path)
+
+ log.info("Services: %s" % ", ".join(args.service_names))
+
+ django_cli_args = ['xos-migrate.py', 'makemigrations']
+
+ # generate the code for each service and create a list of parameters to pass to django
+ app_list = []
+ for service in args.service_names:
+ if service == "core":
+
+ generate_core_models(core_dir)
+ django_cli_args.append("core")
+ else:
+ service_dir = os.path.join(service_base_dir, service)
+ service_name = get_service_name_from_config(service_dir)
+ generate_service_models(service_dir, service_dest_dir, service_name)
+ app_list.append("services.%s" % service_name)
+
+ django_cli_args.append(service_name)
+
+ os.environ["INSTALLED_APPS"] = ",".join(app_list)
+
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+ monkey_patch_migration_template()
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(django_cli_args)
+
+ # copying migrations back to the service
+ for service in args.service_names:
+ if service == "core":
+ # we don't need to copy migrations for the core
+ continue
+ else:
+ service_dir = os.path.join(service_base_dir, service)
+ service_name = get_service_name_from_config(service_dir)
+ copy_service_migrations(service_dir, service_dest_dir, service_name)
+
+ # restore orginal sys.path
+ sys.path = original_sys_path
diff --git a/lib/xos-migrate/xosmigrate/migration_cfg.yaml b/lib/xos-migrate/xosmigrate/migration_cfg.yaml
new file mode 100644
index 0000000..b3d6e58
--- /dev/null
+++ b/lib/xos-migrate/xosmigrate/migration_cfg.yaml
@@ -0,0 +1,26 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+name: migrations
+logging:
+ version: 1
+ handlers:
+ console:
+ class: logging.StreamHandler
+ loggers:
+ 'multistructlog':
+ handlers:
+ - console
diff --git a/lib/xos-migrate/xosmigrate/migration_cfg_schema.yaml b/lib/xos-migrate/xosmigrate/migration_cfg_schema.yaml
new file mode 100644
index 0000000..3a28d64
--- /dev/null
+++ b/lib/xos-migrate/xosmigrate/migration_cfg_schema.yaml
@@ -0,0 +1,16 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+type: any
\ No newline at end of file