I'm building a set of Ansible tasks that work with renewing HTTPS certificates through Namecheap using their API. Their API returns XML responses, so I've been using the Ansible community.general.xml module for parsing out what I need from each response. One of the tasks is to pull out an concatenate each intermediate certificate to build the certificate chain file, but the property names returned by the Ansible module include special characters to indicate the XML namespace.
Here's an example response from the Namecheap API endpoint. I'm trying to get the three <Certificate>
nodes, each inside their own <Certificate Type="INTERMEDIATE">
node.
<?xml version="1.0" encoding="UTF-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors/>
<Warnings/>
<RequestedCommand>namecheap.ssl.getInfo</RequestedCommand>
<CommandResponse Type="namecheap.ssl.getInfo">
<SSLGetInfoResult Status="active" StatusDescription="Certificate is Active." Type="EssentialSSL Wildcard" IssuedOn="9/8/2022" Years="1" Expires="9/22/2023" ActivationExpireDate="" OrderId="1234567890" SANSCount="0">
<CertificateDetails>
<CSR>
<![CDATA[-----BEGIN CERTIFICATE REQUEST----- ... -----END CERTIFICATE REQUEST-----]]>
</CSR>
<ApproverEmail>CNAMECSRHASH</ApproverEmail>
<CommonName>*.example.com</CommonName>
<AdministratorEmail>example@example.com</AdministratorEmail>
<Certificates CertificateReturned="true" ReturnType="INDIVIDUAL">
<Certificate>
<![CDATA[-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----]]>
</Certificate>
<CaCertificates>
<Certificate Type="INTERMEDIATE">
<Certificate>
<![CDATA[-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----]]>
</Certificate>
</Certificate>
<Certificate Type="INTERMEDIATE">
<Certificate>
<![CDATA[-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----]]>
</Certificate>
</Certificate>
<Certificate Type="INTERMEDIATE">
<Certificate>
<![CDATA[-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----]]>
</Certificate>
</Certificate>
</CaCertificates>
</Certificates>
</CertificateDetails>
<Provider>
<OrderID>1234567890</OrderID>
<Name>COMODO</Name>
</Provider>
</SSLGetInfoResult>
</CommandResponse>
<Server>e4fee023b712</Server>
<GMTTimeDifference>--5:00</GMTTimeDifference>
<ExecutionTime>0.117</ExecutionTime>
</ApiResponse>
This is my "baseline" for the current set of Ansible tasks I'm working on.
- name: parse info response for cert chain
community.general.xml:
xmlstring: "{{ namecheap_info.content }}"
namespaces:
x: 'http://api.namecheap.com/xml.response'
xpath: '/x:ApiResponse/x:CommandResponse/x:SSLGetInfoResult/x:CertificateDetails/x:Certificates/x:CaCertificates/x:Certificate/x:Certificate'
content: text
register: parsed_cert_chain
- ansible.builtin.set_fact:
namecheap_cert_chain_content: "{{ parsed_cert_chain.matches | map(attribute='{http://api.namecheap.com/xml.response}Certificate') }}"
When I run these tasks, I get an error from the ansible.builtin.set_fact module: The task includes an option with an undefined variable. The error was: 'dict object' has no attribute '{http://api'.
. I understand that this is because the period in the URL is being treated as a property accessor, but I have yet to successfully figure out how to escape it.
Here are some things I've tried (note that some of these are just shots in the dark) and their respective error messages:
'dict object' has no attribute '{http://api\\\\'.
)'dict object' has no attribute '{http://api\\\\\\\\'.
)'dict object' has no attribute '\\\\{http://api'.
)dict object has no element ['{http://api.namecheap.com/xml.response}Certificate'].
)'dict object' has no attribute '[{http://api'.
)I suppose I could use a replace filter to remove the XML namespace from the string before passing it into the xml module, but that feels a little bandaid-like. What would be the correct way to escape the special characters for the map(attribute)
Jinja2 filter?
If you use json_query
instead of map
, you can do this:
- name: parse info response for cert chain
community.general.xml:
xmlstring: "{{ namecheap_info.content }}"
namespaces:
x: 'http://api.namecheap.com/xml.response'
xpath: '/x:ApiResponse/x:CommandResponse/x:SSLGetInfoResult/x:CertificateDetails/x:Certificates/x:CaCertificates/x:Certificate/x:Certificate'
content: text
register: parsed_cert_chain
- ansible.builtin.set_fact:
namecheap_cert_chain_content: >-
{{
parsed_cert_chain.matches | json_query('[*]."{http://api.namecheap.com/xml.response}Certificate"')
}}
- debug:
var: namecheap_cert_chain_content
Which produces:
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"namecheap_cert_chain_content": [
"\n -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----\n ",
"\n -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----\n ",
"\n -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----\n "
]
}