Ansible - Compare dictionaries works but will not accept an addition - ansible

I am trying to compare two dictionaries and create a 3rd dictionary if an item has changed or is not present. I have this playbook
---
- hosts: localhost
connection: local
gather_facts: no
vars:
host_list1:
host1: 1.1.1.1
host2: 2.2.2.2
host3: 3.3.3.3
host_list2:
host1: 1.1.1.1
host2: 2.2.2.2
host3: 3.3.3.3
tasks:
- name: Define Dict
set_fact:
diff_list: {}
- name: Compare lists
set_fact:
diff_list: "{{ diff_list|combine({item: host_list1[item]}) }}"
loop: "{{ host_list1.keys()|list }}"
when: host_list1[item] != host_list2[item]
- name: Display Results
debug:
msg: "{{ diff_list }}"
If I change the IP in host_list1 it has the desired result and will update the diff_list.
PLAY [localhost] **************************************************************************************************************************************************************
TASK [Define Dict] ************************************************************************************************************************************************************
ok: [localhost]
TASK [Compare lists] **********************************************************************************************************************************************************
ok: [localhost] => (item=host3)
ok: [localhost] => (item=host2)
skipping: [localhost] => (item=host1)
TASK [Display Results] ********************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"host2": "2.2.2.5",
"host3": "3.3.3.4"
}
}
PLAY RECAP ********************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
However if I try and add another host into the host_list1 dictionary I get the following error
PLAY [localhost] **************************************************************************************************************************************************************
TASK [Define Dict] ************************************************************************************************************************************************************
ok: [localhost]
TASK [Compare lists] **********************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The conditional check 'host_list1[item] != host_list2[item]' failed. The error was: error while evaluating conditional (host_list1[item] != host_list2[item]): 'dict object' has no attribute u'host4'\n\nThe error appears to be in '/home/playbooks/test.yml': line 20, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - name: Compare lists\n ^ here\n"}
PLAY RECAP ********************************************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
I have tried updating the when statement
when: host_list1[item] != host_list2[item] or not defined
However this does not work and I am unable to get the new host appended to the diff_list dictionary

The problem is your conditional statement:
when: host_list1[item] != host_list2[item]
If item is key that is present in only host_list1, then host_list2[item] is an error. There are (at least!) two ways of fixing this.
Use a default value
You can use the default filter to provide a default value when the result of an item lookup is undefined:
when: host_list1[item] != host_list2[item]|default(none)
Explicitly check for membership
You can check that the key exists in host_list2 before attempting to get its value:
when: item not in host_list2 or host_list1[item] != host_list2[item]

You can get your result using a single jinja2 expression without using any tasks.
The following playbook:
---
- hosts: localhost
connection: local
gather_facts: no
vars:
hosts_current:
host1: 1.1.1.1
host2: 2.2.2.3
host3: 3.3.3.4
host4: 5.5.5.5
hosts_origin:
host1: 1.1.1.1
host2: 2.2.2.2
host3: 3.3.3.3
diff_dict: >-
{{
hosts_origin | dict2items
| symmetric_difference(hosts_current | dict2items)
| items2dict
}}
tasks:
- name: Show the calculated var
debug:
var: diff_dict
gives:
PLAY [localhost] **************************************************************************************************************************************************************************************************************
TASK [Show the calculated var] ************************************************************************************************************************************************************************************************
ok: [localhost] => {
"diff_dict": {
"host2": "2.2.2.3",
"host3": "3.3.3.4",
"host4": "5.5.5.5"
}
}
PLAY RECAP ********************************************************************************************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Related

get the value from a dict using filter

