Repository: ansible/ansible

Issue:
xml module: Adding diff support would help with debugging
##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
xml
##### ANSIBLE VERSION
v2.4

##### SUMMARY
I have issues with adding or setting child elements in a complex multi-namespaced path.
It would be a lot easier if the module would return the difference before/after, even in check_mode.

_Copied from original issue: cmprescott/ansible-xml#89_


Discussion:
cc @magnus919 @tbielawa
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Having a similar issue. I can set children using xpath but once the child spec gets complicated I can't get idempotent results. And since there is no diff support I've been adding statements trying to find the differences between what the server writes out and what Ansible writes out. I get that ordering is preserved for a reason but this module has no built in help for trying to find out what is different which makes testing very difficult.

Also not clear was default namespaces for children need to be set if you want subsequent 'set_children' runs to come back as not changed. Even though you can set them into an element that is namespaces, and lxml will insert the children with the namespace set, the code that detects changes isn't default namespace aware so it shows the children as being different, even though they aren't. So confusing!!!

If you don't include a xmlns= namespace at the top of your child xml that you set with set_children, then subsequent runs will always show as 'changed'. If you omit the namespace, but your xpath contains a namespace, the children will be set with the correct namespace, but the diff code currently in the module doesn't properly handle detecting the differences. Its not even close to correct. Gets tripped up by whitepsace, or ordering, or any number of other things.

So I would request a working diff, as well as a way of diffing with a module argument. The current situation is very difficult to use.
cc @sm4rk0
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc @cmprescott
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/files/xml.py b/lib/ansible/modules/files/xml.py
--- a/lib/ansible/modules/files/xml.py
+++ b/lib/ansible/modules/files/xml.py
@@ -94,6 +94,7 @@
 requirements:
 - lxml >= 2.3.0
 notes:
+- Use the C(--check) and C(--diff) options when testing your expressions.
 - This module does not handle complicated xpath expressions, so limit xpath selectors to simple expressions.
 - Beware that in case your XML elements are namespaced, you need to use the C(namespaces) parameter.
 - Namespaces prefix should be used for all children of an element where namespace is defined, unless another namespace is defined for them.
@@ -216,13 +217,13 @@
 import traceback
 
 from collections import MutableMapping
+import copy
 from distutils.version import LooseVersion
 from io import BytesIO
 
-
-HAS_LXML = True
 try:
     from lxml import etree
+    HAS_LXML = True
 except ImportError:
     HAS_LXML = False
 
@@ -621,19 +622,29 @@ def children_to_nodes(module=None, children=[], type='yaml'):
 
 
 def finish(module, tree, xpath, namespaces, changed=False, msg="", hitcount=0, matches=tuple()):
-    actions = dict(xpath=xpath, namespaces=namespaces, state=module.params['state'])
 
-    if not changed:
-        module.exit_json(changed=changed, actions=actions, msg=msg, count=hitcount, matches=matches)
+    result = dict(
+        actions=dict(xpath=xpath, namespaces=namespaces, state=module.params['state']),
+        changed=changed,
+        count=hitcount,
+        matches=matches,
+        msg=msg,
+    )
+
+    if changed and module._diff:
+        result['diff'] = dict(
+            before=etree.tostring(orig_doc, xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print']),
+            after=etree.tostring(tree, xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print']),
+        )
 
     if module.params['path']:
         if not module.check_mode:
             tree.write(module.params['path'], xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print'])
-        module.exit_json(changed=changed, actions=actions, msg=msg, count=hitcount, matches=matches)
 
     if module.params['xmlstring']:
-        xml_string = etree.tostring(tree, xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print'])
-        module.exit_json(changed=changed, actions=actions, msg=msg, count=hitcount, matches=matches, xmlstring=xml_string)
+        result['xmlstring'] = etree.tostring(tree, xml_declaration=True, encoding='UTF-8', pretty_print=module.params['pretty_print'])
+
+    module.exit_json(**result)
 
 
 def main():
@@ -704,6 +715,11 @@ def main():
     except etree.XMLSyntaxError as e:
         module.fail_json(msg="Error while parsing path: %s" % e)
 
+    # Ensure we have the original copy to compare
+    if module._diff:
+        global orig_doc
+        orig_doc = copy.deepcopy(doc)
+
     if print_match:
         do_print_match(module, doc, xpath, namespaces)
 


Test patch: