#!/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-2024 VyOS maintainers # # 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 re import sys import os import argparse import copy import functools from lxml import etree as ET from textwrap import fill # Defaults validator_dir = "/opt/vyatta/libexec/validators" 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 ## Load and validate the inputs try: xml = ET.parse(input_file) except Exception as e: print(f"Failed to load interface definition file {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(f"Interface definition file {input_file} does not match the schema!") sys.exit(1) except Exception as e: print(f"Failed to load the XML schema {schema_file}") print(e) sys.exit(1) if not os.access(output_dir, os.W_OK): print(f"The output directory {output_dir} is not writeable") 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 get_properties(p): props = {} if p is None: return props # Get the help string try: props["help"] = p.find("help").text except: props["help"] = "No help available" # 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") comptype = c.find("imagePath") # Current backend doesn't support multiple allowed: tags # so we get to emulate it comp_exprs = [] for i in lists: comp_exprs.append("echo \"{0}\"".format(i.text)) for i in paths: path = re.sub(r'\s+', '/', i.text) comp_exprs.append("ls /opt/vyatta/config/active/{0} 2>/dev/null".format(path)) for i in scripts: comp_exprs.append("{0}".format(i.text)) if comptype is not None: props["comp_type"] = "imagefiles" comp_exprs.append("echo -n \"\"") comp_help = " && ".join(comp_exprs) props["comp_help"] = comp_help except: props["comp_help"] = [] return props def make_node_def(props, command): # XXX: replace with a template processor if it grows # out of control node_def = "" if "help" in props: help = props["help"] help = fill(help, width=64, subsequent_indent='\t\t\t') node_def += f'help: {help}\n' if "comp_type" in props: node_def += f'comptype: {props["comp_type"]}\n' if "comp_help" in props: node_def += f'allowed: {props["comp_help"]}\n' if command is not None: node_def += f'run: {command.text}\n' if debug: print('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") command = n.find("command") name = n.get("name") node_type = n.tag my_tmpl_dir.append(name) if debug: print(f"Name of the node: {name};\n Created directory: ", end="") os.makedirs(make_path(my_tmpl_dir), exist_ok=True) props = get_properties(props_elem) nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def") if node_type == "node": if debug: print(f"Processing node {name}") # Only create the "node.def" file if it exists but is empty, or if it # does not exist at all. 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, command)) if children is not None: inner_nodes = children.iterfind("*") for inner_n in inner_nodes: process_node(inner_n, my_tmpl_dir) elif node_type == "tagNode": if debug: print(f"Processing tagNode {name}") os.makedirs(make_path(my_tmpl_dir), exist_ok=True) # Only create the "node.def" file if it exists but is empty, or if it # does not exist at all. if not os.path.exists(nodedef_path) or os.path.getsize(nodedef_path) == 0: with open(nodedef_path, "w") as f: f.write('help: {0}\n'.format(props['help'])) # Create the inner node.tag part my_tmpl_dir.append("node.tag") os.makedirs(make_path(my_tmpl_dir), exist_ok=True) if debug: print("Created path for the tagNode: {}".format(make_path(my_tmpl_dir)), end="") # Not sure if we want partially defined tag nodes, write the file unconditionally 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. 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, command)) if children is not None: inner_nodes = children.iterfind("*") for inner_n in inner_nodes: process_node(inner_n, my_tmpl_dir) elif node_type == "leafNode": # This is a leaf node if debug: print(f"Processing leaf node {name}") 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, command)) else: print(f"Unknown node_type: {node_type}") def get_node_key(node, attr=None): """ Return the sorting key of an xml node using tag and attributes """ if attr is None: return '%s' % node.tag + ':'.join([node.get(attr) for attr in sorted(node.attrib)]) if attr in node.attrib: return '%s:%s' % (node.tag, node.get(attr)) return '%s' % node.tag def sort_children(node, attr=None): """ Sort children along tag and given attribute. if attr is None, sort along all attributes """ if not isinstance(node.tag, str): # PYTHON 2: use basestring instead # not a TAG, it is comment or DATA # no need to sort return # sort child along attr node[:] = sorted(node, key=lambda child: get_node_key(child, attr)) # and recurse for child in node: sort_children(child, attr) root = xml.getroot() # process_node() processes the XML tree in a fixed order, "node" before "tagNode" # before "leafNode". If the generator created a "node.def" file, it can no longer # be overwritten - else we would have some stale "node.def" files with an empty # help string (T2555). Without the fixed order this would resulted in a case # where we get a node and a tagNode with the same name, e.g. "show interfaces # ethernet" and "show interfaces ethernet eth0" that the node implementation # was not callable from the CLI, rendering this command useless (T3807). # # This can be fixed by forcing the "node", "tagNode", "leafNode" order by sorting # the input XML file automatically (sorting from https://stackoverflow.com/a/46128043) # thus adding no additional overhead to the user. sort_children(root, 'name') nodes = root.iterfind("*") for n in nodes: process_node(n, [output_dir])