1#!/usr/bin/env python3 2 3""" 4This script applies patches to an XML file. 5 6The patch file is itself an XML file. It can have any root element name, 7and uses XML attributes to specify if the elements in the file should replace 8existing elements or add new ones. An XPath attribute is used to specify 9where the fix should be applied. A <targetFile> element is required in the 10patch file to specify the base name of the XML file the patches should be 11applied to, though the targetFile element is handled outside of this script. 12 13The only restriction is that since the type, xpath, and key attributes are 14used to specify the patch placement the target XML cannot use those at a 15top level element. 16 17 It can apply patches in 5 ways: 18 19 1) Add an element: 20 Put in the element to add, along with the type='add' attribute 21 and an xpath attribute specifying where the new element should go. 22 23 <enumerationType type='add' xpath="./"> 24 <id>MY_TYPE</id> 25 </enumerationType> 26 27 This will add a new enumerationType element child to the root element. 28 29 2) Replace an element: 30 Put in the new element, with the type='replace' attribute 31 and the XPath of the element you want to replace. 32 33 <enumerator type='replace' 34 xpath="enumerationType/[id='TYPE']/enumerator[name='XBUS']"> 35 <name>XBUS</name> 36 <value>the new XBUS value</value> 37 </enumerator> 38 39 This will replace the enumerator element with name XBUS under the 40 enumerationType element with ID TYPE. 41 42 3) Remove an element: 43 Put in the element to remove, with the type='remove' attribute and 44 the XPath of the element you want to remove. The full element contents 45 don't need to be specified, as the XPath is what locates the element. 46 47 <enumerator type='remove' 48 xpath='enumerationType[id='TYPE]/enumerator[name='DIMM']> 49 </enumerator> 50 51 This will remove the enumerator element with name DIMM under the 52 enumerationType element with ID TYPE. 53 54 4) Add child elements to a specific element. Useful when adding several 55 child elements at once. 56 57 Use a type attribute of 'add-child' and specify the target parent with 58 the xpath attribute. 59 60 <enumerationType type="add-child" xpath="enumerationType/[id='TYPE']"> 61 <enumerator> 62 <name>MY_NEW_ENUMERATOR</name> 63 <value>23</value> 64 </enumerator> 65 <enumerator> 66 <name>ANOTHER_NEW_ENUMERATOR</name> 67 <value>99</value> 68 </enumerator> 69 </enumerationType> 70 71 This will add 2 new <enumerator> elements to the enumerationType 72 element with ID TYPE. 73 74 5) Replace a child element inside another element, useful when replacing 75 several child elements of the same parent at once. 76 77 Use a type attribute of 'replace-child' and the xpath attribute 78 as described above, and also use the key attribute to specify which 79 element should be used to match on so the replace can be done. 80 81 <enumerationType type="replace-child" 82 key="name" 83 xpath="enumerationType/[id='TYPE']"> 84 <enumerator> 85 <name>OLD_ENUMERATOR</name> 86 <value>newvalue</value> 87 </enumerator> 88 <enumerator> 89 <name>ANOTHER_OLD_ENUMERATOR</name> 90 <value>anothernewvalue</value> 91 </enumerator> 92 </enumerationType> 93 94 This will replace the <enumerator> elements with the names of 95 OLD_ENUMERATOR and ANOTHER_OLD_ENUMERATOR with the <enumerator> 96 elements specified, inside of the enumerationType element with 97 ID TYPE. 98""" 99 100 101from lxml import etree 102import sys 103import argparse 104 105 106def delete_attrs(element, attrs): 107 for a in attrs: 108 try: 109 del element.attrib[a] 110 except: 111 pass 112 113if __name__ == '__main__': 114 115 parser = argparse.ArgumentParser("Applies fixes to XML files") 116 parser.add_argument("-x", dest='xml', help='The input XML file') 117 parser.add_argument("-p", dest='patch_xml', help='The patch XML file') 118 parser.add_argument("-o", dest='output_xml', help='The output XML file') 119 args = parser.parse_args() 120 121 if not all([args.xml, args.patch_xml, args.output_xml]): 122 parser.print_usage() 123 sys.exit(-1) 124 125 errors = [] 126 patch_num = 0 127 patch_tree = etree.parse(args.patch_xml) 128 patch_root = patch_tree.getroot() 129 tree = etree.parse(args.xml) 130 root = tree.getroot() 131 132 for node in patch_root: 133 if (node.tag is etree.PI) or (node.tag is etree.Comment) or \ 134 (node.tag == "targetFile"): 135 continue 136 137 patch_num = patch_num + 1 138 139 xpath = node.get('xpath', None) 140 patch_type = node.get('type', 'add') 141 patch_key = node.get('key', None) 142 delete_attrs(node, ['xpath', 'type', 'key']) 143 144 print("Patch " + str(patch_num) + ":") 145 146 try: 147 if xpath is None: 148 raise Exception(" E> No XPath attribute found") 149 150 target = tree.find(xpath) 151 152 if target is None: 153 raise Exception(" E> Could not find XPath target " + xpath) 154 155 if patch_type == "add": 156 157 print(" Adding element " + target.tag + " to " + xpath) 158 159 #The ServerWiz API is dependent on ordering for the 160 #elements at the root node, so make sure they get appended 161 #at the end. 162 if (xpath == "./") or (xpath == "/"): 163 root.append(node) 164 else: 165 target.append(node) 166 167 elif patch_type == "remove": 168 169 print(" Removing element " + xpath) 170 parent = target.find("..") 171 if parent is None: 172 raise Exception(" E> Could not find parent of " + xpath + 173 " so can't remove this element") 174 parent.remove(target) 175 176 elif patch_type == "replace": 177 178 print(" Replacing element " + xpath) 179 parent = target.find("..") 180 if parent is None: 181 raise Exception(" E> Could not find parent of " + xpath + 182 " so can't replace this element") 183 parent.remove(target) 184 parent.append(node) 185 186 elif patch_type == "add-child": 187 188 for child in node: 189 print(" Adding a '" + child.tag + "' child element" 190 " to " + xpath) 191 target.append(child) 192 193 elif patch_type == "replace-child": 194 195 if patch_key is None: 196 raise Exception(" E> Patch type is replace-child, but" 197 " 'key' attribute isn't set") 198 updates = [] 199 for child in node: 200 #Use the key to figure out which element to replace 201 key_element = child.find(patch_key) 202 for target_child in target: 203 for grandchild in target_child: 204 if (grandchild.tag == patch_key) and \ 205 (grandchild.text == key_element.text): 206 update = {} 207 update['remove'] = target_child 208 update['add'] = child 209 updates.append(update) 210 211 for update in updates: 212 print(" Replacing a '" + update['remove'].tag + 213 "' element in path " + xpath) 214 target.remove(update['remove']) 215 target.append(update['add']) 216 217 else: 218 raise Exception(" E> Unknown patch type attribute found: " + 219 patch_type) 220 221 except Exception as e: 222 print(e) 223 errors.append(e) 224 225 tree.write(args.output_xml) 226 227 if errors: 228 print("Exiting with " + str(len(errors)) + " total errors") 229 sys.exit(-1) 230