blob: 307e9142dc910a9b619da5ac1084142d7e2a127d [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 if e.message:
265 error = e.message
266 else:
267 error = "xproto parsing error"
Scott Baker115e5262019-06-03 11:49:15 -0700268 if e.error_range is None:
269 # No line number information
270 print(error + "\n")
271 else:
272 line, start, end = e.error_range
Sapan Bhatia85b71012018-01-12 12:11:19 -0500273
Scott Baker115e5262019-06-03 11:49:15 -0700274 ptr = XOSProcessor._find_last_nonempty_line(inputs, start)
275
276 if start == 0:
277 beginning = ""
278 else:
279 beginning = inputs[ptr: start - 1]
280
281 line_end_char = inputs[start + end:].find("\n")
282 line_end = inputs[line_end_char]
283
284 print(error + "\n" + Fore.YELLOW + "Line %d:" % line + Fore.WHITE)
285 print(
286 beginning
287 + Fore.YELLOW
288 + inputs[start - 1: start + end]
289 + Fore.WHITE
290 + line_end
291 )
Sapan Bhatia85b71012018-01-12 12:11:19 -0500292 exit(1)
293
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800294 v = XOSProcessor._attach_parser(ast, args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700295
Scott Baker1f7791d2018-10-04 13:21:20 -0700296 if args.include_models or args.include_apps:
297 for message in v.messages:
298 message["is_included"] = False
299 if message["name"] in args.include_models:
300 message["is_included"] = True
301 else:
Zack Williams9a42f872019-02-15 17:56:04 -0700302 app_label = (
303 message.get("options", {})
304 .get("app_label")
305 .strip('"')
306 )
Scott Baker1f7791d2018-10-04 13:21:20 -0700307 if app_label in args.include_apps:
308 message["is_included"] = True
309 else:
310 for message in v.messages:
311 message["is_included"] = True
312
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800313 validator = XProtoValidator(v.models, line_map)
314 validator.validate()
315 if validator.errors:
Zack Williams5c2ea232019-01-30 15:23:01 -0700316 if args.strict_validation or (args.verbosity >= 0):
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800317 validator.print_errors()
Scott Baker08d10402019-04-08 16:19:59 -0700318 fatal_errors = [x for x in validator.errors if x["severity"] == "ERROR"]
319 if fatal_errors and args.strict_validation:
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800320 sys.exit(-1)
321
Scott Baker08d10402019-04-08 16:19:59 -0700322 if args.lint:
323 return ""
324
325 if not operator:
326 operator = args.target
327 template_path = XOSProcessor._get_template(operator)
328 else:
329 template_path = operator
330
331 [template_folder, template_name] = os.path.split(template_path)
332 os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder])
333 os_template_env = jinja2.Environment(loader=os_template_loader)
334 os_template_env = XOSProcessor._load_jinja2_extensions(
335 os_template_env, args.attic
336 )
337 template = os_template_env.get_template(template_name)
338
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700339 if args.output is not None and args.write_to_file == "model":
Scott Bakerbe2a5172019-04-10 18:02:50 -0700340 # Handle the case where each model is written to a separate python file.
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700341 rendered = {}
Scott Bakerbe2a5172019-04-10 18:02:50 -0700342
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700343 for i, model in enumerate(v.models):
Scott Bakerbe2a5172019-04-10 18:02:50 -0700344 model_dict = v.models[model]
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800345 messages = [XOSProcessor._find_message_by_model_name(v.messages, model)]
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700346
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700347 rendered[model] = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700348 {
349 "proto": {
Scott Bakerbe2a5172019-04-10 18:02:50 -0700350 "message_table": {model: model_dict},
Zack Williams045b63d2019-01-22 16:30:57 -0700351 "messages": messages,
352 "policies": v.policies,
353 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700354 },
355 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700356 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700357 }
358 )
Scott Bakerbe2a5172019-04-10 18:02:50 -0700359 if not rendered[model]:
360 print("Not saving model %s as it is empty" % model, file=sys.stderr)
361 else:
362 legacy = jinja2_extensions.base.xproto_list_evaluates_true(
363 [model_dict.get("options", {}).get("custom_python", None),
364 model_dict.get("options", {}).get("legacy", None),
365 v.options.get("custom_python", None),
366 v.options.get("legacy", None)])
367
368 if legacy:
369 file_name = "%s/%s_decl.%s" % (args.output, model.lower(), args.dest_extension)
370 else:
371 file_name = "%s/%s.%s" % (args.output, model.lower(), args.dest_extension)
372
373 file = open(file_name, "w")
374 file.write(rendered[model])
375 file.close()
376 if not args.quiet:
377 print("Saved: %s" % file_name, file=sys.stderr)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700378 else:
Scott Bakerbe2a5172019-04-10 18:02:50 -0700379 # Handle the case where all models are written to the same python file.
380
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700381 rendered = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700382 {
383 "proto": {
384 "message_table": v.models,
385 "messages": v.messages,
386 "policies": v.policies,
387 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700388 },
389 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700390 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700391 }
392 )
393 if args.output is not None and args.write_to_file == "target":
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800394 XOSProcessor._write_split_target(rendered, args.output, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700395 elif args.output is not None and args.write_to_file == "single":
Zack Williams045b63d2019-01-22 16:30:57 -0700396 XOSProcessor._write_single_file(
397 rendered, args.output, args.dest_file, args.quiet
398 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700399
Sapan Bhatia4c835602017-07-14 01:13:17 -0400400 return rendered