Post Json to API via Ansible - ansible

I want to make a POST request to an API endpoint via Ansible where some of the items inside the post data are dynamic, here is what I try and fail:
My body_content.json:
{
apiKey: '{{ KEY_FROM_VARS }}',
data1: 'foo',
data2: 'bar'
}
And here is my Ansible task:
# Create an item via API
- uri: url="http://www.myapi.com/create"
method=POST return_content=yes HEADER_Content-Type="application/json"
body="{{ lookup('file','create_body.json') | to_json }}"
Sadly this doesn't work:
failed: [localhost] => {"failed": true}
msg: this module requires key=value arguments
....
FATAL: all hosts have already failed -- aborting
My ansible version is 1.9.1

You can't use newlines like this in yaml. Try this instead (the ">" indicates that the next lines are to be concatenated):
# Create an item via API
- uri: >
url="http://www.myapi.com/create"
method=POST return_content=yes HEADER_Content-Type="application/json"
body="{{ lookup('file','create_body.json') | to_json }}"
But I find this much better:
# Create an item via API
- uri:
url: "http://www.myapi.com/create"
method: POST
return_content: yes
HEADER_Content-Type: "application/json"
body: "{{ lookup('file','create_body.json') | to_json }}"

I'm posting below what I ended up using for my usecase (Ansible 2.0). This is useful if your json payload is stated inline (and not in a file).
this task expects 204 as its success return code.
And since the body_format is json, the header is inferred automatically
- name: add user to virtual host
uri:
url: http://0.0.0.0:15672/api/permissions/{{ rabbit_virtualhost }}/{{ rabbit_username }}
method: PUT
user: "{{ rabbit_username }}"
password: "{{ rabbit_password }}"
return_content: yes
body: {"configure":".*","write":".*","read":".*"}
body_format: json
status_code: 204
it is basically equivalent to:
curl -i -u user:pass -H "content-type:application/json" -XPUT http://0.0.0.0:15672/api/permissions/my_vhost/my_user -d '{"configure":".*","write":".*","read":".*"}'

Related

Ansible AWX API call to register hostname in inventory

I am trying to make a post call to the AWX API in order to register the hostname IP into the inventory.
A normal curl request that works would look like this:
curl -k -s --user "awx-user:awx-pwd" -X POST -H "Content-Type: application/json" "https://awx-api-url/api/v2/inventories/host_group_id/hosts/" --data '{ "name": "172.10.100.10", "description": "test-vm" }'
The playbook part that I am trying to use looks like this:
- name: Register host in AWX Inventory
uri:
url: "https://awx-api-url/api/v2/inventories/{{ host_group_id }}/hosts/"
method: POST
validate_certs: false
user: "{{ api_awx_username }}"
password: "{{ awx_pwd_api }}"
return_content: true
force_basic_auth: true
status_code: [200, 202]
body: '{"name": {{ guest_custom_ip }}, "description": {{ vm_hostname }}; }'
headers:
Content-Type: application/json
body_format: raw
register: reg_awx_inventory
The error I get is:
"content": "{\"detail\":\"JSON parse error - Expecting value: line 1 column 1 (char 0)\\nPossible cause: trailing comma.\"}",
I double checked the message, but it doesn't seem to be an issue with the trailing comma, any ideas? Thx.

Ansible template error while templating string : unexpected char '#' in email

I'm using Ansible to get a list of user emails from an API and I want to loop over them.
This is the json response I get from the API:
"users": [
{
"email": "email1#email.com",
"id": 1,
"is_admin": true
},
{
"email": "email2#email.com",
"id": 2,
"is_admin": false
},
]
edit:
the task after that which I need the email for:
- name: Send emails
register: result
uri:
url: http://api
method: GET
body_format: json
return_content: yes
body:
email: "{{ item.email }}"
scope: SCOPE
loop: "{{ users.json['users'] }}"
- name: Print result
debug:
var: result
the error I get:
fatal: [localhost]: FAILED! => {"msg": "template error while templating string: unexpected char '#' at 16. String: {{ email#email.com }}"}
If I use email: item.email the json request body will be "body": {"email": "item.email"} instead of the email value
How Can I get the full email of each user?
You need to remove the {{ and }} markers from your var: directive. The value of var is implicitly evaluated in a Jinja template context so you don't need those markers:
- name: Print returned json dictionary
debug:
var: item.email
loop: "{{ users.json['users'] }}"
(Updated based on edits to your question)
The example you've shown doesn't generate the error you're asking about. Here's a complete reproducer with just the uri changed:
- hosts: localhost
gather_facts: false
vars:
users:
json:
users:
- email: email1#email.com
id: 1
is_admin: true
- email: email2#email.com
id: 2
is_admin: false
tasks:
- name: Send emails
register: result
uri:
url: https://eny1tdcqj6ghp.x.pipedream.net
method: GET
body_format: json
return_content: true
body:
email: "{{ item.email }}"
scope: SCOPE
loop: "{{ users.json['users'] }}"
This runs without errors (although note that I'm suspicious about the
use of a GET request with a JSON body; typically you would expect
that to be a POST request).
You can see the result of running this playbook here, which shows that the values being sent in the request are exactly what we expect from the data.