I have a dictionary, not a dict list, let's say I read a dict from JSON file something.json has content {"a": "b"}
I would like to extract the value using a filter, so I could process that with more filters
# Expecting 'lookup('file', 'something.json') | from_json | ???' gives the value 'b'
- debug:
msg: "{{ lookup('file', 'something.json') | from_json | ??? | urlencode() }}"
I searched that all other examples are for dict list, but I only have a dict here
Given the file below
shell> cat something.json
{"a": "b"}
Q: "Get the value from a dict using a filter."
A: I read your question: "Get the value from a hash without knowing the key." There are many options:
Include the variable from the file to a dictionary, e.g. d1, and display the first value
- hosts: localhost
tasks:
- include_vars:
file: something.json
name: d1
- debug:
msg: "{{ d1.values()|first }}"
gives
shell> ansible-playbook pb.yml
PLAY [localhost] *****************************************************************************
TASK [include_vars] **************************************************************************
ok: [localhost]
TASK [debug] *********************************************************************************
ok: [localhost] =>
msg: b
PLAY RECAP ***********************************************************************************
localhost: ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can put the declarations into the vars
- hosts: localhost
vars:
d1: "{{ lookup('file', 'something.json') }}"
v1: "{{ d1.values()|first }}"
tasks:
- debug:
var: v1
- debug:
var: d1|type_debug
- debug:
var: d1
gives
PLAY [localhost] *****************************************************************************
TASK [debug] *********************************************************************************
ok: [localhost] =>
v1: b
TASK [debug] *********************************************************************************
ok: [localhost] =>
d1|type_debug: dict
TASK [debug] *********************************************************************************
ok: [localhost] =>
d1:
a: b
PLAY RECAP ***********************************************************************************
localhost: ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Use json_query
- hosts: localhost
vars:
d1: "{{ lookup('file', 'something.json') }}"
v1: "{{ d1|json_query('values(#)')|first }}"
tasks:
- debug:
var: v1
gives
PLAY [localhost] *****************************************************************************
TASK [debug] *********************************************************************************
ok: [localhost] =>
v1: b
PLAY RECAP ***********************************************************************************
localhost: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
All the above options assume the file is available at the controller. If the file is at the remote host:
Use slurp
- hosts: localhost
vars:
d1: "{{ out.content|b64decode }}"
v1: "{{ d1.values()|first }}"
tasks:
- slurp:
src: "{{ playbook_dir }}/something.json"
register: out
- debug:
var: v1
gives
PLAY [localhost] *****************************************************************************
TASK [slurp] *********************************************************************************
ok: [localhost]
TASK [debug] *********************************************************************************
ok: [localhost] =>
v1: b
PLAY RECAP ***********************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can also simply register the output of cat
- hosts: localhost
vars:
d1: "{{ out.stdout }}"
v1: "{{ d1.values()|first }}"
tasks:
- command:
cmd: "cat something.json"
chdir: "{{ playbook_dir }}"
register: out
- debug:
var: v1
gives
PLAY [localhost] *****************************************************************************
TASK [command] *******************************************************************************
changed: [localhost]
TASK [debug] *********************************************************************************
ok: [localhost] =>
v1: b
PLAY RECAP ***********************************************************************************
localhost: ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Thanks, Zeitounator, I sorted out using
- debug:
msg: "{{ lookup('file', 'something.json') | from_json | dict2items | map(attribute='value') | urlencode() }}"

Fetch hosts without domain from inventory file

I have following inventory file
$ cat hosts
[web]
server1.example.com
server2.example.com
I would like to fetch the hostname, without the part of domain (.example.com).
I tried with the following playbook, however, it is still fetching with the entire hostname..
$ playbook.yaml
- hosts: localhost
tasks:
- debug:
msg: "{{ groups['web'] }}"
Output
PLAY [localhost] *************************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************
ok: [localhost]
TASK [debug] *****************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
"server1.example.com"
"server2.example.com"
]
}
PLAY RECAP *******************************************************************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Expected output
PLAY [localhost] *************************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************
ok: [localhost]
TASK [debug] *****************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
"server1"
"server2"
]
}
PLAY RECAP *******************************************************************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can get what you want from a magic variable called inventory_hostname_short which basically returns anything before the first . found in the inventory_hostname.
To get this in a normal play host loop, it's as easy as:
- hosts: all
tasks:
- name: show target short name
debug:
var: inventory_hostname_short
If you need to get that for hosts not in the host loop, you will have to go through hostvars. Here is an example to get all those names in a list for a given group running from localhost:
- hosts: localhost
gather_facts: false
tasks:
- name: show list of shortnames for group 'toto'
debug:
msg: "{{ groups['toto'] | map('extract', hostvars, 'inventory_hostname_short') }}"
An other example to get that name only for the first server in group 'toto'
- hosts: localhost
gather_facts: false
tasks:
- name: show shortnames for first server in group 'toto'
vars:
server_name: "{{ groups['toto'][0] }}"
debug:
msg: "{{ hostvars[server_name].inventory_hostname_short }}"

Ansible hosts to be set to a substring of a passed variable

