#!/usr/bin/env python3 # # override-default: preprocessor for XML interface definitions to interpret # redundant entries (relative to path) with tag 'defaultValue' as an override # directive. Must be called before build-command-templates, as the schema # disallows redundancy. # # Copyright (C) 2021 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # # # Use lxml xpath capability to find multiple elements with tag defaultValue # relative to path; replace and remove to override the value. import sys import glob import logging from copy import deepcopy from lxml import etree debug = False logger = logging.getLogger(__name__) logs_handler = logging.StreamHandler() logger.addHandler(logs_handler) if debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) def clear_empty_path(el): # on the odd chance of interleaved comments tmp = [l for l in el if isinstance(l.tag, str)] if not tmp: p = el.getparent() p.remove(el) clear_empty_path(p) def override_element(l: list): """ Allow multiple override elements; use the final one (in document order). """ if len(l) < 2: logger.debug("passing list of single element to override_element") return # assemble list of leafNodes of overriding defaultValues, for later removal parents = [] for el in l[1:]: parents.append(el.getparent()) # replace element with final override l[0].getparent().replace(l[0], l[-1]) # remove all but overridden element for el in parents: tmp = el.getparent() tmp.remove(el) clear_empty_path(tmp) def merge_remaining(l: list, elementtree): """ Merge (now) single leaf node containing 'defaultValue' with leaf nodes of same path and no 'defaultValue'. """ for p in l: p = p.split() path_str = f'/interfaceDefinition/*' path_list = [] for i in range(len(p)): path_list.append(f'[@name="{p[i]}"]') path_str += '/children/*'.join(path_list) rp = elementtree.xpath(path_str) if len(rp) > 1: for el in rp[1:]: # in practice there will only be one child of the path, # either defaultValue or Properties, since # override_element() has already run for child in el: rp[0].append(deepcopy(child)) tmp = el.getparent() tmp.remove(el) clear_empty_path(tmp) def collect_and_override(dir_name): """ Collect elements with defaultValue tag into dictionary indexed by name attributes of ancestor path. """ for fname in glob.glob(f'{dir_name}/*.xml'): tree = etree.parse(fname) root = tree.getroot() defv = {} xpath_str = '//defaultValue' xp = tree.xpath(xpath_str) for element in xp: ap = element.xpath('ancestor::*[@name]') ap_name = [el.get("name") for el in ap] ap_path_str = ' '.join(ap_name) defv.setdefault(ap_path_str, []).append(element) for k, v in defv.items(): if len(v) > 1: logger.info(f"overriding default in path '{k}'") override_element(v) to_merge = list(defv) merge_remaining(to_merge, tree) revised_str = etree.tostring(root, encoding='unicode', pretty_print=True) with open(f'{fname}', 'w') as f: f.write(revised_str) def main(): if len(sys.argv) < 2: logger.critical('Must specify XML directory!') sys.exit(1) dir_name = sys.argv[1] collect_and_override(dir_name) if __name__ == '__main__': main()