blob: af99a23a7f7612025d8f5d7dca6788b1a19c71c5 [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
15
Zack Williams045b63d2019-01-22 16:30:57 -070016from __future__ import print_function
Matteo Scandolo67654fa2017-06-09 09:33:17 -070017import plyxproto.parser as plyxproto
18import jinja2
19import os
20from xos2jinja import XOS2Jinja
21from proto2xproto import Proto2XProto
22import jinja2_extensions
23import yaml
Sapan Bhatia85b71012018-01-12 12:11:19 -050024from colorama import Fore
Matteo Scandolo67654fa2017-06-09 09:33:17 -070025
Zack Williams045b63d2019-01-22 16:30:57 -070026loader = jinja2.PackageLoader(__name__, "templates")
Matteo Scandolo67654fa2017-06-09 09:33:17 -070027env = jinja2.Environment(loader=loader)
28
Zack Williams045b63d2019-01-22 16:30:57 -070029
Scott Baker1f7791d2018-10-04 13:21:20 -070030class XOSProcessorArgs:
31 """ Helper class for use cases that want to call XOSProcessor directly, rather than executing xosgenx from the
32 command line.
33 """
34
35 default_rev = False
36 default_output = None
37 default_attic = None
38 default_kvpairs = None
39 default_write_to_file = None
40 default_dest_file = None
41 default_dest_extension = None
42 default_target = None
43 default_checkers = None
Zack Williams045b63d2019-01-22 16:30:57 -070044 default_verbosity = (
45 0
46 ) # Higher numbers = more verbosity, lower numbers = less verbosity
47 default_include_models = (
48 []
49 ) # If neither include_models nor include_apps is specified, then all models will
50 default_include_apps = [] # be included.
Scott Baker1f7791d2018-10-04 13:21:20 -070051
52 def __init__(self, **kwargs):
53 # set defaults
54 self.rev = XOSProcessorArgs.default_rev
55 self.output = XOSProcessorArgs.default_output
56 self.attic = XOSProcessorArgs.default_attic
57 self.kvpairs = XOSProcessorArgs.default_kvpairs
58 self.verbosity = XOSProcessorArgs.default_verbosity
59 self.write_to_file = XOSProcessorArgs.default_write_to_file
60 self.default_dest_file = XOSProcessorArgs.default_dest_file
61 self.default_dest_extension = XOSProcessorArgs.default_dest_extension
62 self.default_target = XOSProcessorArgs.default_target
63 self.default_checkers = XOSProcessorArgs.default_target
64 self.include_models = XOSProcessorArgs.default_include_models
65 self.include_apps = XOSProcessorArgs.default_include_apps
66
67 # override defaults with kwargs
Zack Williams045b63d2019-01-22 16:30:57 -070068 for (k, v) in kwargs.items():
Scott Baker1f7791d2018-10-04 13:21:20 -070069 setattr(self, k, v)
70
Matteo Scandolo67654fa2017-06-09 09:33:17 -070071
Zack Williams045b63d2019-01-22 16:30:57 -070072class XOSProcessor:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070073 @staticmethod
74 def _read_input_from_files(files):
Zack Williams045b63d2019-01-22 16:30:57 -070075 input = ""
Matteo Scandolo67654fa2017-06-09 09:33:17 -070076 for fname in files:
77 with open(fname) as infile:
78 input += infile.read()
79 return input
80
81 @staticmethod
82 def _attach_parser(ast, args):
Zack Williams045b63d2019-01-22 16:30:57 -070083 if hasattr(args, "rev") and args.rev:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070084 v = Proto2XProto()
85 ast.accept(v)
Sapan Bhatia4c835602017-07-14 01:13:17 -040086
Scott Bakerc237f882018-09-28 14:12:47 -070087 v = XOS2Jinja(args)
Sapan Bhatia4c835602017-07-14 01:13:17 -040088 ast.accept(v)
Matteo Scandolo67654fa2017-06-09 09:33:17 -070089 return v
90
91 @staticmethod
92 def _get_template(target):
93 if not os.path.isabs(target):
Zack Williams045b63d2019-01-22 16:30:57 -070094 return os.path.abspath(
95 os.path.dirname(os.path.realpath(__file__)) + "/targets/" + target
96 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -070097 return target
98
99 @staticmethod
100 def _file_exists(attic):
101 # NOTE this method can be used in the jinja template
102 def file_exists2(name):
103 if attic is not None:
Zack Williams045b63d2019-01-22 16:30:57 -0700104 path = attic + "/" + name
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700105 else:
106 path = name
Zack Williams045b63d2019-01-22 16:30:57 -0700107 return os.path.exists(path)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700108
109 return file_exists2
110
111 @staticmethod
112 def _include_file(attic):
113 # NOTE this method can be used in the jinja template
114 def include_file2(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
119 return open(path).read()
Zack Williams045b63d2019-01-22 16:30:57 -0700120
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700121 return include_file2
122
123 @staticmethod
124 def _load_jinja2_extensions(os_template_env, attic):
125
Zack Williams045b63d2019-01-22 16:30:57 -0700126 os_template_env.globals["include_file"] = XOSProcessor._include_file(
127 attic
128 ) # Generates a function
129 os_template_env.globals["file_exists"] = XOSProcessor._file_exists(
130 attic
131 ) # Generates a function
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700132
Zack Williams045b63d2019-01-22 16:30:57 -0700133 os_template_env.filters["yaml"] = yaml.dump
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700134 for f in dir(jinja2_extensions):
Zack Williams045b63d2019-01-22 16:30:57 -0700135 if f.startswith("xproto"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700136 os_template_env.globals[f] = getattr(jinja2_extensions, f)
137 return os_template_env
138
139 @staticmethod
140 def _add_context(args):
Zack Williams045b63d2019-01-22 16:30:57 -0700141 if not hasattr(args, "kv") or not args.kv:
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700142 return
143 try:
144 context = {}
Zack Williams045b63d2019-01-22 16:30:57 -0700145 for s in args.kv.split(","):
146 k, val = s.split(":")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700147 context[k] = val
148 return context
Zack Williams045b63d2019-01-22 16:30:57 -0700149 except Exception as e:
150 print(e.message)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700151
152 @staticmethod
153 def _write_single_file(rendered, dir, dest_file, quiet):
154
155 file_name = "%s/%s" % (dir, dest_file)
Zack Williams045b63d2019-01-22 16:30:57 -0700156 file = open(file_name, "w")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700157 file.write(rendered)
158 file.close()
Zack Williams045b63d2019-01-22 16:30:57 -0700159 if not quiet:
160 print("Saved: %s" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700161
162 @staticmethod
Scott Bakera33ccb02018-01-26 13:03:28 -0800163 def _write_file_per_model(rendered, dir, suffix, quiet):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700164 for m in rendered:
Scott Bakera33ccb02018-01-26 13:03:28 -0800165 file_name = "%s/%s%s" % (dir, m.lower(), suffix)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700166 if not rendered[m]:
Zack Williams045b63d2019-01-22 16:30:57 -0700167 if not quiet:
168 print("Not saving %s as it is empty" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700169 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700170 file = open(file_name, "w")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700171 file.write(rendered[m])
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
177 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"):
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800251 inputs = 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
254 else:
255 raise Exception("[XosGenX] No inputs provided!")
256
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800257 if not operator:
258 operator = args.target
259 template_path = XOSProcessor._get_template(operator)
260 else:
261 template_path = operator
262
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700263 [template_folder, template_name] = os.path.split(template_path)
264 os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder])
265 os_template_env = jinja2.Environment(loader=os_template_loader)
Zack Williams045b63d2019-01-22 16:30:57 -0700266 os_template_env = XOSProcessor._load_jinja2_extensions(
267 os_template_env, args.attic
268 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700269 template = os_template_env.get_template(template_name)
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800270 context = XOSProcessor._add_context(args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700271
272 parser = plyxproto.ProtobufAnalyzer()
Sapan Bhatia85b71012018-01-12 12:11:19 -0500273 try:
274 ast = parser.parse_string(inputs, debug=0)
Zack Williams045b63d2019-01-22 16:30:57 -0700275 except plyxproto.ParsingError as e:
Sapan Bhatia85b71012018-01-12 12:11:19 -0500276 line, start, end = e.error_range
277
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800278 ptr = XOSProcessor._find_last_nonempty_line(inputs, start)
Sapan Bhatia85b71012018-01-12 12:11:19 -0500279
280 if start == 0:
Zack Williams045b63d2019-01-22 16:30:57 -0700281 beginning = ""
Sapan Bhatia85b71012018-01-12 12:11:19 -0500282 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700283 beginning = inputs[ptr: start - 1]
Sapan Bhatia85b71012018-01-12 12:11:19 -0500284
Zack Williams045b63d2019-01-22 16:30:57 -0700285 line_end_char = inputs[start + end:].find("\n")
Sapan Bhatia85b71012018-01-12 12:11:19 -0500286 line_end = inputs[line_end_char]
287
288 if e.message:
289 error = e.message
290 else:
291 error = "xproto parsing error"
292
Zack Williams045b63d2019-01-22 16:30:57 -0700293 print(error + "\n" + Fore.YELLOW + "Line %d:" % line + Fore.WHITE)
294 print(
295 beginning
296 + Fore.YELLOW
297 + inputs[start - 1: start + end]
298 + Fore.WHITE
299 + line_end
300 )
Sapan Bhatia85b71012018-01-12 12:11:19 -0500301 exit(1)
302
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800303 v = XOSProcessor._attach_parser(ast, args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700304
Scott Baker1f7791d2018-10-04 13:21:20 -0700305 if args.include_models or args.include_apps:
306 for message in v.messages:
307 message["is_included"] = False
308 if message["name"] in args.include_models:
309 message["is_included"] = True
310 else:
311 app_label = message.get("options", {}).get("app_label").strip('"')
312 if app_label in args.include_apps:
313 message["is_included"] = True
314 else:
315 for message in v.messages:
316 message["is_included"] = True
317
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700318 if args.output is not None and args.write_to_file == "model":
319 rendered = {}
320 for i, model in enumerate(v.models):
321 models = {}
322 models[model] = v.models[model]
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800323 messages = [XOSProcessor._find_message_by_model_name(v.messages, model)]
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700324
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700325 rendered[model] = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700326 {
327 "proto": {
328 "message_table": models,
329 "messages": messages,
330 "policies": v.policies,
331 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700332 },
333 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700334 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700335 }
336 )
Zack Williams045b63d2019-01-22 16:30:57 -0700337 if str(v.options.get("legacy", "false")).strip('"').lower() == "true":
Scott Bakera33ccb02018-01-26 13:03:28 -0800338 suffix = "_decl." + args.dest_extension
339 else:
340 suffix = "." + args.dest_extension
Zack Williams045b63d2019-01-22 16:30:57 -0700341 XOSProcessor._write_file_per_model(
342 rendered, args.output, suffix, args.quiet
343 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700344 else:
345 rendered = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700346 {
347 "proto": {
348 "message_table": v.models,
349 "messages": v.messages,
350 "policies": v.policies,
351 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700352 },
353 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700354 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700355 }
356 )
357 if args.output is not None and args.write_to_file == "target":
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800358 XOSProcessor._write_split_target(rendered, args.output, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700359 elif args.output is not None and args.write_to_file == "single":
Zack Williams045b63d2019-01-22 16:30:57 -0700360 XOSProcessor._write_single_file(
361 rendered, args.output, args.dest_file, args.quiet
362 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700363
Sapan Bhatia4c835602017-07-14 01:13:17 -0400364 return rendered