Ansible uri POST with body containing as key value JSON

I have a config_file with values in the format: key-value pair
e.g. a: "{\"type\": \"some_type\"}"
Everything is ok. Util I tried to pass this JSON value as value inside uri utility:
- name: 'Push config values'
uri:
url: '{{ uri }}/api/{{ item.key }}'
method: POST
body: '{"alias":"{{ item.key }}","key":"{{ item.key }}","value":"{{ item.value }}"}'
body_format: json
validate_certs: no
status_code: 204
headers:
X-Token: "{{ token }}"
with_dict: '{{ config_overrides_file }}'
register: response
I tried different way of escaping, also filter combination | from_json | to_json. But nothing is working. I got 'failed to parse JSON input: invalid character ''t'' after object key:value pair' in case of escaping
Probably, exist some standard approach to handle it?
I found just one way to achieve what I want: instead of a: "{\"type\": \"some_type\"}" this value in config file it could be a: {"type": "some_type"} and in uri it will be body: {"alias":"{{ item.key }}","key":"{{ item.key }}", "value": {{ item.value | to_json }}}. It's working approach but I would like not create additional task for json and non-json properties for such type of request.
P.S. When I hardcode this value everything is ok
You're getting bitten by ansible's "helpful" behavior of coercing any string it finds that starts with { back into a dict; the usual fix for that is to send it explicitly through |string to indicate to ansible you do not wish for that to happen. For sure any loop-ish behavior on a task (with_dict: being one of them) makes that problem much worse, because ansible gets really, really weird about coercion and recursive jinja2 templates in that circumstance
- name: test data for SO
set_fact:
config_values_file:
a: '{"type": "some_type"}'
- name: 'Push config values'
uri:
url: '{{ uri }}/api/{{ item.key }}'
method: POST
body: >-
{{ {"alias": item.key,
"key": item.key,
"value": item.value|string
} }}
body_format: json
validate_certs: no
status_code: 204
headers:
X-Token: "{{ token }}"
with_dict: '{{ config_overrides_file }}'
register: response
POSTs:
{"alias": "a", "key": "a", "value": "{\"type\": \"some_type\"}"} which I believe is what you were asking for

Ansible URI module passing integer from variable

I set a variable from a previous play that collects a value (integer), then call it in the following task using the uri module to pass it via API. But I always get a 422 error back saying "Value should be integer".
- name: Deploy Staging Blueprint
local_action:
module: uri
url: "https://{{ server_address }}/api/application/{{app_id}}/deploy"
method: PUT
status_code: 202
validate_certs: no
headers:
Content-Type: 'application/json'
Accept: 'application/json'
AUTHTOKEN: "{{ server_session.token }}"
body_format: json
body:
version: "{{ staging.json.version }}"
run_once: true
register: deploy
changed_when: deploy|succeeded
I first tried converting it to integer with "| int" but that didnt work. I then tried "| type_debug" and validated the variable is an integer.
But I still get this error. If I replace the variable with a raw integer it works just fine. Using verbose output it still appears the value is being passed as a string.
"invocation": {
"module_args": {
"attributes": null,
"backup": null,
"body": {
"version": "48"
},
"body_format": "json",
Any idea what Im missing here or how I can work around this? Im currently running Ansible 2.4.0 for this project.
The uri module can take a pre-formatted body in JSON format. Try:
- name: Deploy Staging Blueprint
local_action:
module: uri
url: "https://{{ server_address }}/api/application/{{app_id}}/deploy"
method: PUT
status_code: 202
validate_certs: no
headers:
Content-Type: 'application/json'
Accept: 'application/json'
AUTHTOKEN: "{{ server_session.token }}"
body_format: json
body: '{ "version": {{ staging.json.version }} }'
run_once: true
register: deploy
changed_when: deploy|succeeded
Yes, this is a really strange default behavior of Ansible. It renders every value as string regardless its original type. So you end up with staging.json.version being a string instead of a numeric type.
This behavior can be altered in recent Ansible releases using the jina2_native flag globally.
Example with
test_int: 50
Try using the Jinja variable without quotes in the body parameter i.e.
body: '{ "test": {{ test_int }} }'
It works for me in Ansible 2.9.13

How to Parse the XML response of a URI in ansible

There is a code which call the web services of an application.
- uri:
url: http://10.164.52.61:8080/ems/v74/ws/customer.ws?customerRefId=f4XXXb15d69c3
method: GET
content_as_json: true
password: admin
user: admin
validate_certs: no
return_content: yes
HEADER_Cookie: "{{login.set_cookie}}"
register: customerId
- debug:
var: customerId.content
The sample response is
"customerId.content": "<listResponse type=\"customer\" count=\"1\"><instance customerId=\"28\" name=\"abc\" customerRefId=\"xyz\" refId1=\"12\" type=\"org\" enabled=\"true\" phone=\"\" fax=\"\" billingZip=\"\" billingAddress=\"\" billingCity=\"\" billingCountry=\"\" billingState=\"\" vendor=\"1\" defaultEmail=\"test\" defaultContactName=\"test\"/></listResponse>"
I want to access the list response in my next block of code. Like i need just the value of "customerId". How this can be achieved using anisble
This is similar to sebaszw's answer, however doesn't require the xml response to be written to a file, instead storing it in a variable.
- uri:
url: http://10.164.52.61:8080/ems/v74/ws/customer.ws?customerRefId=f4XXXb15d69c3
method: GET
content_as_json: true
password: admin
user: admin
validate_certs: no
return_content: yes
HEADER_Cookie: "{{login.set_cookie}}"
register: customerId
- xml:
xmlstring: "{{customerId.content}}"
xpath: /listResponse/instance
content: attribute
register: instance_attributes
- debug:
var: instance_attributes.matches.0.instance.customerId
There is no XML support in Ansible out of the box.
You can use regex filters to make some simple searches in your XML data.
In your example:
- debug: msg="{{ customerId.content | regex_findall('customerId=\"(\d+)\"') }}"
Will search for customerId=\"<number>\" strings and return a list of numbers in quotes.
Update: fully working example:
- hosts: localhost
gather_facts: no
vars:
myvar: "<listResponse type=\"customer\" count=\"1\"><instance customerId=\"28\" name=\"abc\" customerRefId=\"xyz\" refId1=\"12\" type=\"org\" enabled=\"true\" phone=\"\" fax=\"\" billingZip=\"\" billingAddress=\"\" billingCity=\"\" billingCountry=\"\" billingState=\"\" vendor=\"1\" defaultEmail=\"test\" defaultContactName=\"test\"/></listResponse>"
tasks:
- debug: msg="{{ myvar | regex_findall('customerId=\"(\d+)\"') }}"
I use ansible 2.1.1.0.
From Ansible 2.4 you can do this:
- get_url:
url: http://10.164.52.61:8080/ems/v74/ws/customer.ws?customerRefId=f4XXXb15d69c3
dest: ./response.xml
url_password: admin
url_user: admin
validate_certs: no
headers:
Cookie: "{{login.set_cookie}}"
- xml:
path: ./response.xml
xpath: /listResponse/instance
content: attribute
register: instance_attributes
- debug:
var: instance_attributes.matches.0.instance.customerId
I just added XML parsing support in the uri module because I needed it too.
https://github.com/ansible/ansible/pull/53045
Just like the JSON support, it will return an 'xml' key with a dictionary consisting of the XML content for the convenience of accessing the data in the payload.
Your example would look like this:
- uri:
url: http://10.164.52.61:8080/ems/v74/ws/customer.ws?customerRefId=f4XXXb15d69c3
method: GET
password: admin
user: admin
validate_certs: no
HEADER_Cookie: "{{login.set_cookie}}"
register: customerId
- debug:
var: customerId.xml.listResponse.instance['#customerId']
The output in customerId.xml would be:
{
'listResponse': {
'#type': 'customer',
'#count', '1',
'instance': {
'#customerId': '28',
'#name': 'abc'
'#customerRefId': 'xyz',
'#refId1': '12',
'#type': 'org',
'#enabled': 'true',
'#phone': '',
'#fax': '',
'#billingZip': '',
'#billingAddress': '',
'#billingCity': '',
'#billingCountry': '',
'#billingState': '',
'#vendor': '1',
'#defaultEmail': 'test',
'#defaultContactName': 'test'
}
}
}
Its worth looking at ansible-xml for this. It's not part of Ansible but its a super useful module for parsing xml, it uses the lxml Python library under the hood.
As this was reasonably complex getting this to work I thought I'd post my Ansible - XML parameters for parsing a SOAP response:
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<S:Body xmlns:ns2="http://ws.coverity.com/v9">
<ns2:getLdapServerDomainsResponse>
<return>
<name>soapui_test</name>
</return>
</ns2:getLdapServerDomainsResponse>
</S:Body>
</S:Envelope>
The trick was getting namespaces and xpath right. Also as I wanted the actual name I needed content: text
- name: Extract the name from the SOAP
xml:
xmlstring: "{{ ldap_response.content }}"
xpath: "/S:Envelope/S:Body/ns2:getLdapServerDomainsResponse/return/name"
namespaces:
S: "http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV: "http://schemas.xmlsoap.org/soap/envelope/"
ns2: "http://ws.coverity.com/v9"
content: text
register: ldap_name
I still don't understand why the xpath section at the end doesn't work with .../ns2:return/ns2:name as the Ansible - XML docs example suggests. The above fills ldap_name with (simplified):
"ldap_name": {
"matches": [
{
"name": "soapui_test"
}
],
}

Resources