blob: 278a309eeb82afdeb8a3495911cad27dee694c5a [file] [log] [blame]
Matteo Scandolod2044a42017-08-07 16:08:28 -07001# Copyright 2017-present Open Networking Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Zack Williams9a42f872019-02-15 17:56:04 -070015from __future__ import absolute_import, print_function
Matteo Scandolod2044a42017-08-07 16:08:28 -070016
Matteo Scandolo67654fa2017-06-09 09:33:17 -070017import os
Zack Williams9a42f872019-02-15 17:56:04 -070018import jinja2
19import plyxproto.parser as plyxproto
Matteo Scandolo67654fa2017-06-09 09:33:17 -070020import yaml
Sapan Bhatia85b71012018-01-12 12:11:19 -050021from colorama import Fore
Scott Baker7ae3a8f2019-03-05 16:24:14 -080022import sys
Matteo Scandolo67654fa2017-06-09 09:33:17 -070023
Zack Williams9a42f872019-02-15 17:56:04 -070024from . import jinja2_extensions
25from .proto2xproto import Proto2XProto
26from .xos2jinja import XOS2Jinja
Scott Baker7ae3a8f2019-03-05 16:24:14 -080027from .validator import XProtoValidator
Zack Williams9a42f872019-02-15 17:56:04 -070028
Zack Williams045b63d2019-01-22 16:30:57 -070029loader = jinja2.PackageLoader(__name__, "templates")
Matteo Scandolo67654fa2017-06-09 09:33:17 -070030env = jinja2.Environment(loader=loader)
31
Zack Williams045b63d2019-01-22 16:30:57 -070032
Scott Baker1f7791d2018-10-04 13:21:20 -070033class XOSProcessorArgs:
34 """ Helper class for use cases that want to call XOSProcessor directly, rather than executing xosgenx from the
35 command line.
36 """
37
38 default_rev = False
39 default_output = None
40 default_attic = None
41 default_kvpairs = None
42 default_write_to_file = None
43 default_dest_file = None
44 default_dest_extension = None
45 default_target = None
46 default_checkers = None
Zack Williams045b63d2019-01-22 16:30:57 -070047 default_verbosity = (
48 0
49 ) # Higher numbers = more verbosity, lower numbers = less verbosity
50 default_include_models = (
51 []
52 ) # If neither include_models nor include_apps is specified, then all models will
53 default_include_apps = [] # be included.
Scott Baker7ae3a8f2019-03-05 16:24:14 -080054 default_strict_validation = False
Scott Baker08d10402019-04-08 16:19:59 -070055 default_lint = False
Scott Baker1f7791d2018-10-04 13:21:20 -070056
57 def __init__(self, **kwargs):
58 # set defaults
59 self.rev = XOSProcessorArgs.default_rev
60 self.output = XOSProcessorArgs.default_output
61 self.attic = XOSProcessorArgs.default_attic
62 self.kvpairs = XOSProcessorArgs.default_kvpairs
63 self.verbosity = XOSProcessorArgs.default_verbosity
64 self.write_to_file = XOSProcessorArgs.default_write_to_file
65 self.default_dest_file = XOSProcessorArgs.default_dest_file
66 self.default_dest_extension = XOSProcessorArgs.default_dest_extension
67 self.default_target = XOSProcessorArgs.default_target
68 self.default_checkers = XOSProcessorArgs.default_target
69 self.include_models = XOSProcessorArgs.default_include_models
70 self.include_apps = XOSProcessorArgs.default_include_apps
Scott Baker7ae3a8f2019-03-05 16:24:14 -080071 self.strict_validation = XOSProcessorArgs.default_strict_validation
Scott Baker08d10402019-04-08 16:19:59 -070072 self.lint = XOSProcessorArgs.default_lint
Scott Baker1f7791d2018-10-04 13:21:20 -070073
74 # override defaults with kwargs
Zack Williams045b63d2019-01-22 16:30:57 -070075 for (k, v) in kwargs.items():
Scott Baker1f7791d2018-10-04 13:21:20 -070076 setattr(self, k, v)
77
Matteo Scandolo67654fa2017-06-09 09:33:17 -070078
Zack Williams045b63d2019-01-22 16:30:57 -070079class XOSProcessor:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070080 @staticmethod
81 def _read_input_from_files(files):
Scott Baker7ae3a8f2019-03-05 16:24:14 -080082 """ Read the files and return the combined text read.
83
84 Also returns a list of (line_number, filename) tuples that tell which
85 starting line corresponds to each file.
86 """
87 line_map = []
Zack Williams045b63d2019-01-22 16:30:57 -070088 input = ""
Matteo Scandolo67654fa2017-06-09 09:33:17 -070089 for fname in files:
90 with open(fname) as infile:
Zack Williams5c2ea232019-01-30 15:23:01 -070091 line_map.append((len(input.split("\n")), fname))
Matteo Scandolo67654fa2017-06-09 09:33:17 -070092 input += infile.read()
Scott Baker7ae3a8f2019-03-05 16:24:14 -080093 return (input, line_map)
Matteo Scandolo67654fa2017-06-09 09:33:17 -070094
95 @staticmethod
96 def _attach_parser(ast, args):
Zack Williams045b63d2019-01-22 16:30:57 -070097 if hasattr(args, "rev") and args.rev:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070098 v = Proto2XProto()
99 ast.accept(v)
Sapan Bhatia4c835602017-07-14 01:13:17 -0400100
Scott Bakerc237f882018-09-28 14:12:47 -0700101 v = XOS2Jinja(args)
Sapan Bhatia4c835602017-07-14 01:13:17 -0400102 ast.accept(v)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700103 return v
104
105 @staticmethod
106 def _get_template(target):
107 if not os.path.isabs(target):
Zack Williams045b63d2019-01-22 16:30:57 -0700108 return os.path.abspath(
109 os.path.dirname(os.path.realpath(__file__)) + "/targets/" + target
110 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700111 return target
112
113 @staticmethod
114 def _file_exists(attic):
115 # NOTE this method can be used in the jinja template
116 def file_exists2(name):
117 if attic is not None:
Zack Williams045b63d2019-01-22 16:30:57 -0700118 path = attic + "/" + name
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700119 else:
120 path = name
Zack Williams045b63d2019-01-22 16:30:57 -0700121 return os.path.exists(path)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700122
123 return file_exists2
124
125 @staticmethod
126 def _include_file(attic):
127 # NOTE this method can be used in the jinja template
128 def include_file2(name):
129 if attic is not None:
Zack Williams045b63d2019-01-22 16:30:57 -0700130 path = attic + "/" + name
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700131 else:
132 path = name
133 return open(path).read()
Zack Williams045b63d2019-01-22 16:30:57 -0700134
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700135 return include_file2
136
137 @staticmethod
138 def _load_jinja2_extensions(os_template_env, attic):
139
Zack Williams045b63d2019-01-22 16:30:57 -0700140 os_template_env.globals["include_file"] = XOSProcessor._include_file(
141 attic
142 ) # Generates a function
143 os_template_env.globals["file_exists"] = XOSProcessor._file_exists(
144 attic
145 ) # Generates a function
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700146
Zack Williams045b63d2019-01-22 16:30:57 -0700147 os_template_env.filters["yaml"] = yaml.dump
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700148 for f in dir(jinja2_extensions):
Zack Williams045b63d2019-01-22 16:30:57 -0700149 if f.startswith("xproto"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700150 os_template_env.globals[f] = getattr(jinja2_extensions, f)
151 return os_template_env
152
153 @staticmethod
154 def _add_context(args):
Zack Williams045b63d2019-01-22 16:30:57 -0700155 if not hasattr(args, "kv") or not args.kv:
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700156 return
157 try:
158 context = {}
Zack Williams045b63d2019-01-22 16:30:57 -0700159 for s in args.kv.split(","):
160 k, val = s.split(":")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700161 context[k] = val
162 return context
Zack Williams045b63d2019-01-22 16:30:57 -0700163 except Exception as e:
Zack Williams9a42f872019-02-15 17:56:04 -0700164 print(e)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700165
166 @staticmethod
167 def _write_single_file(rendered, dir, dest_file, quiet):
168
169 file_name = "%s/%s" % (dir, dest_file)
Zack Williams045b63d2019-01-22 16:30:57 -0700170 file = open(file_name, "w")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700171 file.write(rendered)
172 file.close()
Zack Williams045b63d2019-01-22 16:30:57 -0700173 if not quiet:
174 print("Saved: %s" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700175
176 @staticmethod
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700177 def _write_split_target(rendered, dir, quiet):
178
179 lines = rendered.splitlines()
180 current_buffer = []
181 for l in lines:
Zack Williams045b63d2019-01-22 16:30:57 -0700182 if l.startswith("+++"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700183
184 if dir:
Zack Williams045b63d2019-01-22 16:30:57 -0700185 path = dir + "/" + l[4:].lower()
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700186
Zack Williams045b63d2019-01-22 16:30:57 -0700187 fil = open(path, "w")
188 buf = "\n".join(current_buffer)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700189
190 obuf = buf
191
192 fil.write(obuf)
193 fil.close()
194
Zack Williams045b63d2019-01-22 16:30:57 -0700195 if not quiet:
196 print("Save file to: %s" % path)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700197
198 current_buffer = []
199 else:
200 current_buffer.append(l)
201
202 @staticmethod
203 def _find_message_by_model_name(messages, model):
Zack Williams045b63d2019-01-22 16:30:57 -0700204 return next((x for x in messages if x["name"] == model), None)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700205
206 @staticmethod
Sapan Bhatia85b71012018-01-12 12:11:19 -0500207 def _find_last_nonempty_line(text, pointer):
208 ne_pointer = pointer
209 found = False
Zack Williams045b63d2019-01-22 16:30:57 -0700210 while ne_pointer != 0 and not found:
211 ne_pointer = text[: (ne_pointer - 1)].rfind("\n")
212 if ne_pointer < 0:
213 ne_pointer = 0
214 if text[ne_pointer - 1] != "\n":
Sapan Bhatia85b71012018-01-12 12:11:19 -0500215 found = True
216
217 return ne_pointer
218
219 @staticmethod
Zack Williams045b63d2019-01-22 16:30:57 -0700220 def process(args, operator=None):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700221 # Setting defaults
Zack Williams045b63d2019-01-22 16:30:57 -0700222 if not hasattr(args, "attic"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700223 args.attic = None
Zack Williams045b63d2019-01-22 16:30:57 -0700224 if not hasattr(args, "write_to_file"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700225 args.write_to_file = None
Zack Williams045b63d2019-01-22 16:30:57 -0700226 if not hasattr(args, "dest_file"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700227 args.dest_file = None
Zack Williams045b63d2019-01-22 16:30:57 -0700228 if not hasattr(args, "dest_extension"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700229 args.dest_extension = None
Zack Williams045b63d2019-01-22 16:30:57 -0700230 if not hasattr(args, "output"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700231 args.output = None
Zack Williams045b63d2019-01-22 16:30:57 -0700232 if not hasattr(args, "quiet"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700233 args.quiet = True
234
235 # Validating
Zack Williams045b63d2019-01-22 16:30:57 -0700236 if args.write_to_file == "single" and args.dest_file is None:
237 raise Exception(
238 "[XosGenX] write_to_file option is specified as 'single' but no dest_file is provided"
239 )
240 if args.write_to_file == "model" and (args.dest_extension is None):
241 raise Exception(
242 "[XosGenX] write_to_file option is specified as 'model' but no dest_extension is provided"
243 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700244
245 if args.output is not None and not os.path.isabs(args.output):
Scott Baker63c27ba2019-03-01 16:06:15 -0800246 raise Exception("[XosGenX] The output dir (%s) must be an absolute path!" % args.output)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700247 if args.output is not None and not os.path.isdir(args.output):
Scott Baker63c27ba2019-03-01 16:06:15 -0800248 raise Exception("[XosGenX] The output dir (%s) must be a directory!" % args.output)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700249
Zack Williams045b63d2019-01-22 16:30:57 -0700250 if hasattr(args, "files"):
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800251 (inputs, line_map) = XOSProcessor._read_input_from_files(args.files)
Zack Williams045b63d2019-01-22 16:30:57 -0700252 elif hasattr(args, "inputs"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700253 inputs = args.inputs
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800254 line_map = []
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700255 else:
256 raise Exception("[XosGenX] No inputs provided!")
257
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800258 context = XOSProcessor._add_context(args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700259
260 parser = plyxproto.ProtobufAnalyzer()
Sapan Bhatia85b71012018-01-12 12:11:19 -0500261 try:
262 ast = parser.parse_string(inputs, debug=0)
Zack Williams045b63d2019-01-22 16:30:57 -0700263 except plyxproto.ParsingError as e:
Sapan Bhatia85b71012018-01-12 12:11:19 -0500264 line, start, end = e.error_range
265
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800266 ptr = XOSProcessor._find_last_nonempty_line(inputs, start)
Sapan Bhatia85b71012018-01-12 12:11:19 -0500267
268 if start == 0:
Zack Williams045b63d2019-01-22 16:30:57 -0700269 beginning = ""
Sapan Bhatia85b71012018-01-12 12:11:19 -0500270 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700271 beginning = inputs[ptr: start - 1]
Sapan Bhatia85b71012018-01-12 12:11:19 -0500272
Zack Williams045b63d2019-01-22 16:30:57 -0700273 line_end_char = inputs[start + end:].find("\n")
Sapan Bhatia85b71012018-01-12 12:11:19 -0500274 line_end = inputs[line_end_char]
275
276 if e.message:
277 error = e.message
278 else:
279 error = "xproto parsing error"
280
Zack Williams045b63d2019-01-22 16:30:57 -0700281 print(error + "\n" + Fore.YELLOW + "Line %d:" % line + Fore.WHITE)
282 print(
283 beginning
284 + Fore.YELLOW
285 + inputs[start - 1: start + end]
286 + Fore.WHITE
287 + line_end
288 )
Sapan Bhatia85b71012018-01-12 12:11:19 -0500289 exit(1)
290
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800291 v = XOSProcessor._attach_parser(ast, args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700292
Scott Baker1f7791d2018-10-04 13:21:20 -0700293 if args.include_models or args.include_apps:
294 for message in v.messages:
295 message["is_included"] = False
296 if message["name"] in args.include_models:
297 message["is_included"] = True
298 else:
Zack Williams9a42f872019-02-15 17:56:04 -0700299 app_label = (
300 message.get("options", {})
301 .get("app_label")
302 .strip('"')
303 )
Scott Baker1f7791d2018-10-04 13:21:20 -0700304 if app_label in args.include_apps:
305 message["is_included"] = True
306 else:
307 for message in v.messages:
308 message["is_included"] = True
309
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800310 validator = XProtoValidator(v.models, line_map)
311 validator.validate()
312 if validator.errors:
Zack Williams5c2ea232019-01-30 15:23:01 -0700313 if args.strict_validation or (args.verbosity >= 0):
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800314 validator.print_errors()
Scott Baker08d10402019-04-08 16:19:59 -0700315 fatal_errors = [x for x in validator.errors if x["severity"] == "ERROR"]
316 if fatal_errors and args.strict_validation:
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800317 sys.exit(-1)
318
Scott Baker08d10402019-04-08 16:19:59 -0700319 if args.lint:
320 return ""
321
322 if not operator:
323 operator = args.target
324 template_path = XOSProcessor._get_template(operator)
325 else:
326 template_path = operator
327
328 [template_folder, template_name] = os.path.split(template_path)
329 os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder])
330 os_template_env = jinja2.Environment(loader=os_template_loader)
331 os_template_env = XOSProcessor._load_jinja2_extensions(
332 os_template_env, args.attic
333 )
334 template = os_template_env.get_template(template_name)
335
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700336 if args.output is not None and args.write_to_file == "model":
Scott Bakerbe2a5172019-04-10 18:02:50 -0700337 # Handle the case where each model is written to a separate python file.
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700338 rendered = {}
Scott Bakerbe2a5172019-04-10 18:02:50 -0700339
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700340 for i, model in enumerate(v.models):
Scott Bakerbe2a5172019-04-10 18:02:50 -0700341 model_dict = v.models[model]
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800342 messages = [XOSProcessor._find_message_by_model_name(v.messages, model)]
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700343
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700344 rendered[model] = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700345 {
346 "proto": {
Scott Bakerbe2a5172019-04-10 18:02:50 -0700347 "message_table": {model: model_dict},
Zack Williams045b63d2019-01-22 16:30:57 -0700348 "messages": messages,
349 "policies": v.policies,
350 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700351 },
352 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700353 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700354 }
355 )
Scott Bakerbe2a5172019-04-10 18:02:50 -0700356 if not rendered[model]:
357 print("Not saving model %s as it is empty" % model, file=sys.stderr)
358 else:
359 legacy = jinja2_extensions.base.xproto_list_evaluates_true(
360 [model_dict.get("options", {}).get("custom_python", None),
361 model_dict.get("options", {}).get("legacy", None),
362 v.options.get("custom_python", None),
363 v.options.get("legacy", None)])
364
365 if legacy:
366 file_name = "%s/%s_decl.%s" % (args.output, model.lower(), args.dest_extension)
367 else:
368 file_name = "%s/%s.%s" % (args.output, model.lower(), args.dest_extension)
369
370 file = open(file_name, "w")
371 file.write(rendered[model])
372 file.close()
373 if not args.quiet:
374 print("Saved: %s" % file_name, file=sys.stderr)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700375 else:
Scott Bakerbe2a5172019-04-10 18:02:50 -0700376 # Handle the case where all models are written to the same python file.
377
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700378 rendered = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700379 {
380 "proto": {
381 "message_table": v.models,
382 "messages": v.messages,
383 "policies": v.policies,
384 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700385 },
386 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700387 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700388 }
389 )
390 if args.output is not None and args.write_to_file == "target":
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800391 XOSProcessor._write_split_target(rendered, args.output, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700392 elif args.output is not None and args.write_to_file == "single":
Zack Williams045b63d2019-01-22 16:30:57 -0700393 XOSProcessor._write_single_file(
394 rendered, args.output, args.dest_file, args.quiet
395 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700396
Sapan Bhatia4c835602017-07-14 01:13:17 -0400397 return rendered