Add patchxml.py for patching MRW XML.

This script can apply fixes specified in another
XML file to the target XML file.  Details on the
format of the patch XML can be found in patchxml.py.

Change-Id: I2bb8e495d7b309d9245b19922fdeda869052ebcf
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
diff --git a/patchxml.py b/patchxml.py
new file mode 100755
index 0000000..ada4e31
--- /dev/null
+++ b/patchxml.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python
+
+"""
+This script applies patches to an XML file.
+
+The patch file is itself an XML file.  It can have any root element name,
+and uses XML attributes to specify if the elements in the file should replace
+existing elements or add new ones.  An XPath attribute is used to specify
+where the fix should be applied.  A <targetFile> element is required in the
+patch file to specify the base name of the XML file the patches should be
+applied to, though the targetFile element is handled outside of this script.
+
+The only restriction is that since the type, xpath, and key attributes are
+used to specify the patch placement the target XML cannot use those at a
+top level element.
+
+ It can apply patches in 5 ways:
+
+ 1) Add an element:
+    Put in the element to add, along with the type='add' attribute
+    and an xpath attribute specifying where the new element should go.
+
+     <enumerationType type='add' xpath="./">
+       <id>MY_TYPE</id>
+     </enumerationType>
+
+     This will add a new enumerationType element child to the root element.
+
+ 2) Replace an element:
+    Put in the new element, with the type='replace' attribute
+    and the XPath of the element you want to replace.
+
+     <enumerator type='replace'
+               xpath="enumerationType/[id='TYPE']/enumerator[name='XBUS']">
+       <name>XBUS</name>
+       <value>the new XBUS value</value>
+     </enumerator>
+
+    This will replace the enumerator element with name XBUS under the
+    enumerationType element with ID TYPE.
+
+ 3) Remove an element:
+    Put in the element to remove, with the type='remove' attribute and
+    the XPath of the element you want to remove. The full element contents
+    don't need to be specified, as the XPath is what locates the element.
+
+    <enumerator type='remove'
+                xpath='enumerationType[id='TYPE]/enumerator[name='DIMM']>
+    </enumerator>
+
+    This will remove the enumerator element with name DIMM under the
+    enumerationType element with ID TYPE.
+
+ 4) Add child elements to a specific element.  Useful when adding several
+    child elements at once.
+
+    Use a type attribute of 'add-child' and specify the target parent with
+    the xpath attribute.
+
+     <enumerationType type="add-child" xpath="enumerationType/[id='TYPE']">
+       <enumerator>
+         <name>MY_NEW_ENUMERATOR</name>
+         <value>23</value>
+       </enumerator>
+       <enumerator>
+         <name>ANOTHER_NEW_ENUMERATOR</name>
+         <value>99</value>
+       </enumerator>
+     </enumerationType>
+
+     This will add 2 new <enumerator> elements to the enumerationType
+     element with ID TYPE.
+
+ 5) Replace a child element inside another element, useful when replacing
+    several child elements of the same parent at once.
+
+    Use a type attribute of 'replace-child' and the xpath attribute
+    as described above, and also use the key attribute to specify which
+    element should be used to match on so the replace can be done.
+
+     <enumerationType type="replace-child"
+                      key="name"
+                      xpath="enumerationType/[id='TYPE']">
+       <enumerator>
+         <name>OLD_ENUMERATOR</name>
+         <value>newvalue</value>
+       </enumerator>
+       <enumerator>
+         <name>ANOTHER_OLD_ENUMERATOR</name>
+         <value>anothernewvalue</value>
+       </enumerator>
+     </enumerationType>
+
+     This will replace the <enumerator> elements with the names of
+     OLD_ENUMERATOR and ANOTHER_OLD_ENUMERATOR with the <enumerator>
+     elements specified, inside of the enumerationType element with
+     ID TYPE.
+"""
+
+
+from lxml import etree
+import sys
+import argparse
+
+
+def delete_attrs(element, attrs):
+    for a in attrs:
+        try:
+            del element.attrib[a]
+        except:
+            pass
+
+if __name__ == '__main__':
+
+    parser = argparse.ArgumentParser("Applies fixes to XML files")
+    parser.add_argument("-x", dest='xml', help='The input XML file')
+    parser.add_argument("-p", dest='patch_xml', help='The patch XML file')
+    parser.add_argument("-o", dest='output_xml', help='The output XML file')
+    args = parser.parse_args()
+
+    if not all([args.xml, args.patch_xml, args.output_xml]):
+        parser.print_usage()
+        sys.exit(-1)
+
+    rc = 0
+    patch_num = 0
+    patch_tree = etree.parse(args.patch_xml)
+    patch_root = patch_tree.getroot()
+    tree = etree.parse(args.xml)
+    root = tree.getroot()
+
+    for node in patch_root:
+        if (node.tag is etree.PI) or (node.tag is etree.Comment) or \
+           (node.tag == "targetFile"):
+            continue
+
+        xpath = node.get('xpath', None)
+        patch_type = node.get('type', 'add')
+        patch_key = node.get('key', None)
+        delete_attrs(node, ['xpath', 'type', 'key'])
+
+        if xpath is None:
+            print("No XPath attribute found for patch " + str(patch_num))
+            rc = -1
+        else:
+            target = tree.find(xpath)
+
+            if target is None:
+                print("Patch " + str(patch_num) + ": Could not find XPath "
+                      "target " + xpath)
+                rc = -1
+            else:
+                print("Patch " + str(patch_num) + ":")
+
+                if patch_type == "add":
+
+                    print("  Adding element " + target.tag + " to " + xpath)
+
+                    #The ServerWiz API is dependent on ordering for the
+                    #elements at the root node, so make sure they get appended
+                    #at the end.
+                    if (xpath == "./") or (xpath == "/"):
+                        root.append(node)
+                    else:
+                        target.append(node)
+
+                elif patch_type == "remove":
+
+                    print("  Removing element " + xpath)
+                    parent = target.find("..")
+                    if parent is None:
+                        print("Could not find parent of " + xpath +
+                              " so can't remove this element")
+                        rc = -1
+                    else:
+                        parent.remove(target)
+
+                elif patch_type == "replace":
+
+                    print("  Replacing element " + xpath)
+                    parent = target.find("..")
+                    if parent is None:
+                        print("Could not find parent of " + xpath +
+                              " so can't replace this element")
+                        rc = -1
+                    else:
+                        parent.remove(target)
+                        parent.append(node)
+
+                elif patch_type == "add-child":
+
+                    for child in node:
+                        print("  Adding a '" + child.tag + "' child element "
+                              "to " + xpath)
+                        target.append(child)
+
+                elif patch_type == "replace-child":
+
+                    if patch_key is not None:
+                        updates = []
+                        for child in node:
+                            #Use the key to figure out which element to replace
+                            key_element = child.find(patch_key)
+                            for target_child in target:
+                                for grandchild in target_child:
+                                    if (grandchild.tag == patch_key) and \
+                                       (grandchild.text == key_element.text):
+                                        update = {}
+                                        update['remove'] = target_child
+                                        update['add'] = child
+                                        updates.append(update)
+
+                        for update in updates:
+                            print("  Replacing a '" + update['remove'].tag +
+                                  "' element in path " + xpath)
+                            target.remove(update['remove'])
+                            target.append(update['add'])
+
+                    else:
+                        print("Patch type is replace-child, but 'key' "
+                              "attribute isn't set")
+                        rc = -1
+
+                else:
+                    print("Unknown patch type attribute found:  " + patch_type)
+                    rc = -1
+
+        patch_num = patch_num + 1
+
+    tree.write(args.output_xml)
+    sys.exit(rc)