blob: 3fcbe894c7a2f36d191d251d27b686c571dde26b [file] [log] [blame]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -08001#!/usr/bin/python
2
3# Copyright 2017-present Open Networking Foundation
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17# TO RUN
18# source scripts/setup_venv.sh
19# xos-migrate [-s <service-name>] [-r ~/cord]
20# eg: xos-migrate -r ~/Sites/cord -s core -s fabric
21
22# TODO
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080023# - add support to specify a name to be given to the generated migration (--name parameter in django makemigrations)
24# - add support to generate empty migrations (needed for data-only migrations)
25
26import os
27import sys
28import argparse
29import yaml
30import shutil
31from xosgenx.generator import XOSProcessor, XOSProcessorArgs
32from xosconfig import Config
33from multistructlog import create_logger
34
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080035def get_abs_path(dir_):
Scott Baker63c27ba2019-03-01 16:06:15 -080036 """ Convert a path specified by the user, which might be relative or based on
37 home directory location, into an absolute path.
38 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080039 if os.path.isabs(dir_):
Matteo Scandoloebd26052019-02-14 10:06:41 -080040 return os.path.realpath(dir_)
Matteo Scandolo1cda4352019-02-19 16:02:42 -080041 if dir_[0] == "~" and not os.path.exists(dir_):
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080042 dir_ = os.path.expanduser(dir_)
43 return os.path.abspath(dir_)
Scott Baker63c27ba2019-03-01 16:06:15 -080044 return os.path.abspath(os.path.join(os.getcwd(), dir_))
45
46
47def get_migration_library_path(dir_):
48 """ Return a directory relative to the location of the migration library """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080049 return os.path.dirname(os.path.realpath(__file__)) + "/" + dir_
50
51
52def print_banner(root):
53 log.info(r"---------------------------------------------------------------")
54 log.info(r" _ __ ")
55 log.info(r" _ ______ _____ ____ ___ (_)___ __________ _/ /____ ")
56 log.info(r" | |/_/ __ \/ ___/_____/ __ `__ \/ / __ `/ ___/ __ `/ __/ _ \ ")
57 log.info(r" _> </ /_/ (__ )_____/ / / / / / / /_/ / / / /_/ / /_/ __/ ")
58 log.info(r"/_/|_|\____/____/ /_/ /_/ /_/_/\__, /_/ \__,_/\__/\___/ ")
59 log.info(r" /____/ ")
60 log.info(r"---------------------------------------------------------------")
61 log.debug("CORD repo root", root=root)
62 log.debug("Storing logs in: %s" % os.environ["LOG_FILE"])
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080063 log.debug(r"---------------------------------------------------------------")
64
65
66def generate_core_models(core_dir):
67 core_xproto = os.path.join(core_dir, "core.xproto")
68
69 args = XOSProcessorArgs(
70 output=core_dir,
71 target="django.xtarget",
72 dest_extension="py",
73 write_to_file="model",
74 files=[core_xproto],
75 )
76 XOSProcessor.process(args)
77
78 security_args = XOSProcessorArgs(
79 output=core_dir,
80 target="django-security.xtarget",
81 dest_file="security.py",
82 write_to_file="single",
83 files=[core_xproto],
84 )
85
86 XOSProcessor.process(security_args)
87
88 init_args = XOSProcessorArgs(
89 output=core_dir,
90 target="init.xtarget",
91 dest_file="__init__.py",
92 write_to_file="single",
93 files=[core_xproto],
94 )
95 XOSProcessor.process(init_args)
96
97
98def find_xproto_in_folder(path):
99 """
100 Recursively iterate a folder tree to look for any xProto file.
101 We use this function in case that the name of the xProto is different from the name of the folder (eg: olt-service)
102 :param path: the root folder to start the search
103 :return: [string]
104 """
105 xprotos = []
106 for fn in os.listdir(path):
107 # skip hidden files and folders. plus other useless things
108 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
109 continue
110 full_path = os.path.join(path, fn)
111 if fn.endswith(".xproto"):
112 xprotos.append(full_path)
113 elif os.path.isdir(full_path):
114 xprotos = xprotos + find_xproto_in_folder(full_path)
115 return xprotos
116
117
118def find_decls_models(path):
119 """
120 Recursively iterate a folder tree to look for any models.py file.
121 This files contain the base model for _decl generated models.
122 :param path: the root folder to start the search
123 :return: [string]
124 """
125 decls = []
126 for fn in os.listdir(path):
127 # skip hidden files and folders. plus other useless things
128 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
129 continue
130 full_path = os.path.join(path, fn)
131 if fn == "models.py":
132 decls.append(full_path)
133 elif os.path.isdir(full_path):
134 decls = decls + find_decls_models(full_path)
135 return decls
136
137
138def get_service_name_from_config(path):
139 """
140 Given a service folder look for the config.yaml file and find the name
141 :param path: the root folder to start the search
142 :return: string
143 """
144 config = os.path.join(path, "xos/synchronizer/config.yaml")
145 if not os.path.isfile(config):
146 raise Exception("Config file not found at: %s" % config)
147
148 cfg_file = open(config)
149 cfg = yaml.load(cfg_file)
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800150 return cfg["name"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800151
152
153def generate_service_models(service_dir, service_dest_dir, service_name):
154 """
155 Generate the django code starting from xProto for a given service.
156 :param service_dir: string (path to the folder)
157 :param service_name: string (name of the service)
158 :return: void
159 """
160 xprotos = find_xproto_in_folder(service_dir)
161 decls = find_decls_models(service_dir)
162 log.debug("Generating models for %s from files %s" % (service_name, ", ".join(xprotos)))
163 out_dir = os.path.join(service_dest_dir, service_name)
164 if not os.path.isdir(out_dir):
165 os.mkdir(out_dir)
166
167 args = XOSProcessorArgs(
168 output=out_dir,
169 files=xprotos,
170 target="service.xtarget",
171 write_to_file="target",
172 )
173 XOSProcessor.process(args)
174
175 security_args = XOSProcessorArgs(
176 output=out_dir,
177 target="django-security.xtarget",
178 dest_file="security.py",
179 write_to_file="single",
180 files=xprotos,
181 )
182
183 XOSProcessor.process(security_args)
184
185 init_py_filename = os.path.join(out_dir, "__init__.py")
186 if not os.path.exists(init_py_filename):
187 open(init_py_filename, "w").write("# created by dynamicbuild")
188
189 # copy over models.py files from the service
190 if len(decls) > 0:
191 for file in decls:
192 fn = os.path.basename(file)
193 src_fn = file
194 dest_fn = os.path.join(out_dir, fn)
195 log.debug("Copying models.py from %s to %s" % (src_fn, dest_fn))
196 shutil.copyfile(src_fn, dest_fn)
197
198 # copy existing migrations from the service, otherwise they won't be incremental
199 src_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
200 if os.path.isdir(src_dir):
201 dest_dir = os.path.join(out_dir, "migrations")
202 if os.path.isdir(dest_dir):
203 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
204 shutil.copytree(src_dir, dest_dir)
205
206
207def copy_service_migrations(service_dir, service_dest_dir, service_name):
208 """
209 Once the migrations are generated, copy them in the correct location
210 :param service_dir: string (path to the folder)
211 :param service_name: string (name of the service)
212 :return: void
213 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800214 log.debug("Copying %s migrations to %s" % (service_name, service_dir))
215 migration_dir = os.path.join(service_dest_dir, service_name, "migrations")
216 dest_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
217 if os.path.isdir(dest_dir):
218 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
219 shutil.copytree(migration_dir, dest_dir)
Matteo Scandoloebd26052019-02-14 10:06:41 -0800220 # clean after the tool, generated migrations has been moved in the service repo
221 shutil.rmtree(get_abs_path(os.path.join(migration_dir, "../")))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800222
223
224def monkey_patch_migration_template():
225 import django
226 django.setup()
227
228 import django.db.migrations.writer as dj
229 dj.MIGRATION_TEMPLATE = """\
230# Copyright 2017-present Open Networking Foundation
231#
232# Licensed under the Apache License, Version 2.0 (the "License");
233# you may not use this file except in compliance with the License.
234# You may obtain a copy of the License at
235#
236# http://www.apache.org/licenses/LICENSE-2.0
237#
238# Unless required by applicable law or agreed to in writing, software
239# distributed under the License is distributed on an "AS IS" BASIS,
240# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
241# See the License for the specific language governing permissions and
242# limitations under the License.
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800243
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800244# -*- coding: utf-8 -*-
245# Generated by Django %(version)s on %(timestamp)s
246from __future__ import unicode_literals
247
248%(imports)s
249
250class Migration(migrations.Migration):
251%(replaces_str)s%(initial_str)s
252 dependencies = [
253%(dependencies)s\
254 ]
255
256 operations = [
257%(operations)s\
258 ]
259"""
260
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800261
262def configure_logging(verbose):
263 global log
264 # INITIALIZING LOGGER
265 Config.init()
266
267 cfg = Config().get("logging")
268 if verbose:
269 cfg["handlers"]["console"]["level"] = "DEBUG"
270
271 log = create_logger(cfg)
272
273
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800274# SETTING ENV
Scott Baker63c27ba2019-03-01 16:06:15 -0800275os.environ["LOG_FILE"] = get_migration_library_path("django.log")
276os.environ["XOS_CONFIG_SCHEMA"] = get_migration_library_path("migration_cfg_schema.yaml")
277os.environ["XOS_CONFIG_FILE"] = get_migration_library_path("migration_cfg.yaml")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800278os.environ["MIGRATIONS"] = "true"
279# this is populated in case we generate migrations for services and it's used in settings.py
280os.environ["INSTALLED_APPS"] = ""
281
282# PARAMS
283parser = argparse.ArgumentParser(description="XOS Migrations")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800284required = parser.add_argument_group("required arguments")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800285
286required.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800287 "-s",
288 "--service",
289 action="append",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800290 required=True,
291 dest="service_names",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800292 help="The name of the folder containing the service in cord/orchestration/xos_services"
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800293)
294
295parser.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800296 "-r",
297 "--repo",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800298 default=get_abs_path("~/cord"),
299 dest="repo_root",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800300 help="The location of the folder containing the CORD repo root (default to ~/cord)"
301)
302
303parser.add_argument(
304 "--check",
305 default=False,
306 action="store_true",
307 dest="check",
308 help="Check if the migrations are generated for a given service. Does not apply any change."
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800309)
310
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800311parser.add_argument(
312 "-v",
313 "--verbose",
314 help="increase log verbosity",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800315 dest="verbose",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800316 action="store_true"
317)
318
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800319
320def run():
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800321 # cleaning up from possible incorrect states
322 if "INSTALLED_APPS" in os.environ:
323 del os.environ["INSTALLED_APPS"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800324
325 args = parser.parse_args()
326
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800327 configure_logging(args.verbose)
328
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800329 print_banner(args.repo_root)
330
331 # find absolute path to the code
332 xos_path = get_abs_path(os.path.join(args.repo_root, "orchestration/xos/xos/"))
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800333 django_path = get_abs_path(os.path.join(xos_path, "manage.py"))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800334 core_dir = get_abs_path(os.path.join(xos_path, "core/models/"))
335 service_base_dir = get_abs_path(os.path.join(xos_path, "../../xos_services/"))
336 service_dest_dir = get_abs_path(os.path.join(xos_path, "services/"))
337
338 # we need to append the xos folder to sys.path
339 original_sys_path = sys.path
340 sys.path.append(xos_path)
341
342 log.info("Services: %s" % ", ".join(args.service_names))
343
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800344 django_cli_args = ['xos-migrate.py', "makemigrations"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800345
346 # generate the code for each service and create a list of parameters to pass to django
347 app_list = []
348 for service in args.service_names:
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800349 # NOTE we need core models to be there as all the services depend on them
350 generate_core_models(core_dir)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800351 if service == "core":
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800352 django_cli_args.append("core")
353 else:
354 service_dir = os.path.join(service_base_dir, service)
355 service_name = get_service_name_from_config(service_dir)
356 generate_service_models(service_dir, service_dest_dir, service_name)
357 app_list.append("services.%s" % service_name)
358
359 django_cli_args.append(service_name)
360
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800361 if len(app_list) > 0:
362 os.environ["INSTALLED_APPS"] = ",".join(app_list)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800363
364 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
365
366 monkey_patch_migration_template()
367
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800368 if args.check:
369 django_cli_args.append("--check")
370 django_cli_args.append("--dry-run")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800371
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800372 from django.core.management import execute_from_command_line
373
374 try:
375 log.debug("Django CLI Args", args=django_cli_args)
376 execute_from_command_line(django_cli_args)
377 returncode = 0
378 except SystemExit as e:
379 returncode = e.message
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800380
381 if returncode != 0:
382 if args.check:
383 log.error("Migrations are not up to date with the service changes!")
384 else:
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800385 log.error("An error occurred")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800386 sys.exit(returncode)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800387
388 # copying migrations back to the service
389 for service in args.service_names:
390 if service == "core":
391 # we don't need to copy migrations for the core
392 continue
393 else:
394 service_dir = os.path.join(service_base_dir, service)
395 service_name = get_service_name_from_config(service_dir)
396 copy_service_migrations(service_dir, service_dest_dir, service_name)
397
398 # restore orginal sys.path
399 sys.path = original_sys_path