I have a play like this:
- name: Perform an action on a Runtime
hosts: all
roles:
- role: mule_action_on_Runtime
A variable at invocation (--extra-vars 'mule_runtime=MuleS01-3.7.3-Testing') has a prefix of the host needed (MuleS01). I want to set hosts: MuleS01. How do I do this?
Given that your pattern is always PartIWant-PartIDonCareAbout-AnotherPartAfterOtherDash you could use the split method of Python, then get the first item of the list via the Jinja filter first.
Here is full working playbook as example:
- hosts: local
gather_facts: no
tasks:
- debug:
msg: "{{ mule_runtime.split('-') | first }}"
This yield the recap:
play.yml --extra-vars 'mule_runtime=MuleS01-3.7.3-Testing'
PLAY [local] *******************************************************************
TASK [debug] *******************************************************************
ok: [local] => {
"msg": "MuleS01"
}
PLAY RECAP *********************************************************************
local : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
With the inventory
shell> cat hosts
MuleS01
MuleS02
MuleS03
this playbook
shell> cat pb.yml
- hosts: all
tasks:
- debug:
msg: Set {{ mule_runtime }}
when: mule_runtime.split('-').0 == inventory_hostname
gives
skipping: [MuleS02]
ok: [MuleS01] => {
"msg": "Set MuleS01-3.7.3-Testing"
}
skipping: [MuleS03]

Ansible PLaybook: Escape '$' in Linux path

I have a path which looks like this -
base_dir/123/path/to/G\$/subdirectory/html/
When I try to set this path in Ansible playbook, it throws error. If add \$ to escape '\', it throws unexpected failure error.
Playbkook -
- hosts: localhost
vars:
account_id: 123
tasks:
- name: Add \ to path
debug:
var: "base_dir/{{ account_id }}/path/to/G\\$/subdirectory/html/"
Result -
TASK [Gathering Facts] *************************************************************************************************************************************************
task path: /playbooks/example_path.yml:2
ok: [localhost]
META: ran handlers
TASK [Add \ to path] ***************************************************************************************************************************************************
task path: /playbooks/exmaple_path.yml:6
fatal: [localhost]: FAILED! => {
"msg": "Unexpected failure during module execution."
}
PLAY RECAP *************************************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=1
As explained in the debug module documentation, the var option is expecting a variable name, not a scalar for output. You are getting an error because \ is not expected in a variable name. Running the playbook with -vvv will give you a little more explanations.
In this case you need to use the msg option.
- hosts: localhost
gather_facts: false
vars:
account_id: 123
tasks:
- name: Add \ to path
debug:
msg: "base_dir/{{ account_id }}/path/to/G\\$/subdirectory/html/"
Result
PLAY [localhost] ***************************************************************
TASK [Add \ to path] ***********************************************************
ok: [localhost] => {
"msg": "base_dir/123/path/to/G\\$/subdirectory/html/"
}
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The next option is to use the Single-Quoted Style. See the example below
- hosts: localhost
vars:
my_dir1: "/scratch/tmp/G1\\$"
my_dir2: '/scratch/tmp/G2\$'
tasks:
- file:
state: directory
path: "{{ item }}"
loop:
- "{{ my_dir1 }}"
- "{{ my_dir2 }}"
# ls -1 /scratch/tmp/
'G1\$'
'G2\$'

How to ignore the connection error in play

I want to continue my playbook to do some other tasks when some hosts are unreachable. However, the ignore_errors seems doesn't work. The debug msg is not printed.
ansible version is 2.5.4. Is there a way to do this in this version?
- name: check accessibility
hosts: myhosts
tasks:
- ping:
ignore_errors: yes
- fail:
msg: "Host {{ansible_hostname}} is not accessible"
when: False
An option would be to ping each 'inventory_hostname' in the block and end the play if the ping fails.
- hosts: myhosts
gather_facts: no
tasks:
- block:
- delegate_to: localhost
command: ping -c1 "{{ inventory_hostname }}"
rescue:
- fail:
msg: "{{ inventory_hostname }} not accessible. End of play."
- debug:
msg: "Host {{ inventory_hostname }} continue play."
- setup:
Notes:
Set 'gather_facts: no', because we are not sure all hosts are available
Use 'inventory_hostname', because of 'gather_facts: no'
use 'setup' module after the 'block' if necessary
Running the playbook with available hosts: test_01, test_02, test_03 and unavailable host test_99 gives (abridged):
TASK [fail]
fatal: [test_99]: FAILED! => {"changed": false, "msg": "test_99 not accessible. End of play."}
TASK [debug]
ok: [test_03] => {
"msg": "Host test_03 continue play."
}
ok: [test_01] => {
"msg": "Host test_01 continue play."
}
ok: [test_02] => {
"msg": "Host test_02 continue play."
}
TASK [setup]
ok: [test_03]
ok: [test_01]
ok: [test_02]
PLAY RECAP
test_01 : ok=3 changed=1 unreachable=0 failed=0
test_02 : ok=3 changed=1 unreachable=0 failed=0
test_03 : ok=3 changed=1 unreachable=0 failed=0
test_99 : ok=0 changed=0 unreachable=0 failed=2

Resources