#!/usr/bin/env python3 # # build-command-template: converts new style command definitions in XML # to the old style (bunch of dirs and node.def's) command templates # # Copyright (C) 2017 VyOS maintainers <maintainers@vyos.net> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA import sys import os import argparse import copy import functools from lxml import etree as ET from textwrap import fill # Defaults #validator_dir = "/usr/libexec/vyos/validators" validator_dir = "${vyos_validators_dir}" default_constraint_err_msg = "Invalid value" ## Get arguments parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates') parser.add_argument('--debug', help='Enable debug information output', action='store_true') parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file") parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file") parser.add_argument('OUTPUT_DIR', type=str, help="Output directory") args = parser.parse_args() input_file = args.INPUT_FILE schema_file = args.SCHEMA_FILE output_dir = args.OUTPUT_DIR debug = args.debug #debug = True ## Load and validate the inputs try: xml = ET.parse(input_file) except Exception as e: print("Failed to load interface definition file {0}".format(input_file)) print(e) sys.exit(1) try: relaxng_xml = ET.parse(schema_file) validator = ET.RelaxNG(relaxng_xml) if not validator.validate(xml): print(validator.error_log) print("Interface definition file {0} does not match the schema!".format(input_file)) sys.exit(1) except Exception as e: print("Failed to load the XML schema {0}".format(schema_file)) print(e) sys.exit(1) if not os.access(output_dir, os.W_OK): print("The output directory {0} is not writeable".format(output_dir)) sys.exit(1) ## If we got this far, everything must be ok and we can convert the file def make_path(l): path = functools.reduce(os.path.join, l) if debug: print(path) return path def collect_validators(ve): regexes = [] regex_elements = ve.findall("regex") if regex_elements is not None: regexes = list(map(lambda e: e.text.strip().replace('\\','\\\\'), regex_elements)) if "" in regexes: print("Warning: empty regex, node will be accepting any value") validator_elements = ve.findall("validator") validators = [] if validator_elements is not None: for v in validator_elements: v_name = os.path.join(validator_dir, v.get("name")) # XXX: lxml returns None for empty arguments v_argument = None try: v_argument = v.get("argument") except: pass if v_argument is None: v_argument = "" validators.append("{0} {1}".format(v_name, v_argument)) regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes)) validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators)) return regex_args + " " + validator_args def get_properties(p, default=None): props = {} if p is None: return props # Get the help string try: help = p.find("help").text if default != None: # DNS forwarding for instance has multiple defaults - specified as whitespace separated list tmp = ', '.join(default.text.split()) help += f' (default: {tmp})' help = fill(help, width=64, subsequent_indent='\t\t\t') props["help"] = help except: pass # Get value help strings try: vhe = p.findall("valueHelp") vh = [] for v in vhe: format = v.find("format").text description = v.find("description").text if default != None and default.text == format: description += f' (default)' vh.append( (format, description) ) props["val_help"] = vh except: props["val_help"] = [] # Get the constraint and constraintGroup statements error_msg = default_constraint_err_msg # Get the error message if it's there try: error_msg = p.find("constraintErrorMessage").text except: pass vce = p.find("constraint") distinct_validator_string = "" if vce is not None: # The old backend doesn't support multiple validators in OR mode # so we emulate it distinct_validator_string = collect_validators(vce) vcge = p.findall("constraintGroup") group_validator_string = "" if len(vcge): for vcg in vcge: group_validator_string = group_validator_string + " --grp " + collect_validators(vcg) if vce is not None or len(vcge): validator_script = '${vyos_libexec_dir}/validate-value' validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, distinct_validator_string, group_validator_string, error_msg) props["constraint"] = validator_string # Get the completion help strings try: che = p.findall("completionHelp") ch = "" for c in che: scripts = c.findall("script") paths = c.findall("path") lists = c.findall("list") # Current backend doesn't support multiple allowed: tags # so we get to emulate it comp_exprs = [] for i in lists: comp_exprs.append(f'echo "{i.text}"') for i in paths: comp_exprs.append(f'/bin/cli-shell-api listNodes {i.text}') for i in scripts: comp_exprs.append(f'sh -c "{i.text}"') comp_help = ' && echo " " && '.join(comp_exprs) props["comp_help"] = comp_help except: props["comp_help"] = [] # Get priority try: props["priority"] = p.find("priority").text except: pass # Get "multi" if p.find("multi") is not None: props["multi"] = True # Get "valueless" if p.find("valueless") is not None: props["valueless"] = True return props def make_node_def(props): # XXX: replace with a template processor if it grows # out of control node_def = "" if "tag" in props: node_def += "tag:\n" if "multi" in props: node_def += "multi:\n" if "type" in props: # Will always be txt in practice if it's set node_def += "type: {0}\n".format(props["type"]) if "priority" in props: node_def += "priority: {0}\n".format(props["priority"]) if "help" in props: node_def += "help: {0}\n".format(props["help"]) if "val_help" in props: for v in props["val_help"]: node_def += "val_help: {0}; {1}\n".format(v[0], v[1]) if "comp_help" in props: node_def += "allowed: {0}\n".format(props["comp_help"]) if "constraint" in props: node_def += "syntax:expression: {0}\n".format(props["constraint"]) shim = '${vyshim}' if "owner" in props: if "tag" in props: node_def += "end: sudo sh -c \"{1} VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"], shim) else: node_def += "end: sudo sh -c \"{1} {0}\"\n".format(props["owner"], shim) if debug: print("The contents of the node.def file:\n", node_def) return node_def def process_node(n, tmpl_dir): # Avoid mangling the path from the outer call my_tmpl_dir = copy.copy(tmpl_dir) props_elem = n.find("properties") children = n.find("children") name = n.get("name") owner = n.get("owner") node_type = n.tag my_tmpl_dir.append(name) if debug: print("Name of the node: {0}. Created directory: {1}\n".format(name, "/".join(my_tmpl_dir)), end="") os.makedirs(make_path(my_tmpl_dir), exist_ok=True) props = get_properties(props_elem, n.find("defaultValue")) if owner: props["owner"] = owner # Type should not be set for non-tag, non-leaf nodes # For non-valueless leaf nodes, set the type to txt: to make them have some type, # actual value validation is handled by constraints translated to syntax:expression: if node_type != "node": if "valueless" not in props.keys(): props["type"] = "txt" if node_type == "tagNode": props["tag"] = "True" if node_type != "leafNode": if "multi" in props: raise ValueError("<multi/> tag is only allowed in <leafNode>") if "valueless" in props: raise ValueError("<valueless/> is only allowed in <leafNode>") nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def") # Only create the "node.def" file if it exists but is empty, or if it does # not exist at all. An empty node.def file could be generated by XML paths # that derive from one another bot having a common base structure like # "protocols static" if not os.path.exists(nodedef_path) or os.path.getsize(nodedef_path) == 0: with open(nodedef_path, "w") as f: f.write(make_node_def(props)) if node_type == "node": inner_nodes = children.iterfind("*") for inner_n in inner_nodes: process_node(inner_n, my_tmpl_dir) if node_type == "tagNode": my_tmpl_dir.append("node.tag") if debug: print("Created path for the tagNode:", end="") os.makedirs(make_path(my_tmpl_dir), exist_ok=True) inner_nodes = children.iterfind("*") for inner_n in inner_nodes: process_node(inner_n, my_tmpl_dir) else: # This is a leaf node pass root = xml.getroot() nodes = root.iterfind("*") for n in nodes: if n.tag == "syntaxVersion": continue process_node(n, [output_dir])