blob: 6e0ca77b1f5cfbe8996231e8fb14ffbfcbd45040 [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
23# - add support for services that are in the cord/orchestration/profiles folders
24# - add support to specify a name to be given to the generated migration (--name parameter in django makemigrations)
25# - add support to generate empty migrations (needed for data-only migrations)
26
27import os
28import sys
29import argparse
30import yaml
31import shutil
32from xosgenx.generator import XOSProcessor, XOSProcessorArgs
33from xosconfig import Config
Matteo Scandolo1cda4352019-02-19 16:02:42 -080034import subprocess
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080035from multistructlog import create_logger
36
37
38def get_abs_path(dir_):
39 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_)
44 return os.path.dirname(os.path.realpath(__file__)) + "/" + dir_
45
46
47def print_banner(root):
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.info(r" /____/ ")
55 log.info(r"---------------------------------------------------------------")
56 log.debug("CORD repo root", root=root)
57 log.debug("Storing logs in: %s" % os.environ["LOG_FILE"])
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080058 log.debug(r"---------------------------------------------------------------")
59
60
61def generate_core_models(core_dir):
62 core_xproto = os.path.join(core_dir, "core.xproto")
63
64 args = XOSProcessorArgs(
65 output=core_dir,
66 target="django.xtarget",
67 dest_extension="py",
68 write_to_file="model",
69 files=[core_xproto],
70 )
71 XOSProcessor.process(args)
72
73 security_args = XOSProcessorArgs(
74 output=core_dir,
75 target="django-security.xtarget",
76 dest_file="security.py",
77 write_to_file="single",
78 files=[core_xproto],
79 )
80
81 XOSProcessor.process(security_args)
82
83 init_args = XOSProcessorArgs(
84 output=core_dir,
85 target="init.xtarget",
86 dest_file="__init__.py",
87 write_to_file="single",
88 files=[core_xproto],
89 )
90 XOSProcessor.process(init_args)
91
92
93def find_xproto_in_folder(path):
94 """
95 Recursively iterate a folder tree to look for any xProto file.
96 We use this function in case that the name of the xProto is different from the name of the folder (eg: olt-service)
97 :param path: the root folder to start the search
98 :return: [string]
99 """
100 xprotos = []
101 for fn in os.listdir(path):
102 # skip hidden files and folders. plus other useless things
103 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
104 continue
105 full_path = os.path.join(path, fn)
106 if fn.endswith(".xproto"):
107 xprotos.append(full_path)
108 elif os.path.isdir(full_path):
109 xprotos = xprotos + find_xproto_in_folder(full_path)
110 return xprotos
111
112
113def find_decls_models(path):
114 """
115 Recursively iterate a folder tree to look for any models.py file.
116 This files contain the base model for _decl generated models.
117 :param path: the root folder to start the search
118 :return: [string]
119 """
120 decls = []
121 for fn in os.listdir(path):
122 # skip hidden files and folders. plus other useless things
123 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
124 continue
125 full_path = os.path.join(path, fn)
126 if fn == "models.py":
127 decls.append(full_path)
128 elif os.path.isdir(full_path):
129 decls = decls + find_decls_models(full_path)
130 return decls
131
132
133def get_service_name_from_config(path):
134 """
135 Given a service folder look for the config.yaml file and find the name
136 :param path: the root folder to start the search
137 :return: string
138 """
139 config = os.path.join(path, "xos/synchronizer/config.yaml")
140 if not os.path.isfile(config):
141 raise Exception("Config file not found at: %s" % config)
142
143 cfg_file = open(config)
144 cfg = yaml.load(cfg_file)
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800145 return cfg["name"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800146
147
148def generate_service_models(service_dir, service_dest_dir, service_name):
149 """
150 Generate the django code starting from xProto for a given service.
151 :param service_dir: string (path to the folder)
152 :param service_name: string (name of the service)
153 :return: void
154 """
155 xprotos = find_xproto_in_folder(service_dir)
156 decls = find_decls_models(service_dir)
157 log.debug("Generating models for %s from files %s" % (service_name, ", ".join(xprotos)))
158 out_dir = os.path.join(service_dest_dir, service_name)
159 if not os.path.isdir(out_dir):
160 os.mkdir(out_dir)
161
162 args = XOSProcessorArgs(
163 output=out_dir,
164 files=xprotos,
165 target="service.xtarget",
166 write_to_file="target",
167 )
168 XOSProcessor.process(args)
169
170 security_args = XOSProcessorArgs(
171 output=out_dir,
172 target="django-security.xtarget",
173 dest_file="security.py",
174 write_to_file="single",
175 files=xprotos,
176 )
177
178 XOSProcessor.process(security_args)
179
180 init_py_filename = os.path.join(out_dir, "__init__.py")
181 if not os.path.exists(init_py_filename):
182 open(init_py_filename, "w").write("# created by dynamicbuild")
183
184 # copy over models.py files from the service
185 if len(decls) > 0:
186 for file in decls:
187 fn = os.path.basename(file)
188 src_fn = file
189 dest_fn = os.path.join(out_dir, fn)
190 log.debug("Copying models.py from %s to %s" % (src_fn, dest_fn))
191 shutil.copyfile(src_fn, dest_fn)
192
193 # copy existing migrations from the service, otherwise they won't be incremental
194 src_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
195 if os.path.isdir(src_dir):
196 dest_dir = os.path.join(out_dir, "migrations")
197 if os.path.isdir(dest_dir):
198 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
199 shutil.copytree(src_dir, dest_dir)
200
201
202def copy_service_migrations(service_dir, service_dest_dir, service_name):
203 """
204 Once the migrations are generated, copy them in the correct location
205 :param service_dir: string (path to the folder)
206 :param service_name: string (name of the service)
207 :return: void
208 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800209 log.debug("Copying %s migrations to %s" % (service_name, service_dir))
210 migration_dir = os.path.join(service_dest_dir, service_name, "migrations")
211 dest_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
212 if os.path.isdir(dest_dir):
213 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
214 shutil.copytree(migration_dir, dest_dir)
Matteo Scandoloebd26052019-02-14 10:06:41 -0800215 # clean after the tool, generated migrations has been moved in the service repo
216 shutil.rmtree(get_abs_path(os.path.join(migration_dir, "../")))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800217
218
219def monkey_patch_migration_template():
220 import django
221 django.setup()
222
223 import django.db.migrations.writer as dj
224 dj.MIGRATION_TEMPLATE = """\
225# Copyright 2017-present Open Networking Foundation
226#
227# Licensed under the Apache License, Version 2.0 (the "License");
228# you may not use this file except in compliance with the License.
229# You may obtain a copy of the License at
230#
231# http://www.apache.org/licenses/LICENSE-2.0
232#
233# Unless required by applicable law or agreed to in writing, software
234# distributed under the License is distributed on an "AS IS" BASIS,
235# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
236# See the License for the specific language governing permissions and
237# limitations under the License.
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800238
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800239# -*- coding: utf-8 -*-
240# Generated by Django %(version)s on %(timestamp)s
241from __future__ import unicode_literals
242
243%(imports)s
244
245class Migration(migrations.Migration):
246%(replaces_str)s%(initial_str)s
247 dependencies = [
248%(dependencies)s\
249 ]
250
251 operations = [
252%(operations)s\
253 ]
254"""
255
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800256
257def configure_logging(verbose):
258 global log
259 # INITIALIZING LOGGER
260 Config.init()
261
262 cfg = Config().get("logging")
263 if verbose:
264 cfg["handlers"]["console"]["level"] = "DEBUG"
265
266 log = create_logger(cfg)
267
268
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800269# SETTING ENV
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800270os.environ["LOG_FILE"] = get_abs_path("django.log")
271os.environ["XOS_CONFIG_SCHEMA"] = get_abs_path("migration_cfg_schema.yaml")
272os.environ["XOS_CONFIG_FILE"] = get_abs_path("migration_cfg.yaml")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800273os.environ["MIGRATIONS"] = "true"
274# this is populated in case we generate migrations for services and it's used in settings.py
275os.environ["INSTALLED_APPS"] = ""
276
277# PARAMS
278parser = argparse.ArgumentParser(description="XOS Migrations")
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800279required = parser.add_argument_group("required arguments")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800280
281required.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800282 "-s",
283 "--service",
284 action="append",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800285 required=True,
286 dest="service_names",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800287 help="The name of the folder containing the service in cord/orchestration/xos_services"
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800288)
289
290parser.add_argument(
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800291 "-r",
292 "--repo",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800293 default=get_abs_path("~/cord"),
294 dest="repo_root",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800295 help="The location of the folder containing the CORD repo root (default to ~/cord)"
296)
297
298parser.add_argument(
299 "--check",
300 default=False,
301 action="store_true",
302 dest="check",
303 help="Check if the migrations are generated for a given service. Does not apply any change."
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800304)
305
306# FIXME this is not working with multistructlog
307parser.add_argument(
308 "-v",
309 "--verbose",
310 help="increase log verbosity",
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800311 dest="verbose",
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800312 action="store_true"
313)
314
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800315
316def run():
317
318 args = parser.parse_args()
319
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800320 configure_logging(args.verbose)
321
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800322 print_banner(args.repo_root)
323
324 # find absolute path to the code
325 xos_path = get_abs_path(os.path.join(args.repo_root, "orchestration/xos/xos/"))
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800326 django_path = get_abs_path(os.path.join(xos_path, "manage.py"))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800327 core_dir = get_abs_path(os.path.join(xos_path, "core/models/"))
328 service_base_dir = get_abs_path(os.path.join(xos_path, "../../xos_services/"))
329 service_dest_dir = get_abs_path(os.path.join(xos_path, "services/"))
330
331 # we need to append the xos folder to sys.path
332 original_sys_path = sys.path
333 sys.path.append(xos_path)
334
335 log.info("Services: %s" % ", ".join(args.service_names))
336
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800337 django_cli_args = ["makemigrations"]
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800338
339 # generate the code for each service and create a list of parameters to pass to django
340 app_list = []
341 for service in args.service_names:
342 if service == "core":
343
344 generate_core_models(core_dir)
345 django_cli_args.append("core")
346 else:
347 service_dir = os.path.join(service_base_dir, service)
348 service_name = get_service_name_from_config(service_dir)
349 generate_service_models(service_dir, service_dest_dir, service_name)
350 app_list.append("services.%s" % service_name)
351
352 django_cli_args.append(service_name)
353
354 os.environ["INSTALLED_APPS"] = ",".join(app_list)
355
356 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
357
358 monkey_patch_migration_template()
359
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800360 if args.check:
361 django_cli_args.append("--check")
362 django_cli_args.append("--dry-run")
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800363
Matteo Scandolo1cda4352019-02-19 16:02:42 -0800364 cmd = "python %s %s" % (django_path, " ".join(django_cli_args))
365 result = subprocess.Popen(cmd, shell=True)
366 result.wait()
367 returncode = result.returncode
368
369 if returncode != 0:
370 if args.check:
371 log.error("Migrations are not up to date with the service changes!")
372 else:
373 log.error(result.communicate()[0])
374 sys.exit(returncode)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800375
376 # copying migrations back to the service
377 for service in args.service_names:
378 if service == "core":
379 # we don't need to copy migrations for the core
380 continue
381 else:
382 service_dir = os.path.join(service_base_dir, service)
383 service_name = get_service_name_from_config(service_dir)
384 copy_service_migrations(service_dir, service_dest_dir, service_name)
385
386 # restore orginal sys.path
387 sys.path = original_sys_path