blob: f4620de49a45673193fb355952037cf36bc35ab0 [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
34from multistructlog import create_logger
35
36
37def get_abs_path(dir_):
38 if os.path.isabs(dir_):
Matteo Scandoloebd26052019-02-14 10:06:41 -080039 return os.path.realpath(dir_)
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080040 if dir_[0] == '~' and not os.path.exists(dir_):
41 dir_ = os.path.expanduser(dir_)
42 return os.path.abspath(dir_)
43 return os.path.dirname(os.path.realpath(__file__)) + "/" + dir_
44
45
46def print_banner(root):
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.info(r"---------------------------------------------------------------")
55 log.debug("CORD repo root", root=root)
56 log.debug("Storing logs in: %s" % os.environ["LOG_FILE"])
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -080057 log.debug(r"---------------------------------------------------------------")
58
59
60def generate_core_models(core_dir):
61 core_xproto = os.path.join(core_dir, "core.xproto")
62
63 args = XOSProcessorArgs(
64 output=core_dir,
65 target="django.xtarget",
66 dest_extension="py",
67 write_to_file="model",
68 files=[core_xproto],
69 )
70 XOSProcessor.process(args)
71
72 security_args = XOSProcessorArgs(
73 output=core_dir,
74 target="django-security.xtarget",
75 dest_file="security.py",
76 write_to_file="single",
77 files=[core_xproto],
78 )
79
80 XOSProcessor.process(security_args)
81
82 init_args = XOSProcessorArgs(
83 output=core_dir,
84 target="init.xtarget",
85 dest_file="__init__.py",
86 write_to_file="single",
87 files=[core_xproto],
88 )
89 XOSProcessor.process(init_args)
90
91
92def find_xproto_in_folder(path):
93 """
94 Recursively iterate a folder tree to look for any xProto file.
95 We use this function in case that the name of the xProto is different from the name of the folder (eg: olt-service)
96 :param path: the root folder to start the search
97 :return: [string]
98 """
99 xprotos = []
100 for fn in os.listdir(path):
101 # skip hidden files and folders. plus other useless things
102 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
103 continue
104 full_path = os.path.join(path, fn)
105 if fn.endswith(".xproto"):
106 xprotos.append(full_path)
107 elif os.path.isdir(full_path):
108 xprotos = xprotos + find_xproto_in_folder(full_path)
109 return xprotos
110
111
112def find_decls_models(path):
113 """
114 Recursively iterate a folder tree to look for any models.py file.
115 This files contain the base model for _decl generated models.
116 :param path: the root folder to start the search
117 :return: [string]
118 """
119 decls = []
120 for fn in os.listdir(path):
121 # skip hidden files and folders. plus other useless things
122 if fn.startswith(".") or fn == "venv-xos" or fn == "htmlcov":
123 continue
124 full_path = os.path.join(path, fn)
125 if fn == "models.py":
126 decls.append(full_path)
127 elif os.path.isdir(full_path):
128 decls = decls + find_decls_models(full_path)
129 return decls
130
131
132def get_service_name_from_config(path):
133 """
134 Given a service folder look for the config.yaml file and find the name
135 :param path: the root folder to start the search
136 :return: string
137 """
138 config = os.path.join(path, "xos/synchronizer/config.yaml")
139 if not os.path.isfile(config):
140 raise Exception("Config file not found at: %s" % config)
141
142 cfg_file = open(config)
143 cfg = yaml.load(cfg_file)
144 return cfg['name']
145
146
147def generate_service_models(service_dir, service_dest_dir, service_name):
148 """
149 Generate the django code starting from xProto for a given service.
150 :param service_dir: string (path to the folder)
151 :param service_name: string (name of the service)
152 :return: void
153 """
154 xprotos = find_xproto_in_folder(service_dir)
155 decls = find_decls_models(service_dir)
156 log.debug("Generating models for %s from files %s" % (service_name, ", ".join(xprotos)))
157 out_dir = os.path.join(service_dest_dir, service_name)
158 if not os.path.isdir(out_dir):
159 os.mkdir(out_dir)
160
161 args = XOSProcessorArgs(
162 output=out_dir,
163 files=xprotos,
164 target="service.xtarget",
165 write_to_file="target",
166 )
167 XOSProcessor.process(args)
168
169 security_args = XOSProcessorArgs(
170 output=out_dir,
171 target="django-security.xtarget",
172 dest_file="security.py",
173 write_to_file="single",
174 files=xprotos,
175 )
176
177 XOSProcessor.process(security_args)
178
179 init_py_filename = os.path.join(out_dir, "__init__.py")
180 if not os.path.exists(init_py_filename):
181 open(init_py_filename, "w").write("# created by dynamicbuild")
182
183 # copy over models.py files from the service
184 if len(decls) > 0:
185 for file in decls:
186 fn = os.path.basename(file)
187 src_fn = file
188 dest_fn = os.path.join(out_dir, fn)
189 log.debug("Copying models.py from %s to %s" % (src_fn, dest_fn))
190 shutil.copyfile(src_fn, dest_fn)
191
192 # copy existing migrations from the service, otherwise they won't be incremental
193 src_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
194 if os.path.isdir(src_dir):
195 dest_dir = os.path.join(out_dir, "migrations")
196 if os.path.isdir(dest_dir):
197 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
198 shutil.copytree(src_dir, dest_dir)
199
200
201def copy_service_migrations(service_dir, service_dest_dir, service_name):
202 """
203 Once the migrations are generated, copy them in the correct location
204 :param service_dir: string (path to the folder)
205 :param service_name: string (name of the service)
206 :return: void
207 """
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800208 log.debug("Copying %s migrations to %s" % (service_name, service_dir))
209 migration_dir = os.path.join(service_dest_dir, service_name, "migrations")
210 dest_dir = os.path.join(service_dir, "xos", "synchronizer", "migrations")
211 if os.path.isdir(dest_dir):
212 shutil.rmtree(dest_dir) # empty the folder, we'll copy everything again
213 shutil.copytree(migration_dir, dest_dir)
Matteo Scandoloebd26052019-02-14 10:06:41 -0800214 # clean after the tool, generated migrations has been moved in the service repo
215 shutil.rmtree(get_abs_path(os.path.join(migration_dir, "../")))
Matteo Scandolo57fdb4b2019-02-06 18:27:56 -0800216
217
218def monkey_patch_migration_template():
219 import django
220 django.setup()
221
222 import django.db.migrations.writer as dj
223 dj.MIGRATION_TEMPLATE = """\
224# Copyright 2017-present Open Networking Foundation
225#
226# Licensed under the Apache License, Version 2.0 (the "License");
227# you may not use this file except in compliance with the License.
228# You may obtain a copy of the License at
229#
230# http://www.apache.org/licenses/LICENSE-2.0
231#
232# Unless required by applicable law or agreed to in writing, software
233# distributed under the License is distributed on an "AS IS" BASIS,
234# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
235# See the License for the specific language governing permissions and
236# limitations under the License.
237
238# -*- coding: utf-8 -*-
239# Generated by Django %(version)s on %(timestamp)s
240from __future__ import unicode_literals
241
242%(imports)s
243
244class Migration(migrations.Migration):
245%(replaces_str)s%(initial_str)s
246 dependencies = [
247%(dependencies)s\
248 ]
249
250 operations = [
251%(operations)s\
252 ]
253"""
254
255# SETTING ENV
256os.environ['LOG_FILE'] = get_abs_path("django.log")
257os.environ['XOS_CONFIG_SCHEMA'] = get_abs_path("migration_cfg_schema.yaml")
258os.environ['XOS_CONFIG_FILE'] = get_abs_path("migration_cfg.yaml")
259os.environ["MIGRATIONS"] = "true"
260# this is populated in case we generate migrations for services and it's used in settings.py
261os.environ["INSTALLED_APPS"] = ""
262
263# PARAMS
264parser = argparse.ArgumentParser(description="XOS Migrations")
265required = parser.add_argument_group('required arguments')
266
267required.add_argument(
268 '-s',
269 '--service',
270 action='append',
271 required=True,
272 dest="service_names",
273 help='The name of the folder containing the service in cord/orchestration/xos_services'
274)
275
276parser.add_argument(
277 '-r',
278 '--repo',
279 default=get_abs_path("~/cord"),
280 dest="repo_root",
281 help='The location of the folder containing the CORD repo root (default to ~/cord)'
282)
283
284# FIXME this is not working with multistructlog
285parser.add_argument(
286 "-v",
287 "--verbose",
288 help="increase log verbosity",
289 action="store_true"
290)
291
292# INITIALIZING LOGGER
293Config.init()
294log = create_logger(Config().get('logging'))
295
296
297def run():
298
299 args = parser.parse_args()
300
301 print_banner(args.repo_root)
302
303 # find absolute path to the code
304 xos_path = get_abs_path(os.path.join(args.repo_root, "orchestration/xos/xos/"))
305 core_dir = get_abs_path(os.path.join(xos_path, "core/models/"))
306 service_base_dir = get_abs_path(os.path.join(xos_path, "../../xos_services/"))
307 service_dest_dir = get_abs_path(os.path.join(xos_path, "services/"))
308
309 # we need to append the xos folder to sys.path
310 original_sys_path = sys.path
311 sys.path.append(xos_path)
312
313 log.info("Services: %s" % ", ".join(args.service_names))
314
315 django_cli_args = ['xos-migrate.py', 'makemigrations']
316
317 # generate the code for each service and create a list of parameters to pass to django
318 app_list = []
319 for service in args.service_names:
320 if service == "core":
321
322 generate_core_models(core_dir)
323 django_cli_args.append("core")
324 else:
325 service_dir = os.path.join(service_base_dir, service)
326 service_name = get_service_name_from_config(service_dir)
327 generate_service_models(service_dir, service_dest_dir, service_name)
328 app_list.append("services.%s" % service_name)
329
330 django_cli_args.append(service_name)
331
332 os.environ["INSTALLED_APPS"] = ",".join(app_list)
333
334 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
335
336 monkey_patch_migration_template()
337
338 from django.core.management import execute_from_command_line
339
340 execute_from_command_line(django_cli_args)
341
342 # copying migrations back to the service
343 for service in args.service_names:
344 if service == "core":
345 # we don't need to copy migrations for the core
346 continue
347 else:
348 service_dir = os.path.join(service_base_dir, service)
349 service_name = get_service_name_from_config(service_dir)
350 copy_service_migrations(service_dir, service_dest_dir, service_name)
351
352 # restore orginal sys.path
353 sys.path = original_sys_path