blob: 4db64ded875e8f1c7334e0d27ca807bfade0dcf0 [file] [log] [blame]
#!/usr/bin/env python3
"""
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)
errors = []
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
patch_num = patch_num + 1
xpath = node.get('xpath', None)
patch_type = node.get('type', 'add')
patch_key = node.get('key', None)
delete_attrs(node, ['xpath', 'type', 'key'])
print("Patch " + str(patch_num) + ":")
try:
if xpath is None:
raise Exception(" E> No XPath attribute found")
target = tree.find(xpath)
if target is None:
raise Exception(" E> Could not find XPath target " + xpath)
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:
raise Exception(" E> Could not find parent of " + xpath +
" so can't remove this element")
parent.remove(target)
elif patch_type == "replace":
print(" Replacing element " + xpath)
parent = target.find("..")
if parent is None:
raise Exception(" E> Could not find parent of " + xpath +
" so can't replace this element")
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 None:
raise Exception(" E> Patch type is replace-child, but"
" 'key' attribute isn't set")
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:
raise Exception(" E> Unknown patch type attribute found: " +
patch_type)
except Exception as e:
print(e)
errors.append(e)
tree.write(args.output_xml)
if errors:
print("Exiting with " + str(len(errors)) + " total errors")
sys.exit(-1)