| # Copyright 2017-present Open Networking Foundation |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| from __future__ import absolute_import, print_function |
| |
| import os |
| import jinja2 |
| import plyxproto.parser as plyxproto |
| import yaml |
| from colorama import Fore |
| import sys |
| |
| from . import jinja2_extensions |
| from .proto2xproto import Proto2XProto |
| from .xos2jinja import XOS2Jinja |
| from .validator import XProtoValidator |
| |
| loader = jinja2.PackageLoader(__name__, "templates") |
| env = jinja2.Environment(loader=loader) |
| |
| |
| class XOSProcessorArgs: |
| """ Helper class for use cases that want to call XOSProcessor directly, rather than executing xosgenx from the |
| command line. |
| """ |
| |
| default_rev = False |
| default_output = None |
| default_attic = None |
| default_kvpairs = None |
| default_write_to_file = None |
| default_dest_file = None |
| default_dest_extension = None |
| default_target = None |
| default_checkers = None |
| default_verbosity = ( |
| 0 |
| ) # Higher numbers = more verbosity, lower numbers = less verbosity |
| default_include_models = ( |
| [] |
| ) # If neither include_models nor include_apps is specified, then all models will |
| default_include_apps = [] # be included. |
| default_strict_validation = False |
| default_lint = False |
| |
| def __init__(self, **kwargs): |
| # set defaults |
| self.rev = XOSProcessorArgs.default_rev |
| self.output = XOSProcessorArgs.default_output |
| self.attic = XOSProcessorArgs.default_attic |
| self.kvpairs = XOSProcessorArgs.default_kvpairs |
| self.verbosity = XOSProcessorArgs.default_verbosity |
| self.write_to_file = XOSProcessorArgs.default_write_to_file |
| self.default_dest_file = XOSProcessorArgs.default_dest_file |
| self.default_dest_extension = XOSProcessorArgs.default_dest_extension |
| self.default_target = XOSProcessorArgs.default_target |
| self.default_checkers = XOSProcessorArgs.default_target |
| self.include_models = XOSProcessorArgs.default_include_models |
| self.include_apps = XOSProcessorArgs.default_include_apps |
| self.strict_validation = XOSProcessorArgs.default_strict_validation |
| self.lint = XOSProcessorArgs.default_lint |
| |
| # override defaults with kwargs |
| for (k, v) in kwargs.items(): |
| setattr(self, k, v) |
| |
| |
| class XOSProcessor: |
| @staticmethod |
| def _read_input_from_files(files): |
| """ Read the files and return the combined text read. |
| |
| Also returns a list of (line_number, filename) tuples that tell which |
| starting line corresponds to each file. |
| """ |
| line_map = [] |
| input = "" |
| for fname in files: |
| with open(fname) as infile: |
| line_map.append((len(input.split("\n")), fname)) |
| input += infile.read() |
| return (input, line_map) |
| |
| @staticmethod |
| def _attach_parser(ast, args): |
| if hasattr(args, "rev") and args.rev: |
| v = Proto2XProto() |
| ast.accept(v) |
| |
| v = XOS2Jinja(args) |
| ast.accept(v) |
| return v |
| |
| @staticmethod |
| def _get_template(target): |
| if not os.path.isabs(target): |
| return os.path.abspath( |
| os.path.dirname(os.path.realpath(__file__)) + "/targets/" + target |
| ) |
| return target |
| |
| @staticmethod |
| def _file_exists(attic): |
| # NOTE this method can be used in the jinja template |
| def file_exists2(name): |
| if attic is not None: |
| path = attic + "/" + name |
| else: |
| path = name |
| return os.path.exists(path) |
| |
| return file_exists2 |
| |
| @staticmethod |
| def _include_file(attic): |
| # NOTE this method can be used in the jinja template |
| def include_file2(name): |
| if attic is not None: |
| path = attic + "/" + name |
| else: |
| path = name |
| return open(path).read() |
| |
| return include_file2 |
| |
| @staticmethod |
| def _load_jinja2_extensions(os_template_env, attic): |
| |
| os_template_env.globals["include_file"] = XOSProcessor._include_file( |
| attic |
| ) # Generates a function |
| os_template_env.globals["file_exists"] = XOSProcessor._file_exists( |
| attic |
| ) # Generates a function |
| |
| os_template_env.filters["yaml"] = yaml.dump |
| for f in dir(jinja2_extensions): |
| if f.startswith("xproto"): |
| os_template_env.globals[f] = getattr(jinja2_extensions, f) |
| return os_template_env |
| |
| @staticmethod |
| def _add_context(args): |
| if not hasattr(args, "kv") or not args.kv: |
| return |
| try: |
| context = {} |
| for s in args.kv.split(","): |
| k, val = s.split(":") |
| context[k] = val |
| return context |
| except Exception as e: |
| print(e) |
| |
| @staticmethod |
| def _write_single_file(rendered, dir, dest_file, quiet): |
| |
| file_name = "%s/%s" % (dir, dest_file) |
| file = open(file_name, "w") |
| file.write(rendered) |
| file.close() |
| if not quiet: |
| print("Saved: %s" % file_name) |
| |
| @staticmethod |
| def _write_split_target(rendered, dir, quiet): |
| |
| lines = rendered.splitlines() |
| current_buffer = [] |
| for line in lines: |
| if line.startswith("+++"): |
| |
| if dir: |
| path = dir + "/" + line[4:].lower() |
| |
| fil = open(path, "w") |
| buf = "\n".join(current_buffer) |
| |
| obuf = buf |
| |
| fil.write(obuf) |
| fil.close() |
| |
| if not quiet: |
| print("Save file to: %s" % path) |
| |
| current_buffer = [] |
| else: |
| current_buffer.append(line) |
| |
| @staticmethod |
| def _find_message_by_model_name(messages, model): |
| return next((x for x in messages if x["name"] == model), None) |
| |
| @staticmethod |
| def _find_last_nonempty_line(text, pointer): |
| ne_pointer = pointer |
| found = False |
| while ne_pointer != 0 and not found: |
| ne_pointer = text[: (ne_pointer - 1)].rfind("\n") |
| if ne_pointer < 0: |
| ne_pointer = 0 |
| if text[ne_pointer - 1] != "\n": |
| found = True |
| |
| return ne_pointer |
| |
| @staticmethod |
| def process(args, operator=None): |
| # Setting defaults |
| if not hasattr(args, "attic"): |
| args.attic = None |
| if not hasattr(args, "write_to_file"): |
| args.write_to_file = None |
| if not hasattr(args, "dest_file"): |
| args.dest_file = None |
| if not hasattr(args, "dest_extension"): |
| args.dest_extension = None |
| if not hasattr(args, "output"): |
| args.output = None |
| if not hasattr(args, "quiet"): |
| args.quiet = True |
| |
| # Validating |
| if args.write_to_file == "single" and args.dest_file is None: |
| raise Exception( |
| "[XosGenX] write_to_file option is specified as 'single' but no dest_file is provided" |
| ) |
| if args.write_to_file == "model" and (args.dest_extension is None): |
| raise Exception( |
| "[XosGenX] write_to_file option is specified as 'model' but no dest_extension is provided" |
| ) |
| |
| if args.output is not None and not os.path.isabs(args.output): |
| raise Exception("[XosGenX] The output dir (%s) must be an absolute path!" % args.output) |
| if args.output is not None and not os.path.isdir(args.output): |
| raise Exception("[XosGenX] The output dir (%s) must be a directory!" % args.output) |
| |
| if hasattr(args, "files"): |
| (inputs, line_map) = XOSProcessor._read_input_from_files(args.files) |
| elif hasattr(args, "inputs"): |
| inputs = args.inputs |
| line_map = [] |
| else: |
| raise Exception("[XosGenX] No inputs provided!") |
| |
| context = XOSProcessor._add_context(args) |
| |
| parser = plyxproto.ProtobufAnalyzer() |
| try: |
| ast = parser.parse_string(inputs, debug=0) |
| except plyxproto.ParsingError as e: |
| if e.message: |
| error = e.message |
| else: |
| error = "xproto parsing error" |
| if e.error_range is None: |
| # No line number information |
| print(error + "\n") |
| else: |
| line, start, end = e.error_range |
| |
| ptr = XOSProcessor._find_last_nonempty_line(inputs, start) |
| |
| if start == 0: |
| beginning = "" |
| else: |
| beginning = inputs[ptr: start - 1] |
| |
| line_end_char = inputs[start + end:].find("\n") |
| line_end = inputs[line_end_char] |
| |
| print(error + "\n" + Fore.YELLOW + "Line %d:" % line + Fore.WHITE) |
| print( |
| beginning |
| + Fore.YELLOW |
| + inputs[start - 1: start + end] |
| + Fore.WHITE |
| + line_end |
| ) |
| exit(1) |
| |
| v = XOSProcessor._attach_parser(ast, args) |
| |
| if args.include_models or args.include_apps: |
| for message in v.messages: |
| message["is_included"] = False |
| if message["name"] in args.include_models: |
| message["is_included"] = True |
| else: |
| app_label = ( |
| message.get("options", {}) |
| .get("app_label") |
| .strip('"') |
| ) |
| if app_label in args.include_apps: |
| message["is_included"] = True |
| else: |
| for message in v.messages: |
| message["is_included"] = True |
| |
| validator = XProtoValidator(v.models, line_map) |
| validator.validate() |
| if validator.errors: |
| if args.strict_validation or (args.verbosity >= 0): |
| validator.print_errors() |
| fatal_errors = [x for x in validator.errors if x["severity"] == "ERROR"] |
| if fatal_errors and args.strict_validation: |
| sys.exit(-1) |
| |
| if args.lint: |
| return "" |
| |
| if not operator: |
| operator = args.target |
| template_path = XOSProcessor._get_template(operator) |
| else: |
| template_path = operator |
| |
| [template_folder, template_name] = os.path.split(template_path) |
| os_template_loader = jinja2.FileSystemLoader(searchpath=[template_folder]) |
| os_template_env = jinja2.Environment(loader=os_template_loader) |
| os_template_env = XOSProcessor._load_jinja2_extensions( |
| os_template_env, args.attic |
| ) |
| template = os_template_env.get_template(template_name) |
| |
| if args.output is not None and args.write_to_file == "model": |
| # Handle the case where each model is written to a separate python file. |
| rendered = {} |
| |
| for i, model in enumerate(v.models): |
| model_dict = v.models[model] |
| messages = [XOSProcessor._find_message_by_model_name(v.messages, model)] |
| |
| rendered[model] = template.render( |
| { |
| "proto": { |
| "message_table": {model: model_dict}, |
| "messages": messages, |
| "policies": v.policies, |
| "message_names": [m["name"] for m in v.messages], |
| }, |
| "context": context, |
| "options": v.options, |
| } |
| ) |
| if not rendered[model]: |
| print("Not saving model %s as it is empty" % model, file=sys.stderr) |
| else: |
| legacy = jinja2_extensions.base.xproto_list_evaluates_true( |
| [model_dict.get("options", {}).get("custom_python", None), |
| model_dict.get("options", {}).get("legacy", None), |
| v.options.get("custom_python", None), |
| v.options.get("legacy", None)]) |
| |
| if legacy: |
| file_name = "%s/%s_decl.%s" % (args.output, model.lower(), args.dest_extension) |
| else: |
| file_name = "%s/%s.%s" % (args.output, model.lower(), args.dest_extension) |
| |
| file = open(file_name, "w") |
| file.write(rendered[model]) |
| file.close() |
| if not args.quiet: |
| print("Saved: %s" % file_name, file=sys.stderr) |
| else: |
| # Handle the case where all models are written to the same python file. |
| |
| rendered = template.render( |
| { |
| "proto": { |
| "message_table": v.models, |
| "messages": v.messages, |
| "policies": v.policies, |
| "message_names": [m["name"] for m in v.messages], |
| }, |
| "context": context, |
| "options": v.options, |
| } |
| ) |
| if args.output is not None and args.write_to_file == "target": |
| XOSProcessor._write_split_target(rendered, args.output, args.quiet) |
| elif args.output is not None and args.write_to_file == "single": |
| XOSProcessor._write_single_file( |
| rendered, args.output, args.dest_file, args.quiet |
| ) |
| |
| return rendered |