xref: /openbmc/phosphor-mrw-tools/patchxml.py (revision 7a68567af7bf121143ef135152a39723518b6e06)
1#!/usr/bin/env python
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    rc = 0
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        xpath = node.get('xpath', None)
138        patch_type = node.get('type', 'add')
139        patch_key = node.get('key', None)
140        delete_attrs(node, ['xpath', 'type', 'key'])
141
142        if xpath is None:
143            print("No XPath attribute found for patch " + str(patch_num))
144            rc = -1
145        else:
146            target = tree.find(xpath)
147
148            if target is None:
149                print("Patch " + str(patch_num) + ": Could not find XPath "
150                      "target " + xpath)
151                rc = -1
152            else:
153                print("Patch " + str(patch_num) + ":")
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                        print("Could not find parent of " + xpath +
173                              " so can't remove this element")
174                        rc = -1
175                    else:
176                        parent.remove(target)
177
178                elif patch_type == "replace":
179
180                    print("  Replacing element " + xpath)
181                    parent = target.find("..")
182                    if parent is None:
183                        print("Could not find parent of " + xpath +
184                              " so can't replace this element")
185                        rc = -1
186                    else:
187                        parent.remove(target)
188                        parent.append(node)
189
190                elif patch_type == "add-child":
191
192                    for child in node:
193                        print("  Adding a '" + child.tag + "' child element "
194                              "to " + xpath)
195                        target.append(child)
196
197                elif patch_type == "replace-child":
198
199                    if patch_key is not None:
200                        updates = []
201                        for child in node:
202                            #Use the key to figure out which element to replace
203                            key_element = child.find(patch_key)
204                            for target_child in target:
205                                for grandchild in target_child:
206                                    if (grandchild.tag == patch_key) and \
207                                       (grandchild.text == key_element.text):
208                                        update = {}
209                                        update['remove'] = target_child
210                                        update['add'] = child
211                                        updates.append(update)
212
213                        for update in updates:
214                            print("  Replacing a '" + update['remove'].tag +
215                                  "' element in path " + xpath)
216                            target.remove(update['remove'])
217                            target.append(update['add'])
218
219                    else:
220                        print("Patch type is replace-child, but 'key' "
221                              "attribute isn't set")
222                        rc = -1
223
224                else:
225                    print("Unknown patch type attribute found:  " + patch_type)
226                    rc = -1
227
228        patch_num = patch_num + 1
229
230    tree.write(args.output_xml)
231    sys.exit(rc)
232