blob: 312477d0a8332a308d47b6841f466ce61985f996 [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
35
36def get_abs_path(dir_):
37 if os.path.isabs(dir_):
Matteo Scandoloebd26052019-02-14 10:06:41 -080038 return os.path.realpath(dir_)
Matteo Scandolo1cda4352019-02-19 16:02:42 -080039 if dir_[0] == "~" and not os.path.exists(dir_):
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080040 dir_ = os.path.expanduser(dir_)
41 return os.path.abspath(dir_)
42 return os.path.dirname(os.path.realpath(__file__)) + "/" + dir_
43
44
45def print_banner(root):
46 log.info(r"---------------------------------------------------------------")
47 log.info(r" _ __ ")
48 log.info(r" _ ______ _____ ____ ___ (_)___ __________ _/ /____ ")
49 log.info(r" | |/_/ __ \/ ___/_____/ __ `__ \/ / __ `/ ___/ __ `/ __/ _ \ ")
50 log.info(r" _> </ /_/ (__ )_____/ / / / / / / /_/ / / / /_/ / /_/ __/ ")
51 log.info(r"/_/|_|\____/____/ /_/ /_/ /_/_/\__, /_/ \__,_/\__/\___/ ")
52 log.info(r" /____/ ")
53 log.info(r"---------------------------------------------------------------")
54 log.debug("CORD repo root", root=root)
55 log.debug("Storing logs in: %s" % os.environ["LOG_FILE"])
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080056 log.debug(r"---------------------------------------------------------------")
57
58
59def generate_core_models(core_dir):
60 core_xproto = os.path.join(core_dir, "core.xproto")
61
62 args = XOSProcessorArgs(
63 output=core_dir,
64 target="django.xtarget",
65 dest_extension="py",
66 write_to_file="model",
67 files=[core_xproto],
68 )
69 XOSProcessor.process(args)
70
71 security_args = XOSProcessorArgs(
72 output=core_dir,
73 target="django-security.xtarget",
74 dest_file="security.py",
75 write_to_file="single",
76 files=[core_xproto],
77 )
78
79 XOSProcessor.process(security_args)
80
81 init_args = XOSProcessorArgs(
82 output=core_dir,
83 target="init.xtarget",
84 dest_file="__init__.py",
85 write_to_file="single",
86 files=[core_xproto],
87 )
88 XOSProcessor.process(init_args)
89
90
91def find_xproto_in_folder(path):
92 """
93 Recursively iterate a folder tree to look for any xProto file.
94 We use this function in case that the name of the xProto is different from the name of the folder (eg: olt-service)
95 :param path: the root folder to start the search
96 :return: [string]
97 """
98 xprotos = []
99 for fn in os.listdir(path):
100 # skip hidden files and folders. plus other useless things
101 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
102 continue
103 full_path = os.path.join(path, fn)
104 if fn.endswith(".xproto"):
105 xprotos.append(full_path)
106 elif os.path.isdir(full_path):
107 xprotos = xprotos + find_xproto_in_folder(full_path)
108 return xprotos
109
110
111def find_decls_models(path):
112 """
113 Recursively iterate a folder tree to look for any models.py file.
114 This files contain the base model for _decl generated models.
115 :param path: the root folder to start the search
116 :return: [string]
117 """
118 decls = []
119 for fn in os.listdir(path):
120 # skip hidden files and folders. plus other useless things
121 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
122 continue
123 full_path = os.path.join(path, fn)
124 if fn == "models.py":
125 decls.append(full_path)
126 elif os.path.isdir(full_path):
127 decls = decls + find_decls_models(full_path)
128 return decls
129
130
131def get_service_name_from_config(path):
132 """
133 Given a service folder look for the config.yaml file and find the name
134 :param path: the root folder to start the search
135 :return: string
136 """
137 config = os.path.join(path, "xos/synchronizer/config.yaml")
138 if not os.path.isfile(config):
139 raise Exception("Config file not found at: %s" % config)
140
141 cfg_file = open(config)
142 cfg = yaml.load(cfg_file)
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800143 return cfg["name"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800144
145
146def generate_service_models(service_dir, service_dest_dir, service_name):
147 """
148 Generate the django code starting from xProto for a given service.
149 :param service_dir: string (path to the folder)
150 :param service_name: string (name of the service)
151 :return: void
152 """
153 xprotos = find_xproto_in_folder(service_dir)
154 decls = find_decls_models(service_dir)
155 log.debug("Generating models for %s from files %s" % (service_name, ", ".join(xprotos)))
156 out_dir = os.path.join(service_dest_dir, service_name)
157 if not os.path.isdir(out_dir):
158 os.mkdir(out_dir)
159
160 args = XOSProcessorArgs(
161 output=out_dir,
162 files=xprotos,
163 target="service.xtarget",
164 write_to_file="target",
165 )
166 XOSProcessor.process(args)
167
168 security_args = XOSProcessorArgs(
169 output=out_dir,
170 target="django-security.xtarget",
171 dest_file="security.py",
172 write_to_file="single",
173 files=xprotos,
174 )
175
176 XOSProcessor.process(security_args)
177
178 init_py_filename = os.path.join(out_dir, "__init__.py")
179 if not os.path.exists(init_py_filename):
180 open(init_py_filename, "w").write("# created by dynamicbuild")
181
182 # copy over models.py files from the service
183 if len(decls) > 0:
184 for file in decls:
185 fn = os.path.basename(file)
186 src_fn = file
187 dest_fn = os.path.join(out_dir, fn)
188 log.debug("Copying models.py from %s to %s" % (src_fn, dest_fn))
189 shutil.copyfile(src_fn, dest_fn)
190
191 # copy existing migrations from the service, otherwise they won't be incremental
192 src_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
193 if os.path.isdir(src_dir):
194 dest_dir = os.path.join(out_dir, "migrations")
195 if os.path.isdir(dest_dir):
196 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
197 shutil.copytree(src_dir, dest_dir)
198
199
200def copy_service_migrations(service_dir, service_dest_dir, service_name):
201 """
202 Once the migrations are generated, copy them in the correct location
203 :param service_dir: string (path to the folder)
204 :param service_name: string (name of the service)
205 :return: void
206 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800207 log.debug("Copying %s migrations to %s" % (service_name, service_dir))
208 migration_dir = os.path.join(service_dest_dir, service_name, "migrations")
209 dest_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
210 if os.path.isdir(dest_dir):
211 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
212 shutil.copytree(migration_dir, dest_dir)
Matteo Scandoloebd26052019-02-14 10:06:41 -0800213 # clean after the tool, generated migrations has been moved in the service repo
214 shutil.rmtree(get_abs_path(os.path.join(migration_dir, "../")))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800215
216
217def monkey_patch_migration_template():
218 import django
219 django.setup()
220
221 import django.db.migrations.writer as dj
222 dj.MIGRATION_TEMPLATE = """\
223# Copyright 2017-present Open Networking Foundation
224#
225# Licensed under the Apache License, Version 2.0 (the "License");
226# you may not use this file except in compliance with the License.
227# You may obtain a copy of the License at
228#
229# http://www.apache.org/licenses/LICENSE-2.0
230#
231# Unless required by applicable law or agreed to in writing, software
232# distributed under the License is distributed on an "AS IS" BASIS,
233# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
234# See the License for the specific language governing permissions and
235# limitations under the License.
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800236
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800237# -*- coding: utf-8 -*-
238# Generated by Django %(version)s on %(timestamp)s
239from __future__ import unicode_literals
240
241%(imports)s
242
243class Migration(migrations.Migration):
244%(replaces_str)s%(initial_str)s
245 dependencies = [
246%(dependencies)s\
247 ]
248
249 operations = [
250%(operations)s\
251 ]
252"""
253
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800254
255def configure_logging(verbose):
256 global log
257 # INITIALIZING LOGGER
258 Config.init()
259
260 cfg = Config().get("logging")
261 if verbose:
262 cfg["handlers"]["console"]["level"] = "DEBUG"
263
264 log = create_logger(cfg)
265
266
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800267# SETTING ENV
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800268os.environ["LOG_FILE"] = get_abs_path("django.log")
269os.environ["XOS_CONFIG_SCHEMA"] = get_abs_path("migration_cfg_schema.yaml")
270os.environ["XOS_CONFIG_FILE"] = get_abs_path("migration_cfg.yaml")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800271os.environ["MIGRATIONS"] = "true"
272# this is populated in case we generate migrations for services and it's used in settings.py
273os.environ["INSTALLED_APPS"] = ""
274
275# PARAMS
276parser = argparse.ArgumentParser(description="XOS Migrations")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800277required = parser.add_argument_group("required arguments")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800278
279required.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800280 "-s",
281 "--service",
282 action="append",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800283 required=True,
284 dest="service_names",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800285 help="The name of the folder containing the service in cord/orchestration/xos_services"
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800286)
287
288parser.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800289 "-r",
290 "--repo",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800291 default=get_abs_path("~/cord"),
292 dest="repo_root",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800293 help="The location of the folder containing the CORD repo root (default to ~/cord)"
294)
295
296parser.add_argument(
297 "--check",
298 default=False,
299 action="store_true",
300 dest="check",
301 help="Check if the migrations are generated for a given service. Does not apply any change."
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800302)
303
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800304parser.add_argument(
305 "-v",
306 "--verbose",
307 help="increase log verbosity",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800308 dest="verbose",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800309 action="store_true"
310)
311
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800312
313def run():
314
315 args = parser.parse_args()
316
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800317 configure_logging(args.verbose)
318
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800319 print_banner(args.repo_root)
320
321 # find absolute path to the code
322 xos_path = get_abs_path(os.path.join(args.repo_root, "orchestration/xos/xos/"))
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800323 django_path = get_abs_path(os.path.join(xos_path, "manage.py"))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800324 core_dir = get_abs_path(os.path.join(xos_path, "core/models/"))
325 service_base_dir = get_abs_path(os.path.join(xos_path, "../../xos_services/"))
326 service_dest_dir = get_abs_path(os.path.join(xos_path, "services/"))
327
328 # we need to append the xos folder to sys.path
329 original_sys_path = sys.path
330 sys.path.append(xos_path)
331
332 log.info("Services: %s" % ", ".join(args.service_names))
333
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800334 django_cli_args = ['xos-migrate.py', "makemigrations"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800335
336 # generate the code for each service and create a list of parameters to pass to django
337 app_list = []
338 for service in args.service_names:
339 if service == "core":
340
341 generate_core_models(core_dir)
342 django_cli_args.append("core")
343 else:
344 service_dir = os.path.join(service_base_dir, service)
345 service_name = get_service_name_from_config(service_dir)
346 generate_service_models(service_dir, service_dest_dir, service_name)
347 app_list.append("services.%s" % service_name)
348
349 django_cli_args.append(service_name)
350
351 os.environ["INSTALLED_APPS"] = ",".join(app_list)
352
353 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
354
355 monkey_patch_migration_template()
356
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800357 if args.check:
358 django_cli_args.append("--check")
359 django_cli_args.append("--dry-run")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800360
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800361 from django.core.management import execute_from_command_line
362
363 try:
364 log.debug("Django CLI Args", args=django_cli_args)
365 execute_from_command_line(django_cli_args)
366 returncode = 0
367 except SystemExit as e:
368 returncode = e.message
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800369
370 if returncode != 0:
371 if args.check:
372 log.error("Migrations are not up to date with the service changes!")
373 else:
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800374 log.error("An error occurred")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800375 sys.exit(returncode)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800376
377 # copying migrations back to the service
378 for service in args.service_names:
379 if service == "core":
380 # we don't need to copy migrations for the core
381 continue
382 else:
383 service_dir = os.path.join(service_base_dir, service)
384 service_name = get_service_name_from_config(service_dir)
385 copy_service_migrations(service_dir, service_dest_dir, service_name)
386
387 # restore orginal sys.path
388 sys.path = original_sys_path