blob: 3355fb5c0557ced06726a2b672958cc07193f42e [file] [log] [blame]
Matteo Scandolod2044a42017-08-07 16:08:28 -07001
2# Copyright 2017-present Open Networking Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16
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
26loader = jinja2.PackageLoader(__name__, 'templates')
27env = jinja2.Environment(loader=loader)
28
Scott Baker1f7791d2018-10-04 13:21:20 -070029class XOSProcessorArgs:
30 """ Helper class for use cases that want to call XOSProcessor directly, rather than executing xosgenx from the
31 command line.
32 """
33
34 default_rev = False
35 default_output = None
36 default_attic = None
37 default_kvpairs = None
38 default_write_to_file = None
39 default_dest_file = None
40 default_dest_extension = None
41 default_target = None
42 default_checkers = None
43 default_verbosity = 0 # Higher numbers = more verbosity, lower numbers = less verbosity
44 default_include_models = [] # If neither include_models nor include_apps is specified, then all models will
45 default_include_apps = [] # be included.
46
47 def __init__(self, **kwargs):
48 # set defaults
49 self.rev = XOSProcessorArgs.default_rev
50 self.output = XOSProcessorArgs.default_output
51 self.attic = XOSProcessorArgs.default_attic
52 self.kvpairs = XOSProcessorArgs.default_kvpairs
53 self.verbosity = XOSProcessorArgs.default_verbosity
54 self.write_to_file = XOSProcessorArgs.default_write_to_file
55 self.default_dest_file = XOSProcessorArgs.default_dest_file
56 self.default_dest_extension = XOSProcessorArgs.default_dest_extension
57 self.default_target = XOSProcessorArgs.default_target
58 self.default_checkers = XOSProcessorArgs.default_target
59 self.include_models = XOSProcessorArgs.default_include_models
60 self.include_apps = XOSProcessorArgs.default_include_apps
61
62 # override defaults with kwargs
63 for (k,v) in kwargs.items():
64 setattr(self, k, v)
65
Sapan Bhatiabfb233a2018-02-09 14:53:09 -080066class XOSProcessor:
Matteo Scandolo67654fa2017-06-09 09:33:17 -070067
68 @staticmethod
69 def _read_input_from_files(files):
70 input = ''
71 for fname in files:
72 with open(fname) as infile:
73 input += infile.read()
74 return input
75
76 @staticmethod
77 def _attach_parser(ast, args):
78 if hasattr(args, 'rev') and args.rev:
79 v = Proto2XProto()
80 ast.accept(v)
Sapan Bhatia4c835602017-07-14 01:13:17 -040081
Scott Bakerc237f882018-09-28 14:12:47 -070082 v = XOS2Jinja(args)
Sapan Bhatia4c835602017-07-14 01:13:17 -040083 ast.accept(v)
Matteo Scandolo67654fa2017-06-09 09:33:17 -070084 return v
85
86 @staticmethod
87 def _get_template(target):
88 if not os.path.isabs(target):
89 return os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/targets/' + target)
90 return target
91
92 @staticmethod
93 def _file_exists(attic):
94 # NOTE this method can be used in the jinja template
95 def file_exists2(name):
96 if attic is not None:
97 path = attic + '/' + name
98 else:
99 path = name
100 return (os.path.exists(path))
101
102 return file_exists2
103
104 @staticmethod
105 def _include_file(attic):
106 # NOTE this method can be used in the jinja template
107 def include_file2(name):
108 if attic is not None:
109 path = attic + '/' + name
110 else:
111 path = name
112 return open(path).read()
113 return include_file2
114
115 @staticmethod
116 def _load_jinja2_extensions(os_template_env, attic):
117
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800118 os_template_env.globals['include_file'] = XOSProcessor._include_file(attic) # Generates a function
119 os_template_env.globals['file_exists'] = XOSProcessor._file_exists(attic) # Generates a function
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700120
121 os_template_env.filters['yaml'] = yaml.dump
122 for f in dir(jinja2_extensions):
123 if f.startswith('xproto'):
124 os_template_env.globals[f] = getattr(jinja2_extensions, f)
125 return os_template_env
126
127 @staticmethod
128 def _add_context(args):
129 if not hasattr(args, 'kv') or not args.kv:
130 return
131 try:
132 context = {}
133 for s in args.kv.split(','):
134 k, val = s.split(':')
135 context[k] = val
136 return context
137 except Exception, e:
138 print e.message
139
140 @staticmethod
141 def _write_single_file(rendered, dir, dest_file, quiet):
142
143 file_name = "%s/%s" % (dir, dest_file)
144 file = open(file_name, 'w')
145 file.write(rendered)
146 file.close()
147 if quiet == False:
148 print "Saved: %s" % file_name
149
150 @staticmethod
Scott Bakera33ccb02018-01-26 13:03:28 -0800151 def _write_file_per_model(rendered, dir, suffix, quiet):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700152 for m in rendered:
Scott Bakera33ccb02018-01-26 13:03:28 -0800153 file_name = "%s/%s%s" % (dir, m.lower(), suffix)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700154 if not rendered[m]:
155 if quiet == False:
156 print "Not saving %s as it is empty" % file_name
157 else:
158 file = open(file_name, 'w')
159 file.write(rendered[m])
160 file.close()
161 if quiet == False:
162 print "Saved: %s" % file_name
163
164 @staticmethod
165 def _write_split_target(rendered, dir, quiet):
166
167 lines = rendered.splitlines()
168 current_buffer = []
169 for l in lines:
170 if (l.startswith('+++')):
171
172 if dir:
173 path = dir + '/' + l[4:].lower()
174
175 fil = open(path, 'w')
176 buf = '\n'.join(current_buffer)
177
178 obuf = buf
179
180 fil.write(obuf)
181 fil.close()
182
183 if quiet == False:
184 print "Save file to: %s" % path
185
186 current_buffer = []
187 else:
188 current_buffer.append(l)
189
190 @staticmethod
191 def _find_message_by_model_name(messages, model):
192 return next((x for x in messages if x['name'] == model), None)
193
194 @staticmethod
Sapan Bhatia85b71012018-01-12 12:11:19 -0500195 def _find_last_nonempty_line(text, pointer):
196 ne_pointer = pointer
197 found = False
198 while ne_pointer!=0 and not found:
199 ne_pointer = text[:(ne_pointer-1)].rfind('\n')
200 if ne_pointer<0: ne_pointer = 0
201 if text[ne_pointer-1]!='\n':
202 found = True
203
204 return ne_pointer
205
206 @staticmethod
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800207 def process(args, operator = None):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700208 # Setting defaults
209 if not hasattr(args, 'attic'):
210 args.attic = None
211 if not hasattr(args, 'write_to_file'):
212 args.write_to_file = None
213 if not hasattr(args, 'dest_file'):
214 args.dest_file = None
215 if not hasattr(args, 'dest_extension'):
216 args.dest_extension = None
217 if not hasattr(args, 'output'):
218 args.output = None
219 if not hasattr(args, 'quiet'):
220 args.quiet = True
221
222 # Validating
223 if args.write_to_file == 'single' and args.dest_file is None:
224 raise Exception("[XosGenX] write_to_file option is specified as 'single' but no dest_file is provided")
Scott Bakera33ccb02018-01-26 13:03:28 -0800225 if args.write_to_file == 'model' and (args.dest_extension is None):
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700226 raise Exception("[XosGenX] write_to_file option is specified as 'model' but no dest_extension is provided")
227
228 if args.output is not None and not os.path.isabs(args.output):
229 raise Exception("[XosGenX] The output dir must be an absolute path!")
230 if args.output is not None and not os.path.isdir(args.output):
231 raise Exception("[XosGenX] The output dir must be a directory!")
232
233 if hasattr(args, 'files'):
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800234 inputs = XOSProcessor._read_input_from_files(args.files)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700235 elif hasattr(args, 'inputs'):
236 inputs = args.inputs
237 else:
238 raise Exception("[XosGenX] No inputs provided!")
239
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800240 if not operator:
241 operator = args.target
242 template_path = XOSProcessor._get_template(operator)
243 else:
244 template_path = operator
245
246
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700247 [template_folder, template_name] = os.path.split(template_path)
248 os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder])
249 os_template_env = jinja2.Environment(loader=os_template_loader)
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800250 os_template_env = XOSProcessor._load_jinja2_extensions(os_template_env, args.attic)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700251 template = os_template_env.get_template(template_name)
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800252 context = XOSProcessor._add_context(args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700253
254 parser = plyxproto.ProtobufAnalyzer()
Sapan Bhatia85b71012018-01-12 12:11:19 -0500255 try:
256 ast = parser.parse_string(inputs, debug=0)
257 except plyxproto.ParsingError, e:
258 line, start, end = e.error_range
259
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800260 ptr = XOSProcessor._find_last_nonempty_line(inputs, start)
Sapan Bhatia85b71012018-01-12 12:11:19 -0500261
262 if start == 0:
263 beginning = ''
264 else:
Scott Baker1f7791d2018-10-04 13:21:20 -0700265 beginning = inputs[ptr:start-1]
Sapan Bhatia85b71012018-01-12 12:11:19 -0500266
267 line_end_char = inputs[start+end:].find('\n')
268 line_end = inputs[line_end_char]
269
270 if e.message:
271 error = e.message
272 else:
273 error = "xproto parsing error"
274
275 print error + "\n" + Fore.YELLOW + "Line %d:"%line + Fore.WHITE
276 print beginning + Fore.YELLOW + inputs[start-1:start+end] + Fore.WHITE + line_end
277 exit(1)
278
279
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800280 v = XOSProcessor._attach_parser(ast, args)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700281
Scott Baker1f7791d2018-10-04 13:21:20 -0700282 if args.include_models or args.include_apps:
283 for message in v.messages:
284 message["is_included"] = False
285 if message["name"] in args.include_models:
286 message["is_included"] = True
287 else:
288 app_label = message.get("options", {}).get("app_label").strip('"')
289 if app_label in args.include_apps:
290 message["is_included"] = True
291 else:
292 for message in v.messages:
293 message["is_included"] = True
294
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700295 if args.output is not None and args.write_to_file == "model":
296 rendered = {}
297 for i, model in enumerate(v.models):
298 models = {}
299 models[model] = v.models[model]
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800300 messages = [XOSProcessor._find_message_by_model_name(v.messages, model)]
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700301
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700302 rendered[model] = template.render(
303 {"proto":
304 {
305 'message_table': models,
306 'messages': messages,
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700307 'policies': v.policies,
Scott Baker1f7791d2018-10-04 13:21:20 -0700308 'message_names': [m['name'] for m in v.messages]
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700309 },
310 "context": context,
311 "options": v.options
312 }
313 )
Scott Bakera33ccb02018-01-26 13:03:28 -0800314 if (str(v.options.get("legacy", "false")).strip('"').lower() == "true"):
315 suffix = "_decl." + args.dest_extension
316 else:
317 suffix = "." + args.dest_extension
318 XOSProcessor._write_file_per_model(rendered, args.output, suffix, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700319 else:
320 rendered = template.render(
321 {"proto":
322 {
323 'message_table': v.models,
324 'messages': v.messages,
Sapan Bhatiadb183c22017-06-23 02:47:42 -0700325 'policies': v.policies,
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700326 'message_names': [m['name'] for m in v.messages]
327 },
328 "context": context,
329 "options": v.options
330 }
331 )
332 if args.output is not None and args.write_to_file == "target":
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800333 XOSProcessor._write_split_target(rendered, args.output, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700334 elif args.output is not None and args.write_to_file == "single":
Sapan Bhatiabfb233a2018-02-09 14:53:09 -0800335 XOSProcessor._write_single_file(rendered, args.output, args.dest_file, args.quiet)
Matteo Scandolo67654fa2017-06-09 09:33:17 -0700336
Sapan Bhatia4c835602017-07-14 01:13:17 -0400337 return rendered