blob: d1dc99afc9468310edde62404b9e4ca66ae00f37 [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
Scott Bakera33ccb02018-01-26 13:03:28 -0800177 def _write_file_per_model(rendered, dir, suffix, quiet):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700178 for m in rendered:
Scott Bakera33ccb02018-01-26 13:03:28 -0800179 file_name = "%s/%s%s" % (dir, m.lower(), suffix)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700180 if not rendered[m]:
Zack Williams045b63d2019-01-22 16:30:57 -0700181 if not quiet:
182 print("Not saving %s as it is empty" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700183 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700184 file = open(file_name, "w")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700185 file.write(rendered[m])
186 file.close()
Zack Williams045b63d2019-01-22 16:30:57 -0700187 if not quiet:
188 print("Saved: %s" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700189
190 @staticmethod
191 def _write_split_target(rendered, dir, quiet):
192
193 lines = rendered.splitlines()
194 current_buffer = []
195 for l in lines:
Zack Williams045b63d2019-01-22 16:30:57 -0700196 if l.startswith("+++"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700197
198 if dir:
Zack Williams045b63d2019-01-22 16:30:57 -0700199 path = dir + "/" + l[4:].lower()
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700200
Zack Williams045b63d2019-01-22 16:30:57 -0700201 fil = open(path, "w")
202 buf = "\n".join(current_buffer)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700203
204 obuf = buf
205
206 fil.write(obuf)
207 fil.close()
208
Zack Williams045b63d2019-01-22 16:30:57 -0700209 if not quiet:
210 print("Save file to: %s" % path)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700211
212 current_buffer = []
213 else:
214 current_buffer.append(l)
215
216 @staticmethod
217 def _find_message_by_model_name(messages, model):
Zack Williams045b63d2019-01-22 16:30:57 -0700218 return next((x for x in messages if x["name"] == model), None)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700219
220 @staticmethod
Sapan Bhatia85b71012018-01-12 12:11:19 -0500221 def _find_last_nonempty_line(text, pointer):
222 ne_pointer = pointer
223 found = False
Zack Williams045b63d2019-01-22 16:30:57 -0700224 while ne_pointer != 0 and not found:
225 ne_pointer = text[: (ne_pointer - 1)].rfind("\n")
226 if ne_pointer < 0:
227 ne_pointer = 0
228 if text[ne_pointer - 1] != "\n":
Sapan Bhatia85b71012018-01-12 12:11:19 -0500229 found = True
230
231 return ne_pointer
232
233 @staticmethod
Zack Williams045b63d2019-01-22 16:30:57 -0700234 def process(args, operator=None):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700235 # Setting defaults
Zack Williams045b63d2019-01-22 16:30:57 -0700236 if not hasattr(args, "attic"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700237 args.attic = None
Zack Williams045b63d2019-01-22 16:30:57 -0700238 if not hasattr(args, "write_to_file"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700239 args.write_to_file = None
Zack Williams045b63d2019-01-22 16:30:57 -0700240 if not hasattr(args, "dest_file"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700241 args.dest_file = None
Zack Williams045b63d2019-01-22 16:30:57 -0700242 if not hasattr(args, "dest_extension"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700243 args.dest_extension = None
Zack Williams045b63d2019-01-22 16:30:57 -0700244 if not hasattr(args, "output"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700245 args.output = None
Zack Williams045b63d2019-01-22 16:30:57 -0700246 if not hasattr(args, "quiet"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700247 args.quiet = True
248
249 # Validating
Zack Williams045b63d2019-01-22 16:30:57 -0700250 if args.write_to_file == "single" and args.dest_file is None:
251 raise Exception(
252 "[XosGenX] write_to_file option is specified as 'single' but no dest_file is provided"
253 )
254 if args.write_to_file == "model" and (args.dest_extension is None):
255 raise Exception(
256 "[XosGenX] write_to_file option is specified as 'model' but no dest_extension is provided"
257 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700258
259 if args.output is not None and not os.path.isabs(args.output):
Scott Baker63c27ba2019-03-01 16:06:15 -0800260 raise Exception("[XosGenX] The output dir (%s) must be an absolute path!" % args.output)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700261 if args.output is not None and not os.path.isdir(args.output):
Scott Baker63c27ba2019-03-01 16:06:15 -0800262 raise Exception("[XosGenX] The output dir (%s) must be a directory!" % args.output)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700263
Zack Williams045b63d2019-01-22 16:30:57 -0700264 if hasattr(args, "files"):
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800265 (inputs, line_map) = XOSProcessor._read_input_from_files(args.files)
Zack Williams045b63d2019-01-22 16:30:57 -0700266 elif hasattr(args, "inputs"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700267 inputs = args.inputs
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800268 line_map = []
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700269 else:
270 raise Exception("[XosGenX] No inputs provided!")
271
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800272 context = XOSProcessor._add_context(args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700273
274 parser = plyxproto.ProtobufAnalyzer()
Sapan Bhatia85b71012018-01-12 12:11:19 -0500275 try:
276 ast = parser.parse_string(inputs, debug=0)
Zack Williams045b63d2019-01-22 16:30:57 -0700277 except plyxproto.ParsingError as e:
Sapan Bhatia85b71012018-01-12 12:11:19 -0500278 line, start, end = e.error_range
279
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800280 ptr = XOSProcessor._find_last_nonempty_line(inputs, start)
Sapan Bhatia85b71012018-01-12 12:11:19 -0500281
282 if start == 0:
Zack Williams045b63d2019-01-22 16:30:57 -0700283 beginning = ""
Sapan Bhatia85b71012018-01-12 12:11:19 -0500284 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700285 beginning = inputs[ptr: start - 1]
Sapan Bhatia85b71012018-01-12 12:11:19 -0500286
Zack Williams045b63d2019-01-22 16:30:57 -0700287 line_end_char = inputs[start + end:].find("\n")
Sapan Bhatia85b71012018-01-12 12:11:19 -0500288 line_end = inputs[line_end_char]
289
290 if e.message:
291 error = e.message
292 else:
293 error = "xproto parsing error"
294
Zack Williams045b63d2019-01-22 16:30:57 -0700295 print(error + "\n" + Fore.YELLOW + "Line %d:" % line + Fore.WHITE)
296 print(
297 beginning
298 + Fore.YELLOW
299 + inputs[start - 1: start + end]
300 + Fore.WHITE
301 + line_end
302 )
Sapan Bhatia85b71012018-01-12 12:11:19 -0500303 exit(1)
304
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800305 v = XOSProcessor._attach_parser(ast, args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700306
Scott Baker1f7791d2018-10-04 13:21:20 -0700307 if args.include_models or args.include_apps:
308 for message in v.messages:
309 message["is_included"] = False
310 if message["name"] in args.include_models:
311 message["is_included"] = True
312 else:
Zack Williams9a42f872019-02-15 17:56:04 -0700313 app_label = (
314 message.get("options", {})
315 .get("app_label")
316 .strip('"')
317 )
Scott Baker1f7791d2018-10-04 13:21:20 -0700318 if app_label in args.include_apps:
319 message["is_included"] = True
320 else:
321 for message in v.messages:
322 message["is_included"] = True
323
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800324 validator = XProtoValidator(v.models, line_map)
325 validator.validate()
326 if validator.errors:
Zack Williams5c2ea232019-01-30 15:23:01 -0700327 if args.strict_validation or (args.verbosity >= 0):
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800328 validator.print_errors()
Scott Baker08d10402019-04-08 16:19:59 -0700329 fatal_errors = [x for x in validator.errors if x["severity"] == "ERROR"]
330 if fatal_errors and args.strict_validation:
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800331 sys.exit(-1)
332
Scott Baker08d10402019-04-08 16:19:59 -0700333 if args.lint:
334 return ""
335
336 if not operator:
337 operator = args.target
338 template_path = XOSProcessor._get_template(operator)
339 else:
340 template_path = operator
341
342 [template_folder, template_name] = os.path.split(template_path)
343 os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder])
344 os_template_env = jinja2.Environment(loader=os_template_loader)
345 os_template_env = XOSProcessor._load_jinja2_extensions(
346 os_template_env, args.attic
347 )
348 template = os_template_env.get_template(template_name)
349
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700350 if args.output is not None and args.write_to_file == "model":
351 rendered = {}
352 for i, model in enumerate(v.models):
353 models = {}
354 models[model] = v.models[model]
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800355 messages = [XOSProcessor._find_message_by_model_name(v.messages, model)]
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700356
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700357 rendered[model] = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700358 {
359 "proto": {
360 "message_table": models,
361 "messages": messages,
362 "policies": v.policies,
363 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700364 },
365 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700366 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700367 }
368 )
Zack Williams045b63d2019-01-22 16:30:57 -0700369 if str(v.options.get("legacy", "false")).strip('"').lower() == "true":
Scott Bakera33ccb02018-01-26 13:03:28 -0800370 suffix = "_decl." + args.dest_extension
371 else:
372 suffix = "." + args.dest_extension
Zack Williams045b63d2019-01-22 16:30:57 -0700373 XOSProcessor._write_file_per_model(
374 rendered, args.output, suffix, args.quiet
375 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700376 else:
377 rendered = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700378 {
379 "proto": {
380 "message_table": v.models,
381 "messages": v.messages,
382 "policies": v.policies,
383 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700384 },
385 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700386 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700387 }
388 )
389 if args.output is not None and args.write_to_file == "target":
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800390 XOSProcessor._write_split_target(rendered, args.output, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700391 elif args.output is not None and args.write_to_file == "single":
Zack Williams045b63d2019-01-22 16:30:57 -0700392 XOSProcessor._write_single_file(
393 rendered, args.output, args.dest_file, args.quiet
394 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700395
Sapan Bhatia4c835602017-07-14 01:13:17 -0400396 return rendered