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 101import argparse 102import sys 103 104from lxml import etree 105 106 107def delete_attrs(element, attrs): 108 for a in attrs: 109 try: 110 del element.attrib[a] 111 except Exception: 112 pass 113 114 115if __name__ == "__main__": 116 parser = argparse.ArgumentParser("Applies fixes to XML files") 117 parser.add_argument("-x", dest="xml", help="The input XML file") 118 parser.add_argument("-p", dest="patch_xml", help="The patch XML file") 119 parser.add_argument("-o", dest="output_xml", help="The output XML file") 120 args = parser.parse_args() 121 122 if not all([args.xml, args.patch_xml, args.output_xml]): 123 parser.print_usage() 124 sys.exit(-1) 125 126 errors = [] 127 patch_num = 0 128 patch_tree = etree.parse(args.patch_xml) 129 patch_root = patch_tree.getroot() 130 tree = etree.parse(args.xml) 131 root = tree.getroot() 132 133 for node in patch_root: 134 if ( 135 (node.tag is etree.PI) 136 or (node.tag is etree.Comment) 137 or (node.tag == "targetFile") 138 ): 139 continue 140 141 patch_num = patch_num + 1 142 143 xpath = node.get("xpath", None) 144 patch_type = node.get("type", "add") 145 patch_key = node.get("key", None) 146 delete_attrs(node, ["xpath", "type", "key"]) 147 148 print("Patch " + str(patch_num) + ":") 149 150 try: 151 if xpath is None: 152 raise Exception(" E> No XPath attribute found") 153 154 target = tree.find(xpath) 155 156 if target is None: 157 raise Exception(" E> Could not find XPath target " + xpath) 158 159 if patch_type == "add": 160 print(" Adding element " + target.tag + " to " + xpath) 161 162 # The ServerWiz API is dependent on ordering for the 163 # elements at the root node, so make sure they get appended 164 # at the end. 165 if (xpath == "./") or (xpath == "/"): 166 root.append(node) 167 else: 168 target.append(node) 169 170 elif patch_type == "remove": 171 print(" Removing element " + xpath) 172 parent = target.find("..") 173 if parent is None: 174 raise Exception( 175 " E> Could not find parent of " 176 + xpath 177 + " so can't remove this element" 178 ) 179 parent.remove(target) 180 181 elif patch_type == "replace": 182 print(" Replacing element " + xpath) 183 parent = target.find("..") 184 if parent is None: 185 raise Exception( 186 " E> Could not find parent of " 187 + xpath 188 + " so can't replace this element" 189 ) 190 parent.remove(target) 191 parent.append(node) 192 193 elif patch_type == "add-child": 194 for child in node: 195 print( 196 " Adding a '" 197 + child.tag 198 + "' child element to " 199 + xpath 200 ) 201 target.append(child) 202 203 elif patch_type == "replace-child": 204 if patch_key is None: 205 raise Exception( 206 " E> Patch type is replace-child, but" 207 " 'key' attribute isn't set" 208 ) 209 updates = [] 210 for child in node: 211 # Use the key to figure out which element to replace 212 key_element = child.find(patch_key) 213 for target_child in target: 214 for grandchild in target_child: 215 if (grandchild.tag == patch_key) and ( 216 grandchild.text == key_element.text 217 ): 218 update = {} 219 update["remove"] = target_child 220 update["add"] = child 221 updates.append(update) 222 223 for update in updates: 224 print( 225 " Replacing a '" 226 + update["remove"].tag 227 + "' element in path " 228 + xpath 229 ) 230 target.remove(update["remove"]) 231 target.append(update["add"]) 232 233 else: 234 raise Exception( 235 " E> Unknown patch type attribute found: " + patch_type 236 ) 237 238 except Exception as e: 239 print(e) 240 errors.append(e) 241 242 tree.write(args.output_xml) 243 244 if errors: 245 print("Exiting with " + str(len(errors)) + " total errors") 246 sys.exit(-1) 247