blob: 707f87b0e0ef2996519fa8ae146e22147d0561e1 [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 Baker1f7791d2018-10-04 13:21:20 -070055
56 def __init__(self, **kwargs):
57 # set defaults
58 self.rev = XOSProcessorArgs.default_rev
59 self.output = XOSProcessorArgs.default_output
60 self.attic = XOSProcessorArgs.default_attic
61 self.kvpairs = XOSProcessorArgs.default_kvpairs
62 self.verbosity = XOSProcessorArgs.default_verbosity
63 self.write_to_file = XOSProcessorArgs.default_write_to_file
64 self.default_dest_file = XOSProcessorArgs.default_dest_file
65 self.default_dest_extension = XOSProcessorArgs.default_dest_extension
66 self.default_target = XOSProcessorArgs.default_target
67 self.default_checkers = XOSProcessorArgs.default_target
68 self.include_models = XOSProcessorArgs.default_include_models
69 self.include_apps = XOSProcessorArgs.default_include_apps
Scott Baker7ae3a8f2019-03-05 16:24:14 -080070 self.strict_validation = XOSProcessorArgs.default_strict_validation
Scott Baker1f7791d2018-10-04 13:21:20 -070071
72 # override defaults with kwargs
Zack Williams045b63d2019-01-22 16:30:57 -070073 for (k, v) in kwargs.items():
Scott Baker1f7791d2018-10-04 13:21:20 -070074 setattr(self, k, v)
75
Matteo Scandolo67654fa2017-06-09 09:33:17 -070076
Zack Williams045b63d2019-01-22 16:30:57 -070077class XOSProcessor:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070078 @staticmethod
79 def _read_input_from_files(files):
Scott Baker7ae3a8f2019-03-05 16:24:14 -080080 """ Read the files and return the combined text read.
81
82 Also returns a list of (line_number, filename) tuples that tell which
83 starting line corresponds to each file.
84 """
85 line_map = []
Zack Williams045b63d2019-01-22 16:30:57 -070086 input = ""
Matteo Scandolo67654fa2017-06-09 09:33:17 -070087 for fname in files:
88 with open(fname) as infile:
Zack Williams5c2ea232019-01-30 15:23:01 -070089 line_map.append((len(input.split("\n")), fname))
Matteo Scandolo67654fa2017-06-09 09:33:17 -070090 input += infile.read()
Scott Baker7ae3a8f2019-03-05 16:24:14 -080091 return (input, line_map)
Matteo Scandolo67654fa2017-06-09 09:33:17 -070092
93 @staticmethod
94 def _attach_parser(ast, args):
Zack Williams045b63d2019-01-22 16:30:57 -070095 if hasattr(args, "rev") and args.rev:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070096 v = Proto2XProto()
97 ast.accept(v)
Sapan Bhatia4c835602017-07-14 01:13:17 -040098
Scott Bakerc237f882018-09-28 14:12:47 -070099 v = XOS2Jinja(args)
Sapan Bhatia4c835602017-07-14 01:13:17 -0400100 ast.accept(v)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700101 return v
102
103 @staticmethod
104 def _get_template(target):
105 if not os.path.isabs(target):
Zack Williams045b63d2019-01-22 16:30:57 -0700106 return os.path.abspath(
107 os.path.dirname(os.path.realpath(__file__)) + "/targets/" + target
108 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700109 return target
110
111 @staticmethod
112 def _file_exists(attic):
113 # NOTE this method can be used in the jinja template
114 def file_exists2(name):
115 if attic is not None:
Zack Williams045b63d2019-01-22 16:30:57 -0700116 path = attic + "/" + name
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700117 else:
118 path = name
Zack Williams045b63d2019-01-22 16:30:57 -0700119 return os.path.exists(path)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700120
121 return file_exists2
122
123 @staticmethod
124 def _include_file(attic):
125 # NOTE this method can be used in the jinja template
126 def include_file2(name):
127 if attic is not None:
Zack Williams045b63d2019-01-22 16:30:57 -0700128 path = attic + "/" + name
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700129 else:
130 path = name
131 return open(path).read()
Zack Williams045b63d2019-01-22 16:30:57 -0700132
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700133 return include_file2
134
135 @staticmethod
136 def _load_jinja2_extensions(os_template_env, attic):
137
Zack Williams045b63d2019-01-22 16:30:57 -0700138 os_template_env.globals["include_file"] = XOSProcessor._include_file(
139 attic
140 ) # Generates a function
141 os_template_env.globals["file_exists"] = XOSProcessor._file_exists(
142 attic
143 ) # Generates a function
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700144
Zack Williams045b63d2019-01-22 16:30:57 -0700145 os_template_env.filters["yaml"] = yaml.dump
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700146 for f in dir(jinja2_extensions):
Zack Williams045b63d2019-01-22 16:30:57 -0700147 if f.startswith("xproto"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700148 os_template_env.globals[f] = getattr(jinja2_extensions, f)
149 return os_template_env
150
151 @staticmethod
152 def _add_context(args):
Zack Williams045b63d2019-01-22 16:30:57 -0700153 if not hasattr(args, "kv") or not args.kv:
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700154 return
155 try:
156 context = {}
Zack Williams045b63d2019-01-22 16:30:57 -0700157 for s in args.kv.split(","):
158 k, val = s.split(":")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700159 context[k] = val
160 return context
Zack Williams045b63d2019-01-22 16:30:57 -0700161 except Exception as e:
Zack Williams9a42f872019-02-15 17:56:04 -0700162 print(e)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700163
164 @staticmethod
165 def _write_single_file(rendered, dir, dest_file, quiet):
166
167 file_name = "%s/%s" % (dir, dest_file)
Zack Williams045b63d2019-01-22 16:30:57 -0700168 file = open(file_name, "w")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700169 file.write(rendered)
170 file.close()
Zack Williams045b63d2019-01-22 16:30:57 -0700171 if not quiet:
172 print("Saved: %s" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700173
174 @staticmethod
Scott Bakera33ccb02018-01-26 13:03:28 -0800175 def _write_file_per_model(rendered, dir, suffix, quiet):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700176 for m in rendered:
Scott Bakera33ccb02018-01-26 13:03:28 -0800177 file_name = "%s/%s%s" % (dir, m.lower(), suffix)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700178 if not rendered[m]:
Zack Williams045b63d2019-01-22 16:30:57 -0700179 if not quiet:
180 print("Not saving %s as it is empty" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700181 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700182 file = open(file_name, "w")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700183 file.write(rendered[m])
184 file.close()
Zack Williams045b63d2019-01-22 16:30:57 -0700185 if not quiet:
186 print("Saved: %s" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700187
188 @staticmethod
189 def _write_split_target(rendered, dir, quiet):
190
191 lines = rendered.splitlines()
192 current_buffer = []
193 for l in lines:
Zack Williams045b63d2019-01-22 16:30:57 -0700194 if l.startswith("+++"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700195
196 if dir:
Zack Williams045b63d2019-01-22 16:30:57 -0700197 path = dir + "/" + l[4:].lower()
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700198
Zack Williams045b63d2019-01-22 16:30:57 -0700199 fil = open(path, "w")
200 buf = "\n".join(current_buffer)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700201
202 obuf = buf
203
204 fil.write(obuf)
205 fil.close()
206
Zack Williams045b63d2019-01-22 16:30:57 -0700207 if not quiet:
208 print("Save file to: %s" % path)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700209
210 current_buffer = []
211 else:
212 current_buffer.append(l)
213
214 @staticmethod
215 def _find_message_by_model_name(messages, model):
Zack Williams045b63d2019-01-22 16:30:57 -0700216 return next((x for x in messages if x["name"] == model), None)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700217
218 @staticmethod
Sapan Bhatia85b71012018-01-12 12:11:19 -0500219 def _find_last_nonempty_line(text, pointer):
220 ne_pointer = pointer
221 found = False
Zack Williams045b63d2019-01-22 16:30:57 -0700222 while ne_pointer != 0 and not found:
223 ne_pointer = text[: (ne_pointer - 1)].rfind("\n")
224 if ne_pointer < 0:
225 ne_pointer = 0
226 if text[ne_pointer - 1] != "\n":
Sapan Bhatia85b71012018-01-12 12:11:19 -0500227 found = True
228
229 return ne_pointer
230
231 @staticmethod
Zack Williams045b63d2019-01-22 16:30:57 -0700232 def process(args, operator=None):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700233 # Setting defaults
Zack Williams045b63d2019-01-22 16:30:57 -0700234 if not hasattr(args, "attic"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700235 args.attic = None
Zack Williams045b63d2019-01-22 16:30:57 -0700236 if not hasattr(args, "write_to_file"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700237 args.write_to_file = None
Zack Williams045b63d2019-01-22 16:30:57 -0700238 if not hasattr(args, "dest_file"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700239 args.dest_file = None
Zack Williams045b63d2019-01-22 16:30:57 -0700240 if not hasattr(args, "dest_extension"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700241 args.dest_extension = None
Zack Williams045b63d2019-01-22 16:30:57 -0700242 if not hasattr(args, "output"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700243 args.output = None
Zack Williams045b63d2019-01-22 16:30:57 -0700244 if not hasattr(args, "quiet"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700245 args.quiet = True
246
247 # Validating
Zack Williams045b63d2019-01-22 16:30:57 -0700248 if args.write_to_file == "single" and args.dest_file is None:
249 raise Exception(
250 "[XosGenX] write_to_file option is specified as 'single' but no dest_file is provided"
251 )
252 if args.write_to_file == "model" and (args.dest_extension is None):
253 raise Exception(
254 "[XosGenX] write_to_file option is specified as 'model' but no dest_extension is provided"
255 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700256
257 if args.output is not None and not os.path.isabs(args.output):
Scott Baker63c27ba2019-03-01 16:06:15 -0800258 raise Exception("[XosGenX] The output dir (%s) must be an absolute path!" % args.output)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700259 if args.output is not None and not os.path.isdir(args.output):
Scott Baker63c27ba2019-03-01 16:06:15 -0800260 raise Exception("[XosGenX] The output dir (%s) must be a directory!" % args.output)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700261
Zack Williams045b63d2019-01-22 16:30:57 -0700262 if hasattr(args, "files"):
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800263 (inputs, line_map) = XOSProcessor._read_input_from_files(args.files)
Zack Williams045b63d2019-01-22 16:30:57 -0700264 elif hasattr(args, "inputs"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700265 inputs = args.inputs
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800266 line_map = []
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700267 else:
268 raise Exception("[XosGenX] No inputs provided!")
269
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800270 if not operator:
271 operator = args.target
272 template_path = XOSProcessor._get_template(operator)
273 else:
274 template_path = operator
275
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700276 [template_folder, template_name] = os.path.split(template_path)
277 os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder])
278 os_template_env = jinja2.Environment(loader=os_template_loader)
Zack Williams045b63d2019-01-22 16:30:57 -0700279 os_template_env = XOSProcessor._load_jinja2_extensions(
280 os_template_env, args.attic
281 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700282 template = os_template_env.get_template(template_name)
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800283 context = XOSProcessor._add_context(args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700284
285 parser = plyxproto.ProtobufAnalyzer()
Sapan Bhatia85b71012018-01-12 12:11:19 -0500286 try:
287 ast = parser.parse_string(inputs, debug=0)
Zack Williams045b63d2019-01-22 16:30:57 -0700288 except plyxproto.ParsingError as e:
Sapan Bhatia85b71012018-01-12 12:11:19 -0500289 line, start, end = e.error_range
290
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800291 ptr = XOSProcessor._find_last_nonempty_line(inputs, start)
Sapan Bhatia85b71012018-01-12 12:11:19 -0500292
293 if start == 0:
Zack Williams045b63d2019-01-22 16:30:57 -0700294 beginning = ""
Sapan Bhatia85b71012018-01-12 12:11:19 -0500295 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700296 beginning = inputs[ptr: start - 1]
Sapan Bhatia85b71012018-01-12 12:11:19 -0500297
Zack Williams045b63d2019-01-22 16:30:57 -0700298 line_end_char = inputs[start + end:].find("\n")
Sapan Bhatia85b71012018-01-12 12:11:19 -0500299 line_end = inputs[line_end_char]
300
301 if e.message:
302 error = e.message
303 else:
304 error = "xproto parsing error"
305
Zack Williams045b63d2019-01-22 16:30:57 -0700306 print(error + "\n" + Fore.YELLOW + "Line %d:" % line + Fore.WHITE)
307 print(
308 beginning
309 + Fore.YELLOW
310 + inputs[start - 1: start + end]
311 + Fore.WHITE
312 + line_end
313 )
Sapan Bhatia85b71012018-01-12 12:11:19 -0500314 exit(1)
315
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800316 v = XOSProcessor._attach_parser(ast, args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700317
Scott Baker1f7791d2018-10-04 13:21:20 -0700318 if args.include_models or args.include_apps:
319 for message in v.messages:
320 message["is_included"] = False
321 if message["name"] in args.include_models:
322 message["is_included"] = True
323 else:
Zack Williams9a42f872019-02-15 17:56:04 -0700324 app_label = (
325 message.get("options", {})
326 .get("app_label")
327 .strip('"')
328 )
Scott Baker1f7791d2018-10-04 13:21:20 -0700329 if app_label in args.include_apps:
330 message["is_included"] = True
331 else:
332 for message in v.messages:
333 message["is_included"] = True
334
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800335 validator = XProtoValidator(v.models, line_map)
336 validator.validate()
337 if validator.errors:
Zack Williams5c2ea232019-01-30 15:23:01 -0700338 if args.strict_validation or (args.verbosity >= 0):
Scott Baker7ae3a8f2019-03-05 16:24:14 -0800339 validator.print_errors()
340 if args.strict_validation:
341 sys.exit(-1)
342
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700343 if args.output is not None and args.write_to_file == "model":
344 rendered = {}
345 for i, model in enumerate(v.models):
346 models = {}
347 models[model] = v.models[model]
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800348 messages = [XOSProcessor._find_message_by_model_name(v.messages, model)]
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700349
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700350 rendered[model] = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700351 {
352 "proto": {
353 "message_table": models,
354 "messages": messages,
355 "policies": v.policies,
356 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700357 },
358 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700359 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700360 }
361 )
Zack Williams045b63d2019-01-22 16:30:57 -0700362 if str(v.options.get("legacy", "false")).strip('"').lower() == "true":
Scott Bakera33ccb02018-01-26 13:03:28 -0800363 suffix = "_decl." + args.dest_extension
364 else:
365 suffix = "." + args.dest_extension
Zack Williams045b63d2019-01-22 16:30:57 -0700366 XOSProcessor._write_file_per_model(
367 rendered, args.output, suffix, args.quiet
368 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700369 else:
370 rendered = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700371 {
372 "proto": {
373 "message_table": v.models,
374 "messages": v.messages,
375 "policies": v.policies,
376 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700377 },
378 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700379 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700380 }
381 )
382 if args.output is not None and args.write_to_file == "target":
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800383 XOSProcessor._write_split_target(rendered, args.output, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700384 elif args.output is not None and args.write_to_file == "single":
Zack Williams045b63d2019-01-22 16:30:57 -0700385 XOSProcessor._write_single_file(
386 rendered, args.output, args.dest_file, args.quiet
387 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700388
Sapan Bhatia4c835602017-07-14 01:13:17 -0400389 return rendered