blob: e7be1040c43f4e3df27dec626d06e674251f1f80 [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 Scandolo8419c082019-03-11 13:54:44 -070035REPO_ROOT = "~/cord"
36
Zack Williams5c2ea232019-01-30 15:23:01 -070037
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080038def get_abs_path(dir_):
Scott Baker63c27ba2019-03-01 16:06:15 -080039 """ Convert a path specified by the user, which might be relative or based on
40 home directory location, into an absolute path.
41 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080042 if os.path.isabs(dir_):
Matteo Scandoloebd26052019-02-14 10:06:41 -080043 return os.path.realpath(dir_)
Matteo Scandolo1cda4352019-02-19 16:02:42 -080044 if dir_[0] == "~" and not os.path.exists(dir_):
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080045 dir_ = os.path.expanduser(dir_)
46 return os.path.abspath(dir_)
Scott Baker63c27ba2019-03-01 16:06:15 -080047 return os.path.abspath(os.path.join(os.getcwd(), dir_))
48
49
50def get_migration_library_path(dir_):
51 """ Return a directory relative to the location of the migration library """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080052 return os.path.dirname(os.path.realpath(__file__)) + "/" + dir_
53
54
55def print_banner(root):
56 log.info(r"---------------------------------------------------------------")
57 log.info(r" _ __ ")
58 log.info(r" _ ______ _____ ____ ___ (_)___ __________ _/ /____ ")
59 log.info(r" | |/_/ __ \/ ___/_____/ __ `__ \/ / __ `/ ___/ __ `/ __/ _ \ ")
60 log.info(r" _> </ /_/ (__ )_____/ / / / / / / /_/ / / / /_/ / /_/ __/ ")
61 log.info(r"/_/|_|\____/____/ /_/ /_/ /_/_/\__, /_/ \__,_/\__/\___/ ")
62 log.info(r" /____/ ")
63 log.info(r"---------------------------------------------------------------")
64 log.debug("CORD repo root", root=root)
65 log.debug("Storing logs in: %s" % os.environ["LOG_FILE"])
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080066 log.debug(r"---------------------------------------------------------------")
67
68
69def generate_core_models(core_dir):
70 core_xproto = os.path.join(core_dir, "core.xproto")
71
72 args = XOSProcessorArgs(
73 output=core_dir,
74 target="django.xtarget",
75 dest_extension="py",
76 write_to_file="model",
77 files=[core_xproto],
78 )
79 XOSProcessor.process(args)
80
81 security_args = XOSProcessorArgs(
82 output=core_dir,
83 target="django-security.xtarget",
84 dest_file="security.py",
85 write_to_file="single",
86 files=[core_xproto],
87 )
88
89 XOSProcessor.process(security_args)
90
91 init_args = XOSProcessorArgs(
92 output=core_dir,
93 target="init.xtarget",
94 dest_file="__init__.py",
95 write_to_file="single",
96 files=[core_xproto],
97 )
98 XOSProcessor.process(init_args)
99
100
101def find_xproto_in_folder(path):
102 """
103 Recursively iterate a folder tree to look for any xProto file.
104 We use this function in case that the name of the xProto is different from the name of the folder (eg: olt-service)
105 :param path: the root folder to start the search
106 :return: [string]
107 """
108 xprotos = []
109 for fn in os.listdir(path):
110 # skip hidden files and folders. plus other useless things
111 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
112 continue
113 full_path = os.path.join(path, fn)
114 if fn.endswith(".xproto"):
115 xprotos.append(full_path)
116 elif os.path.isdir(full_path):
117 xprotos = xprotos + find_xproto_in_folder(full_path)
118 return xprotos
119
120
121def find_decls_models(path):
122 """
123 Recursively iterate a folder tree to look for any models.py file.
124 This files contain the base model for _decl generated models.
125 :param path: the root folder to start the search
126 :return: [string]
127 """
128 decls = []
129 for fn in os.listdir(path):
130 # skip hidden files and folders. plus other useless things
131 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
132 continue
133 full_path = os.path.join(path, fn)
134 if fn == "models.py":
135 decls.append(full_path)
136 elif os.path.isdir(full_path):
137 decls = decls + find_decls_models(full_path)
138 return decls
139
140
141def get_service_name_from_config(path):
142 """
143 Given a service folder look for the config.yaml file and find the name
144 :param path: the root folder to start the search
145 :return: string
146 """
147 config = os.path.join(path, "xos/synchronizer/config.yaml")
148 if not os.path.isfile(config):
149 raise Exception("Config file not found at: %s" % config)
150
151 cfg_file = open(config)
Zack Williams9a42f872019-02-15 17:56:04 -0700152 cfg = yaml.safe_load(cfg_file)
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800153 return cfg["name"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800154
155
156def generate_service_models(service_dir, service_dest_dir, service_name):
157 """
158 Generate the django code starting from xProto for a given service.
159 :param service_dir: string (path to the folder)
160 :param service_name: string (name of the service)
161 :return: void
162 """
163 xprotos = find_xproto_in_folder(service_dir)
164 decls = find_decls_models(service_dir)
165 log.debug("Generating models for %s from files %s" % (service_name, ", ".join(xprotos)))
166 out_dir = os.path.join(service_dest_dir, service_name)
167 if not os.path.isdir(out_dir):
168 os.mkdir(out_dir)
169
170 args = XOSProcessorArgs(
171 output=out_dir,
172 files=xprotos,
173 target="service.xtarget",
174 write_to_file="target",
175 )
176 XOSProcessor.process(args)
177
178 security_args = XOSProcessorArgs(
179 output=out_dir,
180 target="django-security.xtarget",
181 dest_file="security.py",
182 write_to_file="single",
183 files=xprotos,
184 )
185
186 XOSProcessor.process(security_args)
187
188 init_py_filename = os.path.join(out_dir, "__init__.py")
189 if not os.path.exists(init_py_filename):
190 open(init_py_filename, "w").write("# created by dynamicbuild")
191
192 # copy over models.py files from the service
193 if len(decls) > 0:
194 for file in decls:
195 fn = os.path.basename(file)
196 src_fn = file
197 dest_fn = os.path.join(out_dir, fn)
198 log.debug("Copying models.py from %s to %s" % (src_fn, dest_fn))
199 shutil.copyfile(src_fn, dest_fn)
200
201 # copy existing migrations from the service, otherwise they won't be incremental
202 src_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
203 if os.path.isdir(src_dir):
204 dest_dir = os.path.join(out_dir, "migrations")
205 if os.path.isdir(dest_dir):
206 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
207 shutil.copytree(src_dir, dest_dir)
208
209
210def copy_service_migrations(service_dir, service_dest_dir, service_name):
211 """
212 Once the migrations are generated, copy them in the correct location
213 :param service_dir: string (path to the folder)
214 :param service_name: string (name of the service)
215 :return: void
216 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800217 log.debug("Copying %s migrations to %s" % (service_name, service_dir))
218 migration_dir = os.path.join(service_dest_dir, service_name, "migrations")
219 dest_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
220 if os.path.isdir(dest_dir):
221 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
222 shutil.copytree(migration_dir, dest_dir)
Matteo Scandoloebd26052019-02-14 10:06:41 -0800223 # clean after the tool, generated migrations has been moved in the service repo
224 shutil.rmtree(get_abs_path(os.path.join(migration_dir, "../")))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800225
226
227def monkey_patch_migration_template():
228 import django
229 django.setup()
230
231 import django.db.migrations.writer as dj
232 dj.MIGRATION_TEMPLATE = """\
233# Copyright 2017-present Open Networking Foundation
234#
235# Licensed under the Apache License, Version 2.0 (the "License");
236# you may not use this file except in compliance with the License.
237# You may obtain a copy of the License at
238#
239# http://www.apache.org/licenses/LICENSE-2.0
240#
241# Unless required by applicable law or agreed to in writing, software
242# distributed under the License is distributed on an "AS IS" BASIS,
243# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
244# See the License for the specific language governing permissions and
245# limitations under the License.
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800246
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800247# -*- coding: utf-8 -*-
248# Generated by Django %(version)s on %(timestamp)s
249from __future__ import unicode_literals
250
251%(imports)s
252
253class Migration(migrations.Migration):
254%(replaces_str)s%(initial_str)s
255 dependencies = [
256%(dependencies)s\
257 ]
258
259 operations = [
260%(operations)s\
261 ]
262"""
263
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800264
265def configure_logging(verbose):
266 global log
267 # INITIALIZING LOGGER
268 Config.init()
269
270 cfg = Config().get("logging")
271 if verbose:
272 cfg["handlers"]["console"]["level"] = "DEBUG"
273
274 log = create_logger(cfg)
275
276
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800277# SETTING ENV
Scott Baker63c27ba2019-03-01 16:06:15 -0800278os.environ["LOG_FILE"] = get_migration_library_path("django.log")
279os.environ["XOS_CONFIG_SCHEMA"] = get_migration_library_path("migration_cfg_schema.yaml")
280os.environ["XOS_CONFIG_FILE"] = get_migration_library_path("migration_cfg.yaml")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800281os.environ["MIGRATIONS"] = "true"
282# this is populated in case we generate migrations for services and it's used in settings.py
283os.environ["INSTALLED_APPS"] = ""
284
285# PARAMS
286parser = argparse.ArgumentParser(description="XOS Migrations")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800287required = parser.add_argument_group("required arguments")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800288
289required.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800290 "-s",
291 "--service",
292 action="append",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800293 required=True,
294 dest="service_names",
Matteo Scandoloe1576802019-03-06 15:23:40 -0800295 help="The name of the folder containing the service in cord/orchestration/xos-services"
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800296)
297
Matteo Scandolo8419c082019-03-11 13:54:44 -0700298parser.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800299 "-r",
300 "--repo",
Matteo Scandolo8419c082019-03-11 13:54:44 -0700301 default=REPO_ROOT,
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800302 dest="repo_root",
Zack Williams70a67e72019-03-08 12:38:51 -0700303 help="Path to the CORD repo root (defaults to '../..'). Mutually exclusive with '--xos'."
304)
305
Matteo Scandolo8419c082019-03-11 13:54:44 -0700306parser.add_argument(
Zack Williams70a67e72019-03-08 12:38:51 -0700307 "-x",
Matteo Scandolo8419c082019-03-11 13:54:44 -0700308 "--xos-dir",
Zack Williams70a67e72019-03-08 12:38:51 -0700309 default=None,
310 dest="xos_root",
Matteo Scandolo8419c082019-03-11 13:54:44 -0700311 help="Path to directory of the XOS repo. Incompatible with '--repo'."
312)
313
314parser.add_argument(
315 "--services-dir",
316 default=None,
317 dest="services_root",
318 help="Path to directory of the XOS services root. Incompatible with '--repo'." +
319 "Note that all the services repo needs to be siblings"
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800320)
321
322parser.add_argument(
323 "--check",
324 default=False,
325 action="store_true",
326 dest="check",
327 help="Check if the migrations are generated for a given service. Does not apply any change."
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800328)
329
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800330parser.add_argument(
331 "-v",
332 "--verbose",
333 help="increase log verbosity",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800334 dest="verbose",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800335 action="store_true"
336)
337
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800338
339def run():
Matteo Scandolo8419c082019-03-11 13:54:44 -0700340 service_base_dir = None
341
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800342 # cleaning up from possible incorrect states
343 if "INSTALLED_APPS" in os.environ:
344 del os.environ["INSTALLED_APPS"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800345
346 args = parser.parse_args()
347
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800348 configure_logging(args.verbose)
349
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800350 print_banner(args.repo_root)
351
Matteo Scandolo8419c082019-03-11 13:54:44 -0700352 # validating args, the solution is hacky but it does not fit `add_mutually_exclusive_group`
353 # and it's not complex enough for the solution proposed here:
354 # https://stackoverflow.com/questions/17909294/python-argparse-mutual-exclusive-group
355 if args.service_names != ["core"] and \
356 ((args.xos_root and not args.services_root) or (args.services_root and not args.xos_root)):
357 # if we're only generating migrations for the core,
358 # the --xos-dir is the only think we need
359 log.error("You need to set both --xos-dir and \
360 --services-dir parameters when generating migrations for a service")
361 sys.exit(1)
362
363 if (args.xos_root or args.services_root) and (args.repo_root != REPO_ROOT):
364 log.error("The --xos-dir or --services-dir parameters are not compatible with the --repo parameter")
365 sys.exit(1)
366
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800367 # find absolute path to the code
Matteo Scandolo8419c082019-03-11 13:54:44 -0700368 if args.xos_root or args.services_root:
369 xos_path = get_abs_path(os.path.join(args.xos_root, "xos"))
370 if args.services_root:
371 # NOTE this params is optional (we may be generating migrations for the core only
372 service_base_dir = get_abs_path(args.services_root)
Zack Williams70a67e72019-03-08 12:38:51 -0700373 else:
374 xos_path = get_abs_path(os.path.join(args.repo_root, "orchestration/xos/xos/"))
Matteo Scandolo8419c082019-03-11 13:54:44 -0700375 service_base_dir = get_abs_path(os.path.join(xos_path, "../../xos-services/"))
Zack Williams70a67e72019-03-08 12:38:51 -0700376
Matteo Scandolo8419c082019-03-11 13:54:44 -0700377 log.debug("XOS Path: %s" % xos_path)
378 log.debug("Service Base Dir: %s" % service_base_dir)
379
380 service_dest_dir = get_abs_path(os.path.join(xos_path, "services/"))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800381 core_dir = get_abs_path(os.path.join(xos_path, "core/models/"))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800382
383 # we need to append the xos folder to sys.path
384 original_sys_path = sys.path
385 sys.path.append(xos_path)
386
387 log.info("Services: %s" % ", ".join(args.service_names))
388
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800389 django_cli_args = ['xos-migrate.py', "makemigrations"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800390
391 # generate the code for each service and create a list of parameters to pass to django
392 app_list = []
393 for service in args.service_names:
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800394 # NOTE we need core models to be there as all the services depend on them
395 generate_core_models(core_dir)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800396 if service == "core":
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800397 django_cli_args.append("core")
398 else:
399 service_dir = os.path.join(service_base_dir, service)
400 service_name = get_service_name_from_config(service_dir)
401 generate_service_models(service_dir, service_dest_dir, service_name)
402 app_list.append("services.%s" % service_name)
403
404 django_cli_args.append(service_name)
405
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800406 if len(app_list) > 0:
407 os.environ["INSTALLED_APPS"] = ",".join(app_list)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800408
409 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
410
411 monkey_patch_migration_template()
412
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800413 if args.check:
414 django_cli_args.append("--check")
415 django_cli_args.append("--dry-run")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800416
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800417 from django.core.management import execute_from_command_line
418
419 try:
420 log.debug("Django CLI Args", args=django_cli_args)
421 execute_from_command_line(django_cli_args)
422 returncode = 0
423 except SystemExit as e:
424 returncode = e.message
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800425
426 if returncode != 0:
427 if args.check:
428 log.error("Migrations are not up to date with the service changes!")
429 else:
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800430 log.error("An error occurred")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800431 sys.exit(returncode)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800432
433 # copying migrations back to the service
434 for service in args.service_names:
435 if service == "core":
436 # we don't need to copy migrations for the core
437 continue
438 else:
439 service_dir = os.path.join(service_base_dir, service)
440 service_name = get_service_name_from_config(service_dir)
441 copy_service_migrations(service_dir, service_dest_dir, service_name)
442
443 # restore orginal sys.path
444 sys.path = original_sys_path