blob: 8aab3130ad0c73bbd6b908e57204ad353e11cf85 [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):
Zack Williams23938ab2019-03-15 14:33:10 -0700110 # skip hidden files and folders
111 if fn.startswith("."):
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800112 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):
Zack Williams23938ab2019-03-15 14:33:10 -0700130 # skip hidden files and folders
131 if fn.startswith("."):
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800132 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 """
Zack Williams23938ab2019-03-15 14:33:10 -0700163 sync_dir = os.path.join(service_dir, "xos/synchronizer/models")
164 xprotos = find_xproto_in_folder(sync_dir)
165 decls = find_decls_models(sync_dir)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800166 log.debug("Generating models for %s from files %s" % (service_name, ", ".join(xprotos)))
167 out_dir = os.path.join(service_dest_dir, service_name)
168 if not os.path.isdir(out_dir):
169 os.mkdir(out_dir)
170
171 args = XOSProcessorArgs(
172 output=out_dir,
173 files=xprotos,
174 target="service.xtarget",
175 write_to_file="target",
176 )
177 XOSProcessor.process(args)
178
179 security_args = XOSProcessorArgs(
180 output=out_dir,
181 target="django-security.xtarget",
182 dest_file="security.py",
183 write_to_file="single",
184 files=xprotos,
185 )
186
187 XOSProcessor.process(security_args)
188
189 init_py_filename = os.path.join(out_dir, "__init__.py")
190 if not os.path.exists(init_py_filename):
191 open(init_py_filename, "w").write("# created by dynamicbuild")
192
193 # copy over models.py files from the service
194 if len(decls) > 0:
195 for file in decls:
196 fn = os.path.basename(file)
197 src_fn = file
198 dest_fn = os.path.join(out_dir, fn)
199 log.debug("Copying models.py from %s to %s" % (src_fn, dest_fn))
200 shutil.copyfile(src_fn, dest_fn)
201
202 # copy existing migrations from the service, otherwise they won't be incremental
203 src_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
204 if os.path.isdir(src_dir):
205 dest_dir = os.path.join(out_dir, "migrations")
206 if os.path.isdir(dest_dir):
207 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
208 shutil.copytree(src_dir, dest_dir)
209
210
211def copy_service_migrations(service_dir, service_dest_dir, service_name):
212 """
213 Once the migrations are generated, copy them in the correct location
214 :param service_dir: string (path to the folder)
215 :param service_name: string (name of the service)
216 :return: void
217 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800218 log.debug("Copying %s migrations to %s" % (service_name, service_dir))
219 migration_dir = os.path.join(service_dest_dir, service_name, "migrations")
220 dest_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
221 if os.path.isdir(dest_dir):
222 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
223 shutil.copytree(migration_dir, dest_dir)
Matteo Scandoloebd26052019-02-14 10:06:41 -0800224 # clean after the tool, generated migrations has been moved in the service repo
225 shutil.rmtree(get_abs_path(os.path.join(migration_dir, "../")))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800226
227
228def monkey_patch_migration_template():
229 import django
230 django.setup()
231
232 import django.db.migrations.writer as dj
233 dj.MIGRATION_TEMPLATE = """\
234# Copyright 2017-present Open Networking Foundation
235#
236# Licensed under the Apache License, Version 2.0 (the "License");
237# you may not use this file except in compliance with the License.
238# You may obtain a copy of the License at
239#
240# http://www.apache.org/licenses/LICENSE-2.0
241#
242# Unless required by applicable law or agreed to in writing, software
243# distributed under the License is distributed on an "AS IS" BASIS,
244# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
245# See the License for the specific language governing permissions and
246# limitations under the License.
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800247
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800248# -*- coding: utf-8 -*-
249# Generated by Django %(version)s on %(timestamp)s
250from __future__ import unicode_literals
251
252%(imports)s
253
254class Migration(migrations.Migration):
255%(replaces_str)s%(initial_str)s
256 dependencies = [
257%(dependencies)s\
258 ]
259
260 operations = [
261%(operations)s\
262 ]
263"""
264
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800265
266def configure_logging(verbose):
267 global log
268 # INITIALIZING LOGGER
269 Config.init()
270
271 cfg = Config().get("logging")
272 if verbose:
273 cfg["handlers"]["console"]["level"] = "DEBUG"
274
275 log = create_logger(cfg)
276
277
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800278# SETTING ENV
Scott Baker63c27ba2019-03-01 16:06:15 -0800279os.environ["LOG_FILE"] = get_migration_library_path("django.log")
280os.environ["XOS_CONFIG_SCHEMA"] = get_migration_library_path("migration_cfg_schema.yaml")
281os.environ["XOS_CONFIG_FILE"] = get_migration_library_path("migration_cfg.yaml")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800282os.environ["MIGRATIONS"] = "true"
283# this is populated in case we generate migrations for services and it's used in settings.py
284os.environ["INSTALLED_APPS"] = ""
285
286# PARAMS
287parser = argparse.ArgumentParser(description="XOS Migrations")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800288required = parser.add_argument_group("required arguments")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800289
290required.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800291 "-s",
292 "--service",
293 action="append",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800294 required=True,
295 dest="service_names",
Matteo Scandoloe1576802019-03-06 15:23:40 -0800296 help="The name of the folder containing the service in cord/orchestration/xos-services"
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800297)
298
Matteo Scandolo8419c082019-03-11 13:54:44 -0700299parser.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800300 "-r",
301 "--repo",
Matteo Scandolo8419c082019-03-11 13:54:44 -0700302 default=REPO_ROOT,
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800303 dest="repo_root",
Zack Williams70a67e72019-03-08 12:38:51 -0700304 help="Path to the CORD repo root (defaults to '../..'). Mutually exclusive with '--xos'."
305)
306
Matteo Scandolo8419c082019-03-11 13:54:44 -0700307parser.add_argument(
Zack Williams70a67e72019-03-08 12:38:51 -0700308 "-x",
Matteo Scandolo8419c082019-03-11 13:54:44 -0700309 "--xos-dir",
Zack Williams70a67e72019-03-08 12:38:51 -0700310 default=None,
311 dest="xos_root",
Matteo Scandolo8419c082019-03-11 13:54:44 -0700312 help="Path to directory of the XOS repo. Incompatible with '--repo'."
313)
314
315parser.add_argument(
316 "--services-dir",
317 default=None,
318 dest="services_root",
319 help="Path to directory of the XOS services root. Incompatible with '--repo'." +
320 "Note that all the services repo needs to be siblings"
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800321)
322
323parser.add_argument(
324 "--check",
325 default=False,
326 action="store_true",
327 dest="check",
328 help="Check if the migrations are generated for a given service. Does not apply any change."
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800329)
330
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800331parser.add_argument(
332 "-v",
333 "--verbose",
334 help="increase log verbosity",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800335 dest="verbose",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800336 action="store_true"
337)
338
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800339
340def run():
Matteo Scandolo8419c082019-03-11 13:54:44 -0700341 service_base_dir = None
342
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800343 # cleaning up from possible incorrect states
344 if "INSTALLED_APPS" in os.environ:
345 del os.environ["INSTALLED_APPS"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800346
347 args = parser.parse_args()
348
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800349 configure_logging(args.verbose)
350
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800351 print_banner(args.repo_root)
352
Matteo Scandolo8419c082019-03-11 13:54:44 -0700353 # validating args, the solution is hacky but it does not fit `add_mutually_exclusive_group`
354 # and it's not complex enough for the solution proposed here:
355 # https://stackoverflow.com/questions/17909294/python-argparse-mutual-exclusive-group
356 if args.service_names != ["core"] and \
357 ((args.xos_root and not args.services_root) or (args.services_root and not args.xos_root)):
358 # if we're only generating migrations for the core,
359 # the --xos-dir is the only think we need
360 log.error("You need to set both --xos-dir and \
361 --services-dir parameters when generating migrations for a service")
362 sys.exit(1)
363
364 if (args.xos_root or args.services_root) and (args.repo_root != REPO_ROOT):
365 log.error("The --xos-dir or --services-dir parameters are not compatible with the --repo parameter")
366 sys.exit(1)
367
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800368 # find absolute path to the code
Matteo Scandolo8419c082019-03-11 13:54:44 -0700369 if args.xos_root or args.services_root:
370 xos_path = get_abs_path(os.path.join(args.xos_root, "xos"))
371 if args.services_root:
372 # NOTE this params is optional (we may be generating migrations for the core only
373 service_base_dir = get_abs_path(args.services_root)
Zack Williams70a67e72019-03-08 12:38:51 -0700374 else:
375 xos_path = get_abs_path(os.path.join(args.repo_root, "orchestration/xos/xos/"))
Matteo Scandolo8419c082019-03-11 13:54:44 -0700376 service_base_dir = get_abs_path(os.path.join(xos_path, "../../xos-services/"))
Zack Williams70a67e72019-03-08 12:38:51 -0700377
Matteo Scandolo8419c082019-03-11 13:54:44 -0700378 log.debug("XOS Path: %s" % xos_path)
379 log.debug("Service Base Dir: %s" % service_base_dir)
380
381 service_dest_dir = get_abs_path(os.path.join(xos_path, "services/"))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800382 core_dir = get_abs_path(os.path.join(xos_path, "core/models/"))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800383
384 # we need to append the xos folder to sys.path
385 original_sys_path = sys.path
386 sys.path.append(xos_path)
387
388 log.info("Services: %s" % ", ".join(args.service_names))
389
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800390 django_cli_args = ['xos-migrate.py', "makemigrations"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800391
392 # generate the code for each service and create a list of parameters to pass to django
393 app_list = []
394 for service in args.service_names:
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800395 # NOTE we need core models to be there as all the services depend on them
396 generate_core_models(core_dir)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800397 if service == "core":
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800398 django_cli_args.append("core")
399 else:
400 service_dir = os.path.join(service_base_dir, service)
401 service_name = get_service_name_from_config(service_dir)
402 generate_service_models(service_dir, service_dest_dir, service_name)
403 app_list.append("services.%s" % service_name)
404
405 django_cli_args.append(service_name)
406
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800407 if len(app_list) > 0:
408 os.environ["INSTALLED_APPS"] = ",".join(app_list)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800409
410 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
411
412 monkey_patch_migration_template()
413
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800414 if args.check:
415 django_cli_args.append("--check")
416 django_cli_args.append("--dry-run")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800417
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800418 from django.core.management import execute_from_command_line
419
420 try:
421 log.debug("Django CLI Args", args=django_cli_args)
422 execute_from_command_line(django_cli_args)
423 returncode = 0
424 except SystemExit as e:
425 returncode = e.message
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800426
427 if returncode != 0:
428 if args.check:
429 log.error("Migrations are not up to date with the service changes!")
430 else:
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800431 log.error("An error occurred")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800432 sys.exit(returncode)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800433
434 # copying migrations back to the service
435 for service in args.service_names:
436 if service == "core":
437 # we don't need to copy migrations for the core
438 continue
439 else:
440 service_dir = os.path.join(service_base_dir, service)
441 service_name = get_service_name_from_config(service_dir)
442 copy_service_migrations(service_dir, service_dest_dir, service_name)
443
444 # restore orginal sys.path
445 sys.path = original_sys_path