blob: 562ee38e0838d68cc908f764770ef0dcfcda5985 [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
Zack Williams5c2ea232019-01-30 15:23:01 -070035
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080036def get_abs_path(dir_):
Scott Baker63c27ba2019-03-01 16:06:15 -080037 """ Convert a path specified by the user, which might be relative or based on
38 home directory location, into an absolute path.
39 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080040 if os.path.isabs(dir_):
Matteo Scandoloebd26052019-02-14 10:06:41 -080041 return os.path.realpath(dir_)
Matteo Scandolo1cda4352019-02-19 16:02:42 -080042 if dir_[0] == "~" and not os.path.exists(dir_):
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080043 dir_ = os.path.expanduser(dir_)
44 return os.path.abspath(dir_)
Scott Baker63c27ba2019-03-01 16:06:15 -080045 return os.path.abspath(os.path.join(os.getcwd(), dir_))
46
47
48def get_migration_library_path(dir_):
49 """ Return a directory relative to the location of the migration library """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080050 return os.path.dirname(os.path.realpath(__file__)) + "/" + dir_
51
52
53def print_banner(root):
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.info(r"---------------------------------------------------------------")
62 log.debug("CORD repo root", root=root)
63 log.debug("Storing logs in: %s" % os.environ["LOG_FILE"])
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080064 log.debug(r"---------------------------------------------------------------")
65
66
67def generate_core_models(core_dir):
68 core_xproto = os.path.join(core_dir, "core.xproto")
69
70 args = XOSProcessorArgs(
71 output=core_dir,
72 target="django.xtarget",
73 dest_extension="py",
74 write_to_file="model",
75 files=[core_xproto],
76 )
77 XOSProcessor.process(args)
78
79 security_args = XOSProcessorArgs(
80 output=core_dir,
81 target="django-security.xtarget",
82 dest_file="security.py",
83 write_to_file="single",
84 files=[core_xproto],
85 )
86
87 XOSProcessor.process(security_args)
88
89 init_args = XOSProcessorArgs(
90 output=core_dir,
91 target="init.xtarget",
92 dest_file="__init__.py",
93 write_to_file="single",
94 files=[core_xproto],
95 )
96 XOSProcessor.process(init_args)
97
98
99def find_xproto_in_folder(path):
100 """
101 Recursively iterate a folder tree to look for any xProto file.
102 We use this function in case that the name of the xProto is different from the name of the folder (eg: olt-service)
103 :param path: the root folder to start the search
104 :return: [string]
105 """
106 xprotos = []
107 for fn in os.listdir(path):
108 # skip hidden files and folders. plus other useless things
109 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
110 continue
111 full_path = os.path.join(path, fn)
112 if fn.endswith(".xproto"):
113 xprotos.append(full_path)
114 elif os.path.isdir(full_path):
115 xprotos = xprotos + find_xproto_in_folder(full_path)
116 return xprotos
117
118
119def find_decls_models(path):
120 """
121 Recursively iterate a folder tree to look for any models.py file.
122 This files contain the base model for _decl generated models.
123 :param path: the root folder to start the search
124 :return: [string]
125 """
126 decls = []
127 for fn in os.listdir(path):
128 # skip hidden files and folders. plus other useless things
129 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
130 continue
131 full_path = os.path.join(path, fn)
132 if fn == "models.py":
133 decls.append(full_path)
134 elif os.path.isdir(full_path):
135 decls = decls + find_decls_models(full_path)
136 return decls
137
138
139def get_service_name_from_config(path):
140 """
141 Given a service folder look for the config.yaml file and find the name
142 :param path: the root folder to start the search
143 :return: string
144 """
145 config = os.path.join(path, "xos/synchronizer/config.yaml")
146 if not os.path.isfile(config):
147 raise Exception("Config file not found at: %s" % config)
148
149 cfg_file = open(config)
Zack Williams9a42f872019-02-15 17:56:04 -0700150 cfg = yaml.safe_load(cfg_file)
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800151 return cfg["name"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800152
153
154def generate_service_models(service_dir, service_dest_dir, service_name):
155 """
156 Generate the django code starting from xProto for a given service.
157 :param service_dir: string (path to the folder)
158 :param service_name: string (name of the service)
159 :return: void
160 """
161 xprotos = find_xproto_in_folder(service_dir)
162 decls = find_decls_models(service_dir)
163 log.debug("Generating models for %s from files %s" % (service_name, ", ".join(xprotos)))
164 out_dir = os.path.join(service_dest_dir, service_name)
165 if not os.path.isdir(out_dir):
166 os.mkdir(out_dir)
167
168 args = XOSProcessorArgs(
169 output=out_dir,
170 files=xprotos,
171 target="service.xtarget",
172 write_to_file="target",
173 )
174 XOSProcessor.process(args)
175
176 security_args = XOSProcessorArgs(
177 output=out_dir,
178 target="django-security.xtarget",
179 dest_file="security.py",
180 write_to_file="single",
181 files=xprotos,
182 )
183
184 XOSProcessor.process(security_args)
185
186 init_py_filename = os.path.join(out_dir, "__init__.py")
187 if not os.path.exists(init_py_filename):
188 open(init_py_filename, "w").write("# created by dynamicbuild")
189
190 # copy over models.py files from the service
191 if len(decls) > 0:
192 for file in decls:
193 fn = os.path.basename(file)
194 src_fn = file
195 dest_fn = os.path.join(out_dir, fn)
196 log.debug("Copying models.py from %s to %s" % (src_fn, dest_fn))
197 shutil.copyfile(src_fn, dest_fn)
198
199 # copy existing migrations from the service, otherwise they won't be incremental
200 src_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
201 if os.path.isdir(src_dir):
202 dest_dir = os.path.join(out_dir, "migrations")
203 if os.path.isdir(dest_dir):
204 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
205 shutil.copytree(src_dir, dest_dir)
206
207
208def copy_service_migrations(service_dir, service_dest_dir, service_name):
209 """
210 Once the migrations are generated, copy them in the correct location
211 :param service_dir: string (path to the folder)
212 :param service_name: string (name of the service)
213 :return: void
214 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800215 log.debug("Copying %s migrations to %s" % (service_name, service_dir))
216 migration_dir = os.path.join(service_dest_dir, service_name, "migrations")
217 dest_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
218 if os.path.isdir(dest_dir):
219 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
220 shutil.copytree(migration_dir, dest_dir)
Matteo Scandoloebd26052019-02-14 10:06:41 -0800221 # clean after the tool, generated migrations has been moved in the service repo
222 shutil.rmtree(get_abs_path(os.path.join(migration_dir, "../")))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800223
224
225def monkey_patch_migration_template():
226 import django
227 django.setup()
228
229 import django.db.migrations.writer as dj
230 dj.MIGRATION_TEMPLATE = """\
231# Copyright 2017-present Open Networking Foundation
232#
233# Licensed under the Apache License, Version 2.0 (the "License");
234# you may not use this file except in compliance with the License.
235# You may obtain a copy of the License at
236#
237# http://www.apache.org/licenses/LICENSE-2.0
238#
239# Unless required by applicable law or agreed to in writing, software
240# distributed under the License is distributed on an "AS IS" BASIS,
241# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
242# See the License for the specific language governing permissions and
243# limitations under the License.
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800244
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800245# -*- coding: utf-8 -*-
246# Generated by Django %(version)s on %(timestamp)s
247from __future__ import unicode_literals
248
249%(imports)s
250
251class Migration(migrations.Migration):
252%(replaces_str)s%(initial_str)s
253 dependencies = [
254%(dependencies)s\
255 ]
256
257 operations = [
258%(operations)s\
259 ]
260"""
261
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800262
263def configure_logging(verbose):
264 global log
265 # INITIALIZING LOGGER
266 Config.init()
267
268 cfg = Config().get("logging")
269 if verbose:
270 cfg["handlers"]["console"]["level"] = "DEBUG"
271
272 log = create_logger(cfg)
273
274
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800275# SETTING ENV
Scott Baker63c27ba2019-03-01 16:06:15 -0800276os.environ["LOG_FILE"] = get_migration_library_path("django.log")
277os.environ["XOS_CONFIG_SCHEMA"] = get_migration_library_path("migration_cfg_schema.yaml")
278os.environ["XOS_CONFIG_FILE"] = get_migration_library_path("migration_cfg.yaml")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800279os.environ["MIGRATIONS"] = "true"
280# this is populated in case we generate migrations for services and it's used in settings.py
281os.environ["INSTALLED_APPS"] = ""
282
283# PARAMS
284parser = argparse.ArgumentParser(description="XOS Migrations")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800285required = parser.add_argument_group("required arguments")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800286
287required.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800288 "-s",
289 "--service",
290 action="append",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800291 required=True,
292 dest="service_names",
Matteo Scandoloe1576802019-03-06 15:23:40 -0800293 help="The name of the folder containing the service in cord/orchestration/xos-services"
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800294)
295
Zack Williams70a67e72019-03-08 12:38:51 -0700296pathgroup = parser.add_mutually_exclusive_group()
297
298pathgroup.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800299 "-r",
300 "--repo",
Zack Williams70a67e72019-03-08 12:38:51 -0700301 default=get_abs_path("../.."),
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
306pathgroup.add_argument(
307 "-x",
308 "--xos",
309 default=None,
310 dest="xos_root",
311 help="Path to directory of the XOS repo. Incompatible with '--repo' and only works for core migrations."
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800312)
313
314parser.add_argument(
315 "--check",
316 default=False,
317 action="store_true",
318 dest="check",
319 help="Check if the migrations are generated for a given service. Does not apply any change."
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800320)
321
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800322parser.add_argument(
323 "-v",
324 "--verbose",
325 help="increase log verbosity",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800326 dest="verbose",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800327 action="store_true"
328)
329
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800330
331def run():
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800332 # cleaning up from possible incorrect states
333 if "INSTALLED_APPS" in os.environ:
334 del os.environ["INSTALLED_APPS"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800335
336 args = parser.parse_args()
337
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800338 configure_logging(args.verbose)
339
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800340 print_banner(args.repo_root)
341
342 # find absolute path to the code
Zack Williams70a67e72019-03-08 12:38:51 -0700343 if args.xos_root: # if args.xos_root is set, testing only the core
344 xos_path = get_abs_path(args.xos_root)
345 if args.service_names != ["core"]:
346 log.error("When using --xos, can only check the core models")
347 sys.exit(1)
348 else:
349 xos_path = get_abs_path(os.path.join(args.repo_root, "orchestration/xos/xos/"))
350 service_base_dir = get_abs_path(os.path.join(xos_path, "../../xos_services/"))
351 service_dest_dir = get_abs_path(os.path.join(xos_path, "services/"))
352
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800353 core_dir = get_abs_path(os.path.join(xos_path, "core/models/"))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800354
355 # we need to append the xos folder to sys.path
356 original_sys_path = sys.path
357 sys.path.append(xos_path)
358
359 log.info("Services: %s" % ", ".join(args.service_names))
360
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800361 django_cli_args = ['xos-migrate.py', "makemigrations"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800362
363 # generate the code for each service and create a list of parameters to pass to django
364 app_list = []
365 for service in args.service_names:
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800366 # NOTE we need core models to be there as all the services depend on them
367 generate_core_models(core_dir)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800368 if service == "core":
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800369 django_cli_args.append("core")
370 else:
371 service_dir = os.path.join(service_base_dir, service)
372 service_name = get_service_name_from_config(service_dir)
373 generate_service_models(service_dir, service_dest_dir, service_name)
374 app_list.append("services.%s" % service_name)
375
376 django_cli_args.append(service_name)
377
Matteo Scandolo43af45b2019-02-21 15:57:02 -0800378 if len(app_list) > 0:
379 os.environ["INSTALLED_APPS"] = ",".join(app_list)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800380
381 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
382
383 monkey_patch_migration_template()
384
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800385 if args.check:
386 django_cli_args.append("--check")
387 django_cli_args.append("--dry-run")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800388
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800389 from django.core.management import execute_from_command_line
390
391 try:
392 log.debug("Django CLI Args", args=django_cli_args)
393 execute_from_command_line(django_cli_args)
394 returncode = 0
395 except SystemExit as e:
396 returncode = e.message
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800397
398 if returncode != 0:
399 if args.check:
400 log.error("Migrations are not up to date with the service changes!")
401 else:
Matteo Scandolo6e2bd822019-02-20 17:22:39 -0800402 log.error("An error occurred")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800403 sys.exit(returncode)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800404
405 # copying migrations back to the service
406 for service in args.service_names:
407 if service == "core":
408 # we don't need to copy migrations for the core
409 continue
410 else:
411 service_dir = os.path.join(service_base_dir, service)
412 service_name = get_service_name_from_config(service_dir)
413 copy_service_migrations(service_dir, service_dest_dir, service_name)
414
415 # restore orginal sys.path
416 sys.path = original_sys_path