Search code examples
xmltomcatxpathansibletomcat9

How to modify single XML element with ansible instead of appending a new one


I want to modify $CATALINA_HOME/conf/tomcat-users.xml in order to set up admin user and password. I have this playbook 'working':

- name: Set password from encrypted 'tomcat_admin_pass' for Tomcat User Administrator (admin) using ansible.comunity.xml
  community.general.xml:
    path: "{{ tomcat_live_dir }}/conf/tomcat-users.xml"
    xpath: "/tomcat:tomcat-users/user[@username='admin']"
    # tomcat-users is namespaced and needs more info for XPath
    namespaces:
      tomcat: http://tomcat.apache.org/xml
    attribute: password
    value: "{{ tomcat_admin_pass }}"
    state: present
    notify: tomcat_reload

- name: Set roles for Tomcat User Administrator (admin) using ansible.comunity.xml
  community.general.xml:
    path: "{{ tomcat_live_dir }}/conf/tomcat-users.xml"
    xpath: "/tomcat:tomcat-users/user[@username='admin']"
    # tomcat-users is namespaced and needs more info for XPath
    namespaces:
      tomcat: http://tomcat.apache.org/xml
    attribute: roles
    value: "manager-gui,admin-gui"
    state: present
    notify: tomcat_reload

But I end up appending two elements like this:

<user username="admin" password="my_super_pass"/><user username="admin" roles="manager-gui,admin-gui"/>

I am not very experienced in XPath and ansible.community.xml but my query seems to be accurate. I expected that ansible would modify only matching elements as per its idempotence behavior.

I did not want to put a Jinja2 template for tomcat-users.xml in case it is manually modified after, and another execution of my role would overwrite it.


Solution

  • I think your problem here is inconsistent use of XML namespaces. In your expressions, you have written:

    /tomcat:tomcat-users/user[@username='admin']
    

    But if tomcat-users is in the tomcat namespace, then so is user, so you need to write:

    /tomcat:tomcat-users/tomcat:user[@username='admin']
    

    If I use this playbook:

    - hosts: localhost
      gather_facts: false
      vars:
        tomcat_admin_pass: secret
      tasks:
        - name: Set password from encrypted 'tomcat_admin_pass' for Tomcat User Administrator (admin) using ansible.comunity.xml
          community.general.xml:
            path: "tomcat-users.xml"
            xpath: "/tomcat:tomcat-users/tomcat:user[@username='admin']"
            # tomcat-users is namespaced and needs more info for XPath
            namespaces:
              tomcat: http://tomcat.apache.org/xml
            attribute: password
            value: "{{ tomcat_admin_pass }}"
            state: present
    
        - name: Set roles for Tomcat User Administrator (admin) using ansible.comunity.xml
          community.general.xml:
            path: "tomcat-users.xml"
            xpath: "/tomcat:tomcat-users/tomcat:user[@username='admin']"
            # tomcat-users is namespaced and needs more info for XPath
            namespaces:
              tomcat: http://tomcat.apache.org/xml
            attribute: roles
            value: "manager-gui,admin-gui"
            state: present
    

    With this tomcat-users.xml:

    <?xml version='1.0' encoding='UTF-8'?>
    <tomcat-users xmlns="http://tomcat.apache.org/xml">
        <role rolename="tomcat"/>
        <role rolename="role1"/>
        <user username="tomcat" password="tomcat" roles="tomcat"/>
        <user username="both" password="tomcat" roles="tomcat,role1"/>
        <user username="role1" password="tomcat" roles="role1"/>
      </tomcat-users>
    

    The result is:

    <?xml version='1.0' encoding='UTF-8'?>
    <tomcat-users xmlns="http://tomcat.apache.org/xml">
        <role rolename="tomcat"/>
        <role rolename="role1"/>
        <user username="tomcat" password="tomcat" roles="tomcat"/>
        <user username="both" password="tomcat" roles="tomcat,role1"/>
        <user username="role1" password="tomcat" roles="role1"/>
      <user username="admin" password="secret" roles="manager-gui,admin-gui"/></tomcat-users>
    

    If I run the playbook a second time, it does not make any additional changes.


    To expand on the problem a bit:

    With your original xpath expression, your first task creates a new <user> element in the file...but because the file has a default namespace, you actually end up with a <tomcat:user> element. This means that your second task fails to find the element created by the first task, because it's looking for <user> rather than <tomcat:user>.