I have a problem with a loop and failed_with in ansible. I don't know why.
The actual failed_when is much more complex, I know i could use status_code in this case.
It is my goal to parse the json response from a local api for multiple (dynamic list I got early from the api) objects and fail if it is not a response I expect.
- name: "get status for items"
uri:
url: https://{{inventory_hostname}}/status
method: POST
body_format: json
body: "{'index': {{item.index}}"
register: this
failed_when: "this.status != 200"
loop: "{{ item_list.json | json_query('[?state== `UNDEFINED`]') }}"
I get the error:
fatal: [localhost]: FAILED! =>
msg: |-
The task includes an option with an undefined variable. The error was: 'this' is undefined
The error appears to be in '/home/blablup/Devel/ansible/roles/myrole/tasks/update.yml': line 48, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: "get status for items"
^ here
But if I read the documentation it should work this way. Also if I look at this: Using `failed_when` on a `with_items` task depending on return codes example, failed_when should be evaluated for every iteration.
I am unable to reproduce the behavior you've described. If I wrap your task in sufficient code to run it, like this:
- hosts: localhost
gather_facts: false
vars:
item_list:
json:
- state: UNDEFINED
index: 0
- state: okay
index: 1
- state: UNDEFINED
index: 2
tasks:
- name: "get status for items"
uri:
url: http://localhost:5000/status
method: POST
body_format: json
body: "{'index': {{item.index}}"
register: this
failed_when: "this.status != 200"
loop: "{{ item_list.json | json_query('[?state== `UNDEFINED`]') }}"
And provide a server that will return either a 200 or a 400 response:
from flask import Flask, request, make_response
app = Flask(__name__)
#app.route("/status", methods=["POST"])
def status():
if b"2" in request.data:
return make_response("failed", 400)
else:
return make_response("okay", 200)
Then running your playbook produces the expected behavior (it succeeds when the server delivers a 200 response, and fails when the server returns a non-200 response):
TASK [get status for items] *****************************************************************************
ok: [localhost] => (item={'name': 'foo', 'state': 'UNDEFINED', 'index': 0})
failed: [localhost] (item={'name': 'qux', 'state': 'UNDEFINED', 'index': 2}) => {"ansible_loop_var": "item", "changed": false, "connection": "close", "content_length": "6", "content_type": "text/html; charset=utf-8", "date": "Wed, 12 Oct 2022 00:10:05 GMT", "elapsed": 0, "failed_when_result": true, "item": {"index": 2, "name": "qux", "state": "UNDEFINED"}, "msg": "Status code was 400 and not [200]: HTTP Error 400: BAD REQUEST", "redirected": false, "server": "Werkzeug/2.2.2 Python/3.10.7", "status": 400, "url": "http://localhost:5000/status"}
I'm using Ansible core version 2.12.6.
Unrelated to your question, but this looks suspicious:
body: "{'index': {{item.index}}"
You're not delivering a JSON request body (JSON does not use single quotes and your brackets aren't balanced). You want something like this:
body: {
"index": "{{ item.index }}"
}
Related
The API is supposed to return a 10 letter string and using curl command on the API URL returns the correct string. The Ansible uri module is returning a status message.
- name: Get variable output from api
uri:
url: "http://apiurl"
register: my_variable
- name: Print my_variable
debug:
msg: "Value of the variable is {{ my_variable }}"
The message is:
"msg": "Value of the variable {'redirected': False, 'url': 'http://apiurl', 'status': 200, 'server': 'nginx/1.10.3', 'date': 'Thu, 27 Oct 2022 17:47:49 GMT', 'content_type': 'text/plain; charset=utf-8', 'content_length': '23', 'connection': 'close', 'cookies_string': '', 'cookies': {}, 'msg': 'OK (23 bytes)', 'elapsed': 0, 'changed': False, 'failed': False}"
What's going on here?
You are registering the result of the task which means you can then access all attributes of the task. if you want the response content then you need to set the return_content attribute as detailed in the docs
Whether or not to return the body of the response as a “content” key in the dictionary result no matter it succeeded or failed.
Independently of this option, if the reported Content-type is “application/json”, then the JSON is always loaded into a key called json in the dictionary results.
Choices:
no ← (default)
yes
The docs also provide examples on how to use this https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html#examples
- name: Check that a page returns a status 200 and fail if the word AWESOME is not in the page contents
ansible.builtin.uri:
url: http://www.example.com
return_content: yes
register: this
you could then access this.content to get the body of the response. Alternativly you could just use the get_url module and access the body attribute
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.
I know how to process jinja2 templates files and let them create files. I also know how to POST to webservices using the url module.
For now I use some code like this, which successfully posts hardcoded JSON to my remote service:
tasks:
- name: GSA app definition
uri:
url: "http://localhost:8764/api/apps?relatedObjects=false"
method: POST
force_basic_auth: yes
user: "{{ admin_name }}"
password: "{{ admin_pass }}"
body_format: json
body: "{\"name\":\"My new app\", \"description\":\"A really great new app\" }"
follow_redirects: all
status_code: 200
timeout: 15
register: app_gsa_cfg
But the JSON is static, how can I process a jinja2 template and POST its content ? I would prefer not having to create temporary files on disk and POST them, what I am looking for is a direct connection or perhaps an approach that puts the template processing result into a string.
For starters a jinja2 template could look like this, later I will add variables too:
{#
This file creates the basic GSA app in Fusion. See https://doc.lucidworks.com/fusion-server/4.2/reference-guides/api/apps-api.html#create-a-new-app for details
#}
{
"name": "GSA",
"description": "Contains all configuration specific to the migrated GSA legacy searches"
}
(I know that this has little advantage over a static json included into the playbook. But is is easier to edit and offers me the opportunity to have (jinja style) comments in Json, which is normally not possible)
In my case, what I do is the following:
I have an API, so I do the following:
- name: Change API Status
uri:
url: "{{ enpoint }}/v1/requests/{{ whatever }}"
method: PATCH
user: "{{ tokenid }}"
password: x
headers:
X-4me-Account: "myaccount"
body: '{ "status":"{{ reqstatus }}" }'
body_format: json
status_code:
- 201
- 200
force_basic_auth: true
validate_certs: false
return_content: true
Then your reqstatus var will change.
Even you can add your whole text as yaml, import into a variable and convert with filters {{ some_variable | to_json }}
Note: Have a look to the formatting without escaping quotes. That will help.
It makes no sense creating a file with jinja2 if you are not going to copy it remotely. Ansible supports jinja natively but its strength is the possibility to have plugins for better maintainability. There is no difference between template (or win_template) modules unless (as said) you copy the file somewhere. Look this example:
---
- name: Adhoc Jinja
hosts: localhost
connection: local
gather_facts: false
vars:
mytemplate:
- name: "GSA"
description: "Contains all configuration specific to the migrated GSA legacy searches"
- name: "Another Name"
description: "Contains Another Var"
tasks:
- name: Read Vars Loop
debug:
msg: "{{ item | to_json }}"
with_items: "{{ mytemplate }}"
- name: Include Vars
include_vars: adhocjinja2.yml
- name: Read Vars Loop
debug:
msg: "{{ item | to_json }}"
with_items: "{{ mytemplate }}"
And adhocjinja2.yml:
mytemplate:
- name: "GSA2"
description: "Contains all configuration specific to the migrated GSA legacy searches"
- name: "Another Name 2"
description: "Contains Another Var"
The output is:
TASK [Read Vars Loop] **************************************************************************************
ok: [localhost] => (item={'name': 'GSA', 'description': 'Contains all configuration specific to the migrated GSA legacy searches'}) => {
"msg": "{\"name\": \"GSA\", \"description\": \"Contains all configuration specific to the migrated GSA legacy searches\"}"
}
ok: [localhost] => (item={'name': 'Another Name', 'description': 'Contains Another Var'}) => {
"msg": "{\"name\": \"Another Name\", \"description\": \"Contains Another Var\"}"
}
TASK [Include Vars] ****************************************************************************************
ok: [localhost]
TASK [Read Vars Loop] **************************************************************************************
ok: [localhost] => (item={'name': 'GSA2', 'description': 'Contains all configuration specific to the migrated GSA legacy searches'}) => {
"msg": "{\"name\": \"GSA2\", \"description\": \"Contains all configuration specific to the migrated GSA legacy searches\"}"
}
ok: [localhost] => (item={'name': 'Another Name 2', 'description': 'Contains Another Var'}) => {
"msg": "{\"name\": \"Another Name 2\", \"description\": \"Contains Another Var\"}"
}
You can manage your variables as you want and create your json on the fly as Ansible has jinja and json it its heart.
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
Here is the Ansible source code where with_items is linked to a dictionary type variable named "grafana_datasource_body". I always get an error message "ith_items expects a list or a set". Here is the source code and related output.
Ansible source code :
- name: Configure datasource creation http body
set_fact:
grafana_datasource_body:
name: "{{Ass_grafana_Datasource_Name}}"
type: "{{Ass_grafana_Datasource_Type}}"
isDefault: "{{Ass_grafana_Datasource_IsDefault}}"
access: "{{Ass_grafana_Datasource_Access}}"
basicAuth: "{{Ass_grafana_Datasource_BasicAuth}}"
url: "{{InfluxDB_Server}}:{{Ass_grafana_Datasource_Http_Port}}"
database: "{{Ass_grafana_Datasource_Db}}"
username: "{{Ass_grafana_Datasource_DbUser}}"
password: "{{Ass_grafana_Datasource_DbPassword}}"
when: Ass_grafana_Datasource_Name not in grafana_datasources_output.json|map(attribute='name')|list
- debug: msg="Datasource creation http body = {{grafana_datasource_body}}"
when: Ass_grafana_Datasource_Name not in grafana_datasources_output.json|map(attribute='name')|list
# Create non existing datasources
- name: datasource > Create datasource
register: grafana_datasources_create_output
failed_when: "'Datasource added' not in grafana_datasources_create_output|to_json"
uri:
url: "http://127.0.0.1:{{Ass_grafana_Listen_Http_Port}}/api/datasources"
method: POST
HEADER_Content-Type: application/json
body: '{{ item|to_json }}'
force_basic_auth: true
user: "{{Ass_grafana_Api_User}}"
password: "{{Ass_grafana_Api_Password}}"
status_code: 200
with_items: "{{grafana_datasource_body}}"
when: item.name not in grafana_datasources_output.json|map(attribute='name')|list
Output
TASK: [./grafana/grafana_Role | Configure datasource creation http body] ******
<10.0.20.7> ESTABLISH CONNECTION FOR USER: centos
ok: [10.0.20.7] => {"ansible_facts": {"grafana_datasource_body": {"access": "proxy", "basicAuth": "false", "database": "webcom-int", "isDefault": "true", "name": "MonTonton", "password": "webcom", "type": "influxdb", "url": "http://localhost:8086", "username": "webcom"}}}
TASK: [./grafana/grafana_Role | debug msg="Datasource creation http body = {{grafana_datasource_body}}"] ***
<10.0.20.7> ESTABLISH CONNECTION FOR USER: centos
ok: [10.0.20.7] => {
"msg": "Datasource creation http body = {'username': u'webcom', 'name': u'MonTonton', 'database': u'webcom-int', 'url': u'http://localhost:8086', 'basicAuth': u'false', 'access': u'proxy', 'password': u'webcom', 'type': u'influxdb', 'isDefault': u'true'}"
}
TASK: [./grafana/grafana_Role | datasource > Create datasource] ***************
fatal: [10.0.20.7] => with_items expects a list or a set
FATAL: all hosts have already failed -- aborting
Any real reason to use with_items loop here?
Replace
with_items: "{{grafana_datasource_body}}"
with
with_items: "{{ [grafana_datasource_body] }}"
this will work.
But you can use body: '{{ grafana_datasource_body|to_json }}' and don't use with_items.