xref: /openbmc/phosphor-mrw-tools/patchxml.py (revision 721dcbd2)
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