Patrick Williams | 721dcbd | 2020-04-08 07:32:57 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 2 | |
| 3 | """ |
| 4 | This script applies patches to an XML file. |
| 5 | |
| 6 | The patch file is itself an XML file. It can have any root element name, |
| 7 | and uses XML attributes to specify if the elements in the file should replace |
| 8 | existing elements or add new ones. An XPath attribute is used to specify |
| 9 | where the fix should be applied. A <targetFile> element is required in the |
| 10 | patch file to specify the base name of the XML file the patches should be |
| 11 | applied to, though the targetFile element is handled outside of this script. |
| 12 | |
| 13 | The only restriction is that since the type, xpath, and key attributes are |
| 14 | used to specify the patch placement the target XML cannot use those at a |
| 15 | top 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 | |
| 101 | from lxml import etree |
| 102 | import sys |
| 103 | import argparse |
| 104 | |
| 105 | |
| 106 | def delete_attrs(element, attrs): |
| 107 | for a in attrs: |
| 108 | try: |
| 109 | del element.attrib[a] |
| 110 | except: |
| 111 | pass |
| 112 | |
| 113 | if __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 | |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 125 | errors = [] |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 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 | |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 137 | patch_num = patch_num + 1 |
| 138 | |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 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 | |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 144 | print("Patch " + str(patch_num) + ":") |
| 145 | |
| 146 | try: |
| 147 | if xpath is None: |
| 148 | raise Exception(" E> No XPath attribute found") |
| 149 | |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 150 | target = tree.find(xpath) |
| 151 | |
| 152 | if target is None: |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 153 | raise Exception(" E> Could not find XPath target " + xpath) |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 154 | |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 155 | if patch_type == "add": |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 156 | |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 157 | print(" Adding element " + target.tag + " to " + xpath) |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 158 | |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 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) |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 164 | else: |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 165 | target.append(node) |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 166 | |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 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: |
Patrick Williams | 721dcbd | 2020-04-08 07:32:57 -0500 | [diff] [blame] | 222 | print(e) |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 223 | errors.append(e) |
Matt Spinler | ab015d7 | 2016-09-06 15:33:17 -0500 | [diff] [blame] | 224 | |
| 225 | tree.write(args.output_xml) |
Matt Spinler | cb99d1e | 2016-09-13 14:47:25 -0500 | [diff] [blame] | 226 | |
| 227 | if errors: |
| 228 | print("Exiting with " + str(len(errors)) + " total errors") |
| 229 | sys.exit(-1) |