xref: /openbmc/phosphor-mrw-tools/patchxml.py (revision 66367a15)
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
101import argparse
102import sys
103
104from lxml import etree
105
106
107def delete_attrs(element, attrs):
108    for a in attrs:
109        try:
110            del element.attrib[a]
111        except Exception:
112            pass
113
114
115if __name__ == "__main__":
116    parser = argparse.ArgumentParser("Applies fixes to XML files")
117    parser.add_argument("-x", dest="xml", help="The input XML file")
118    parser.add_argument("-p", dest="patch_xml", help="The patch XML file")
119    parser.add_argument("-o", dest="output_xml", help="The output XML file")
120    args = parser.parse_args()
121
122    if not all([args.xml, args.patch_xml, args.output_xml]):
123        parser.print_usage()
124        sys.exit(-1)
125
126    errors = []
127    patch_num = 0
128    patch_tree = etree.parse(args.patch_xml)
129    patch_root = patch_tree.getroot()
130    tree = etree.parse(args.xml)
131    root = tree.getroot()
132
133    for node in patch_root:
134        if (
135            (node.tag is etree.PI)
136            or (node.tag is etree.Comment)
137            or (node.tag == "targetFile")
138        ):
139            continue
140
141        patch_num = patch_num + 1
142
143        xpath = node.get("xpath", None)
144        patch_type = node.get("type", "add")
145        patch_key = node.get("key", None)
146        delete_attrs(node, ["xpath", "type", "key"])
147
148        print("Patch " + str(patch_num) + ":")
149
150        try:
151            if xpath is None:
152                raise Exception("  E>  No XPath attribute found")
153
154            target = tree.find(xpath)
155
156            if target is None:
157                raise Exception("  E>  Could not find XPath target " + xpath)
158
159            if patch_type == "add":
160                print("  Adding element " + target.tag + " to " + xpath)
161
162                # The ServerWiz API is dependent on ordering for the
163                # elements at the root node, so make sure they get appended
164                # at the end.
165                if (xpath == "./") or (xpath == "/"):
166                    root.append(node)
167                else:
168                    target.append(node)
169
170            elif patch_type == "remove":
171                print("  Removing element " + xpath)
172                parent = target.find("..")
173                if parent is None:
174                    raise Exception(
175                        "  E>  Could not find parent of "
176                        + xpath
177                        + " so can't remove this element"
178                    )
179                parent.remove(target)
180
181            elif patch_type == "replace":
182                print("  Replacing element " + xpath)
183                parent = target.find("..")
184                if parent is None:
185                    raise Exception(
186                        "  E>  Could not find parent of "
187                        + xpath
188                        + " so can't replace this element"
189                    )
190                parent.remove(target)
191                parent.append(node)
192
193            elif patch_type == "add-child":
194                for child in node:
195                    print(
196                        "  Adding a '"
197                        + child.tag
198                        + "' child element to "
199                        + xpath
200                    )
201                    target.append(child)
202
203            elif patch_type == "replace-child":
204                if patch_key is None:
205                    raise Exception(
206                        "  E>  Patch type is replace-child, but"
207                        " 'key' attribute isn't set"
208                    )
209                updates = []
210                for child in node:
211                    # Use the key to figure out which element to replace
212                    key_element = child.find(patch_key)
213                    for target_child in target:
214                        for grandchild in target_child:
215                            if (grandchild.tag == patch_key) and (
216                                grandchild.text == key_element.text
217                            ):
218                                update = {}
219                                update["remove"] = target_child
220                                update["add"] = child
221                                updates.append(update)
222
223                for update in updates:
224                    print(
225                        "  Replacing a '"
226                        + update["remove"].tag
227                        + "' element in path "
228                        + xpath
229                    )
230                    target.remove(update["remove"])
231                    target.append(update["add"])
232
233            else:
234                raise Exception(
235                    "  E>  Unknown patch type attribute found:  " + patch_type
236                )
237
238        except Exception as e:
239            print(e)
240            errors.append(e)
241
242    tree.write(args.output_xml)
243
244    if errors:
245        print("Exiting with " + str(len(errors)) + " total errors")
246        sys.exit(-1)
247