Repository: ansible/ansible

Issue:
xml module: Changing an attribute value is non-obvious ('attribute' is not documented)
##### ISSUE TYPE
 - Documentation Report

##### COMPONENT NAME
lib/ansible/modules/files/xml.py

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```shell
charles@work ~/ansible.gerph (master￪13)> ansible --version
ansible 2.2.1.0
  config file = /home/charles/.ansible.cfg
  configured module search path = Default w/o overrides
```

I'm using the module on an earlier version of ansible than the latest, as I've just noticed that it's been pulled in to the main core. This particular issue should not be relevant to the version.

##### OS / ENVIRONMENT
N/A, although I'm testing with ubuntu/trusty.

##### SUMMARY
I want to change an attribute value in an XML file, but the way you do it isn't obvious using the documented calls. The 'xpath' parameter + 'value' would seem to be all I need, but it appears that when changing the attribute of an element it is necessary to explicitly select the element node and supply the attribute value, rather than specifying the attribute node and giving the value to set.


##### STEPS TO REPRODUCE
My initial attempt to do this was:

```yaml
- name: Update port number
  xml:
    file: /opt/jfrog/artifactory/tomcat/conf/server.xml
    xpath: "/Server/Service/Connector[not(@protocol)]/@port"
    value: "{{ artifactory_port }}"
    pretty_print: yes
```
However this returns an error, saying that it selects no nodes. That seems an outright lie :) For example:

```shell
root@charon:/home/local# xpath -e '/Server/Service/Connector[not(@protocol)]/@port' /opt/jfrog/artifactory/tomcat/conf/server.xml
Found 1 nodes in /opt/jfrog/artifactory/tomcat/conf/server.xml:
-- NODE --
 port="8081"

root@charon:/home/local# xmllint --xpath '/Server/Service/Connector[not(@protocol)]/@port' /opt/jfrog/artifactory/tomcat/conf/server.xml
 port="8081"
```

Both the perl xpath, and the xmllint one both believe that this selects a node for the attribute.

A little digging around showed that there is an 'attribute' parameter that names the attribute on an element node that needs to be set. The following works:

```yaml
- name: Update port number
  xml:
    file: /opt/jfrog/artifactory/tomcat/conf/server.xml
    xpath: "/Server/Service/Connector[not(@protocol)]"
    attribute: 'port'
    value: "{{ artifactory_port }}"
    pretty_print: yes
```

So the problem is at least 2-fold:

* Changing an attribute value is non-obvious, and requires the element to be selected, and an out-of-xpath 'attribute' supplied.
* The documentation of the 'attribute' parameter is not given.

... and potentially:

* An example of changing an attribute value would really help make this obvious.

It'd be nice if the xpath '.../@port' could be used, rather than having a separate syntax for changing attribute values, but as a quick and easy fix, documenting that it's not (and maybe giving a clue in the error message if the xpath element ends in `/@<name>`) would probably be enough.


##### EXPECTED RESULTS
* I had expected that plain 'xpath' + 'value' to work.
* I had hoped to find something in the documentation that would explain why this didn't work and what I needed to do to change the attribute.
* (after finding the answer by looking at the test code) I had hoped that I had missed the documentation of the 'attribute' parameter.



Discussion:
cc @dagwieers @magnus919 @sm4rk0 @tbielawa
[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
@@ -51,6 +51,10 @@
     default: present
     choices: [ absent, present ]
     aliases: [ ensure ]
+  attribute:
+    description:
+    - The attribute to select when using parameter C(value).
+    - This is a string, not prepended with C(@).
   value:
     description:
     - Desired state of the selected attribute.
@@ -59,23 +63,27 @@
     - Attributes default to an empty string.
   add_children:
     description:
-    - Add additional child-element(s) to a selected element.
+    - Add additional child-element(s) to a selected element for a given C(xpath).
     - Child elements must be given in a list and each item may be either a string
       (eg. C(children=ansible) to add an empty C(<ansible/>) child element),
       or a hash where the key is an element name and the value is the element value.
+    - This parameter requires C(xpath) to be set.
   set_children:
     description:
-    - Set the child-element(s) of a selected element.
+    - Set the child-element(s) of a selected element for a given C(xpath).
     - Removes any existing children.
     - Child elements must be specified as in C(add_children).
+    - This parameter requires C(xpath) to be set.
   count:
     description:
     - Search for a given C(xpath) and provide the count of any matches.
+    - This parameter requires C(xpath) to be set.
     type: bool
     default: 'no'
   print_match:
     description:
     - Search for a given C(xpath) and print out any matches.
+    - This parameter requires C(xpath) to be set.
     type: bool
     default: 'no'
   pretty_print:
@@ -86,6 +94,7 @@
   content:
     description:
     - Search for a given C(xpath) and get content.
+    - This parameter requires C(xpath) to be set.
     choices: [ attribute, text ]
   input_type:
     description:
@@ -106,6 +115,7 @@
 - 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.
+- More information about this module is available from the community wiki at U(https://github.com/ansible/community/wiki/Module:-xml)
 author:
 - Tim Bielawa (@tbielawa)
 - Magnus Hedemark (@magnus919)
@@ -161,6 +171,26 @@
     path: /foo/bar.xml
     xpath: /business/website/validxhtml/@validatedon
 
+- name: Add or modify an attribute, add element if needed
+  xml:
+    path: /foo/bar.xml
+    xpath: /business/website/validxhtml
+    attribute: validatedon
+    value: 1976-08-05
+
+# How to read an attrribute value and access it in Ansible
+- name: Read attribute value
+  xml:
+    path: /foo/bar.xml
+    xpath: /business/website/validxhtml
+    content: attribute
+    attribute: validatedon
+  register: xmlresp
+
+- name: Show attribute value
+  debug:
+    var: xmlresp.matches[0].validxhtml.validatedon
+
 - name: Remove all children from the website element (option 1)
   xml:
     path: /foo/bar.xml
@@ -174,7 +204,7 @@
     children: []
 
 # In case of namespaces, like in below XML, they have to be explicitely stated
-# Note: there's the prefix "x" in front of the "bar", too
+# NOTE: there's the prefix "x" in front of the "bar", too
 #<?xml version='1.0' encoding='UTF-8'?>
 #<foo xmlns="http://x.test" xmlns:attr="http://z.test">
 #  <bar>


Test patch: