blob: 9a6a249496f5282a10e4947342872fe7758f46e4 [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
Matteo Scandolo67654fa2017-06-09 09:33:17 -070022
Zack Williams9a42f872019-02-15 17:56:04 -070023from . import jinja2_extensions
24from .proto2xproto import Proto2XProto
25from .xos2jinja import XOS2Jinja
26
Zack Williams045b63d2019-01-22 16:30:57 -070027loader = jinja2.PackageLoader(__name__, "templates")
Matteo Scandolo67654fa2017-06-09 09:33:17 -070028env = jinja2.Environment(loader=loader)
29
Zack Williams045b63d2019-01-22 16:30:57 -070030
Scott Baker1f7791d2018-10-04 13:21:20 -070031class XOSProcessorArgs:
32 """ Helper class for use cases that want to call XOSProcessor directly, rather than executing xosgenx from the
33 command line.
34 """
35
36 default_rev = False
37 default_output = None
38 default_attic = None
39 default_kvpairs = None
40 default_write_to_file = None
41 default_dest_file = None
42 default_dest_extension = None
43 default_target = None
44 default_checkers = None
Zack Williams045b63d2019-01-22 16:30:57 -070045 default_verbosity = (
46 0
47 ) # Higher numbers = more verbosity, lower numbers = less verbosity
48 default_include_models = (
49 []
50 ) # If neither include_models nor include_apps is specified, then all models will
51 default_include_apps = [] # be included.
Scott Baker1f7791d2018-10-04 13:21:20 -070052
53 def __init__(self, **kwargs):
54 # set defaults
55 self.rev = XOSProcessorArgs.default_rev
56 self.output = XOSProcessorArgs.default_output
57 self.attic = XOSProcessorArgs.default_attic
58 self.kvpairs = XOSProcessorArgs.default_kvpairs
59 self.verbosity = XOSProcessorArgs.default_verbosity
60 self.write_to_file = XOSProcessorArgs.default_write_to_file
61 self.default_dest_file = XOSProcessorArgs.default_dest_file
62 self.default_dest_extension = XOSProcessorArgs.default_dest_extension
63 self.default_target = XOSProcessorArgs.default_target
64 self.default_checkers = XOSProcessorArgs.default_target
65 self.include_models = XOSProcessorArgs.default_include_models
66 self.include_apps = XOSProcessorArgs.default_include_apps
67
68 # override defaults with kwargs
Zack Williams045b63d2019-01-22 16:30:57 -070069 for (k, v) in kwargs.items():
Scott Baker1f7791d2018-10-04 13:21:20 -070070 setattr(self, k, v)
71
Matteo Scandolo67654fa2017-06-09 09:33:17 -070072
Zack Williams045b63d2019-01-22 16:30:57 -070073class XOSProcessor:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070074 @staticmethod
75 def _read_input_from_files(files):
Zack Williams045b63d2019-01-22 16:30:57 -070076 input = ""
Matteo Scandolo67654fa2017-06-09 09:33:17 -070077 for fname in files:
78 with open(fname) as infile:
79 input += infile.read()
80 return input
81
82 @staticmethod
83 def _attach_parser(ast, args):
Zack Williams045b63d2019-01-22 16:30:57 -070084 if hasattr(args, "rev") and args.rev:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070085 v = Proto2XProto()
86 ast.accept(v)
Sapan Bhatia4c835602017-07-14 01:13:17 -040087
Scott Bakerc237f882018-09-28 14:12:47 -070088 v = XOS2Jinja(args)
Sapan Bhatia4c835602017-07-14 01:13:17 -040089 ast.accept(v)
Matteo Scandolo67654fa2017-06-09 09:33:17 -070090 return v
91
92 @staticmethod
93 def _get_template(target):
94 if not os.path.isabs(target):
Zack Williams045b63d2019-01-22 16:30:57 -070095 return os.path.abspath(
96 os.path.dirname(os.path.realpath(__file__)) + "/targets/" + target
97 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -070098 return target
99
100 @staticmethod
101 def _file_exists(attic):
102 # NOTE this method can be used in the jinja template
103 def file_exists2(name):
104 if attic is not None:
Zack Williams045b63d2019-01-22 16:30:57 -0700105 path = attic + "/" + name
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700106 else:
107 path = name
Zack Williams045b63d2019-01-22 16:30:57 -0700108 return os.path.exists(path)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700109
110 return file_exists2
111
112 @staticmethod
113 def _include_file(attic):
114 # NOTE this method can be used in the jinja template
115 def include_file2(name):
116 if attic is not None:
Zack Williams045b63d2019-01-22 16:30:57 -0700117 path = attic + "/" + name
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700118 else:
119 path = name
120 return open(path).read()
Zack Williams045b63d2019-01-22 16:30:57 -0700121
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700122 return include_file2
123
124 @staticmethod
125 def _load_jinja2_extensions(os_template_env, attic):
126
Zack Williams045b63d2019-01-22 16:30:57 -0700127 os_template_env.globals["include_file"] = XOSProcessor._include_file(
128 attic
129 ) # Generates a function
130 os_template_env.globals["file_exists"] = XOSProcessor._file_exists(
131 attic
132 ) # Generates a function
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700133
Zack Williams045b63d2019-01-22 16:30:57 -0700134 os_template_env.filters["yaml"] = yaml.dump
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700135 for f in dir(jinja2_extensions):
Zack Williams045b63d2019-01-22 16:30:57 -0700136 if f.startswith("xproto"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700137 os_template_env.globals[f] = getattr(jinja2_extensions, f)
138 return os_template_env
139
140 @staticmethod
141 def _add_context(args):
Zack Williams045b63d2019-01-22 16:30:57 -0700142 if not hasattr(args, "kv") or not args.kv:
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700143 return
144 try:
145 context = {}
Zack Williams045b63d2019-01-22 16:30:57 -0700146 for s in args.kv.split(","):
147 k, val = s.split(":")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700148 context[k] = val
149 return context
Zack Williams045b63d2019-01-22 16:30:57 -0700150 except Exception as e:
Zack Williams9a42f872019-02-15 17:56:04 -0700151 print(e)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700152
153 @staticmethod
154 def _write_single_file(rendered, dir, dest_file, quiet):
155
156 file_name = "%s/%s" % (dir, dest_file)
Zack Williams045b63d2019-01-22 16:30:57 -0700157 file = open(file_name, "w")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700158 file.write(rendered)
159 file.close()
Zack Williams045b63d2019-01-22 16:30:57 -0700160 if not quiet:
161 print("Saved: %s" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700162
163 @staticmethod
Scott Bakera33ccb02018-01-26 13:03:28 -0800164 def _write_file_per_model(rendered, dir, suffix, quiet):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700165 for m in rendered:
Scott Bakera33ccb02018-01-26 13:03:28 -0800166 file_name = "%s/%s%s" % (dir, m.lower(), suffix)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700167 if not rendered[m]:
Zack Williams045b63d2019-01-22 16:30:57 -0700168 if not quiet:
169 print("Not saving %s as it is empty" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700170 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700171 file = open(file_name, "w")
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700172 file.write(rendered[m])
173 file.close()
Zack Williams045b63d2019-01-22 16:30:57 -0700174 if not quiet:
175 print("Saved: %s" % file_name)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700176
177 @staticmethod
178 def _write_split_target(rendered, dir, quiet):
179
180 lines = rendered.splitlines()
181 current_buffer = []
182 for l in lines:
Zack Williams045b63d2019-01-22 16:30:57 -0700183 if l.startswith("+++"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700184
185 if dir:
Zack Williams045b63d2019-01-22 16:30:57 -0700186 path = dir + "/" + l[4:].lower()
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700187
Zack Williams045b63d2019-01-22 16:30:57 -0700188 fil = open(path, "w")
189 buf = "\n".join(current_buffer)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700190
191 obuf = buf
192
193 fil.write(obuf)
194 fil.close()
195
Zack Williams045b63d2019-01-22 16:30:57 -0700196 if not quiet:
197 print("Save file to: %s" % path)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700198
199 current_buffer = []
200 else:
201 current_buffer.append(l)
202
203 @staticmethod
204 def _find_message_by_model_name(messages, model):
Zack Williams045b63d2019-01-22 16:30:57 -0700205 return next((x for x in messages if x["name"] == model), None)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700206
207 @staticmethod
Sapan Bhatia85b71012018-01-12 12:11:19 -0500208 def _find_last_nonempty_line(text, pointer):
209 ne_pointer = pointer
210 found = False
Zack Williams045b63d2019-01-22 16:30:57 -0700211 while ne_pointer != 0 and not found:
212 ne_pointer = text[: (ne_pointer - 1)].rfind("\n")
213 if ne_pointer < 0:
214 ne_pointer = 0
215 if text[ne_pointer - 1] != "\n":
Sapan Bhatia85b71012018-01-12 12:11:19 -0500216 found = True
217
218 return ne_pointer
219
220 @staticmethod
Zack Williams045b63d2019-01-22 16:30:57 -0700221 def process(args, operator=None):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700222 # Setting defaults
Zack Williams045b63d2019-01-22 16:30:57 -0700223 if not hasattr(args, "attic"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700224 args.attic = None
Zack Williams045b63d2019-01-22 16:30:57 -0700225 if not hasattr(args, "write_to_file"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700226 args.write_to_file = None
Zack Williams045b63d2019-01-22 16:30:57 -0700227 if not hasattr(args, "dest_file"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700228 args.dest_file = None
Zack Williams045b63d2019-01-22 16:30:57 -0700229 if not hasattr(args, "dest_extension"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700230 args.dest_extension = None
Zack Williams045b63d2019-01-22 16:30:57 -0700231 if not hasattr(args, "output"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700232 args.output = None
Zack Williams045b63d2019-01-22 16:30:57 -0700233 if not hasattr(args, "quiet"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700234 args.quiet = True
235
236 # Validating
Zack Williams045b63d2019-01-22 16:30:57 -0700237 if args.write_to_file == "single" and args.dest_file is None:
238 raise Exception(
239 "[XosGenX] write_to_file option is specified as 'single' but no dest_file is provided"
240 )
241 if args.write_to_file == "model" and (args.dest_extension is None):
242 raise Exception(
243 "[XosGenX] write_to_file option is specified as 'model' but no dest_extension is provided"
244 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700245
246 if args.output is not None and not os.path.isabs(args.output):
Scott Baker63c27ba2019-03-01 16:06:15 -0800247 raise Exception("[XosGenX] The output dir (%s) must be an absolute path!" % args.output)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700248 if args.output is not None and not os.path.isdir(args.output):
Scott Baker63c27ba2019-03-01 16:06:15 -0800249 raise Exception("[XosGenX] The output dir (%s) must be a directory!" % args.output)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700250
Zack Williams045b63d2019-01-22 16:30:57 -0700251 if hasattr(args, "files"):
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800252 inputs = XOSProcessor._read_input_from_files(args.files)
Zack Williams045b63d2019-01-22 16:30:57 -0700253 elif hasattr(args, "inputs"):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700254 inputs = args.inputs
255 else:
256 raise Exception("[XosGenX] No inputs provided!")
257
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800258 if not operator:
259 operator = args.target
260 template_path = XOSProcessor._get_template(operator)
261 else:
262 template_path = operator
263
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700264 [template_folder, template_name] = os.path.split(template_path)
265 os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder])
266 os_template_env = jinja2.Environment(loader=os_template_loader)
Zack Williams045b63d2019-01-22 16:30:57 -0700267 os_template_env = XOSProcessor._load_jinja2_extensions(
268 os_template_env, args.attic
269 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700270 template = os_template_env.get_template(template_name)
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800271 context = XOSProcessor._add_context(args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700272
273 parser = plyxproto.ProtobufAnalyzer()
Sapan Bhatia85b71012018-01-12 12:11:19 -0500274 try:
275 ast = parser.parse_string(inputs, debug=0)
Zack Williams045b63d2019-01-22 16:30:57 -0700276 except plyxproto.ParsingError as e:
Sapan Bhatia85b71012018-01-12 12:11:19 -0500277 line, start, end = e.error_range
278
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800279 ptr = XOSProcessor._find_last_nonempty_line(inputs, start)
Sapan Bhatia85b71012018-01-12 12:11:19 -0500280
281 if start == 0:
Zack Williams045b63d2019-01-22 16:30:57 -0700282 beginning = ""
Sapan Bhatia85b71012018-01-12 12:11:19 -0500283 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700284 beginning = inputs[ptr: start - 1]
Sapan Bhatia85b71012018-01-12 12:11:19 -0500285
Zack Williams045b63d2019-01-22 16:30:57 -0700286 line_end_char = inputs[start + end:].find("\n")
Sapan Bhatia85b71012018-01-12 12:11:19 -0500287 line_end = inputs[line_end_char]
288
289 if e.message:
290 error = e.message
291 else:
292 error = "xproto parsing error"
293
Zack Williams045b63d2019-01-22 16:30:57 -0700294 print(error + "\n" + Fore.YELLOW + "Line %d:" % line + Fore.WHITE)
295 print(
296 beginning
297 + Fore.YELLOW
298 + inputs[start - 1: start + end]
299 + Fore.WHITE
300 + line_end
301 )
Sapan Bhatia85b71012018-01-12 12:11:19 -0500302 exit(1)
303
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800304 v = XOSProcessor._attach_parser(ast, args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700305
Scott Baker1f7791d2018-10-04 13:21:20 -0700306 if args.include_models or args.include_apps:
307 for message in v.messages:
308 message["is_included"] = False
309 if message["name"] in args.include_models:
310 message["is_included"] = True
311 else:
Zack Williams9a42f872019-02-15 17:56:04 -0700312 app_label = (
313 message.get("options", {})
314 .get("app_label")
315 .strip('"')
316 )
Scott Baker1f7791d2018-10-04 13:21:20 -0700317 if app_label in args.include_apps:
318 message["is_included"] = True
319 else:
320 for message in v.messages:
321 message["is_included"] = True
322
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700323 if args.output is not None and args.write_to_file == "model":
324 rendered = {}
325 for i, model in enumerate(v.models):
326 models = {}
327 models[model] = v.models[model]
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800328 messages = [XOSProcessor._find_message_by_model_name(v.messages, model)]
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700329
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700330 rendered[model] = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700331 {
332 "proto": {
333 "message_table": models,
334 "messages": messages,
335 "policies": v.policies,
336 "message_names": [m["name"] for m in v.messages],
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700337 },
338 "context": context,
Zack Williams045b63d2019-01-22 16:30:57 -0700339 "options": v.options,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700340 }
341 )
Zack Williams045b63d2019-01-22 16:30:57 -0700342 if str(v.options.get("legacy", "false")).strip('"').lower() == "true":
Scott Bakera33ccb02018-01-26 13:03:28 -0800343 suffix = "_decl." + args.dest_extension
344 else:
345 suffix = "." + args.dest_extension
Zack Williams045b63d2019-01-22 16:30:57 -0700346 XOSProcessor._write_file_per_model(
347 rendered, args.output, suffix, args.quiet
348 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700349 else:
350 rendered = template.render(
Zack Williams045b63d2019-01-22 16:30:57 -0700351 {
352 "proto": {
353 "message_table": v.models,
354 "messages": v.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 )
362 if args.output is not None and args.write_to_file == "target":
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800363 XOSProcessor._write_split_target(rendered, args.output, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700364 elif args.output is not None and args.write_to_file == "single":
Zack Williams045b63d2019-01-22 16:30:57 -0700365 XOSProcessor._write_single_file(
366 rendered, args.output, args.dest_file, args.quiet
367 )
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700368
Sapan Bhatia4c835602017-07-14 01:13:17 -0400369 return rendered