Ansible add lines to file with split variable - ansible

I made a UI that I use to configure our servers. I put the new IP's or servernames into a txt field and have ansbible playbooks ran on them depending on the type of server it is instead of using an inventory file. I'm trying to get ansible to add lines to a file for each value in the {{ips}} variable and not have them all on the same line.
I have tried a few different ways including lineinfile, blockinfile and replace seems to get me closer than the others, but I am still not able to get the results I want.
- name: Add new lines
replace:
path: /foo/bar
regexp: '^# Test Line'
replace: "# Test Line\n/foo/bar {{ ips }}"
This add one line with all the IP's in the ips variable.
# Test Line
/foo/bar test1,test2
What I am trying to get is.
# Test Line
/foo/bar test1
/foo/bar test2
It is hard to account for how many ip's will be in the variable at a time. Sometimes it's one sometimes it's 10.

In the following solution:
I'm using the python split inside a Jinja2 expression to create a list out of your string of comma separated ip strings. This will only work if the format stays the same.
I loop over that list with lineinfile, using the insertafter option
I made sure the solution handles dupes nicely if needed.
This is the demo playbook
---
- name: test for So
hosts: localhost
vars:
test_file: /tmp/test_file.txt
first_ips: 127.0.0.1,127.0.0.2
more_ips: 10.35.26.1,10.35.26.2
dupe_ips: 127.0.0.2,10.35.26.1
tasks:
- name: Make sure we start from scratch
shell: echo "I'm a line of text\n\n# Test Line\n\nThis is the end, my only friend" > {{ test_file }}
- name: Show file at start
debug:
msg: "{{ lookup('file', test_file).split('\n') }}"
- name: Add first ips
lineinfile:
path: "{{ test_file }}"
insertafter: "# Test Line"
line: "/foo/bar {{ item }}"
loop: "{{ first_ips.split(',') }}"
- name: Show file with first ips
debug:
msg: "{{ lookup('file', test_file).split('\n') }}"
- name: Add second list of ips
lineinfile:
path: "{{ test_file }}"
insertafter: "# Test Line"
line: "/foo/bar {{ item }}"
loop: "{{ more_ips.split(',') }}"
- name: Show file with more ips
debug:
msg: "{{ lookup('file', test_file).split('\n') }}"
- name: Test if dupes are handled correctly
lineinfile:
path: "{{ test_file }}"
insertafter: "# Test Line"
line: "/foo/bar {{ item }}"
loop: "{{ dupe_ips.split(',') }}"
- name: Show file that should not have changed
debug:
msg: "{{ lookup('file', test_file).split('\n') }}"
And here is the result. My debug task is showing the result file as a list of lines for display purpose when running the playbook. cat the file yourself if you want to see the result without the extra quotes and commas.
PLAY [test for So] *******************************************************************
TASK [Gathering Facts] ***************************************************************
ok: [localhost]
TASK [Make sure we start from scratch] ***********************************************
changed: [localhost]
TASK [Show file at start] ************************************************************
ok: [localhost] => {
"msg": [
"I'm a line of text",
"",
"# Test Line",
"",
"This is the end, my only friend"
]
}
TASK [Add first ips] *****************************************************************
changed: [localhost] => (item=/foo/bar 127.0.0.1)
changed: [localhost] => (item=/foo/bar 127.0.0.2)
TASK [Show file with first ips] ******************************************************
ok: [localhost] => {
"msg": [
"I'm a line of text",
"",
"# Test Line",
"/foo/bar 127.0.0.2",
"/foo/bar 127.0.0.1",
"",
"This is the end, my only friend"
]
}
TASK [Add second list of ips] ********************************************************
changed: [localhost] => (item=/foo/bar 10.35.26.1)
changed: [localhost] => (item=/foo/bar 10.35.26.2)
TASK [Show file with more ips] *******************************************************
ok: [localhost] => {
"msg": [
"I'm a line of text",
"",
"# Test Line",
"/foo/bar 10.35.26.2",
"/foo/bar 10.35.26.1",
"/foo/bar 127.0.0.2",
"/foo/bar 127.0.0.1",
"",
"This is the end, my only friend"
]
}
TASK [Test if dupes are handled correctly] *******************************************
ok: [localhost] => (item=/foo/bar 127.0.0.2)
ok: [localhost] => (item=/foo/bar 10.35.26.1)
TASK [Show file that should not have changed] ****************************************
ok: [localhost] => {
"msg": [
"I'm a line of text",
"",
"# Test Line",
"/foo/bar 10.35.26.2",
"/foo/bar 10.35.26.1",
"/foo/bar 127.0.0.2",
"/foo/bar 127.0.0.1",
"",
"This is the end, my only friend"
]
}
PLAY RECAP ***************************************************************************
localhost : ok=9 changed=3 unreachable=0 failed=0

Related

Ansible variables

I'm a real Ansible beginner.
Is there any way to reconstruct a variable from another ansible variable?
For example, this playbook :
- hosts: servers
vars:
ex_server1: First
ex_server2: Second
ex_server3: Third
toto: ex_
tasks:
- debug:
msg: "{{ toto+ansible_hostname }}"
It print :
ok: [server2] => {
"msg": "ex_server2"
}
ok: [server3] => {
"msg": "ex_server3"
}
ok: [server1] => {
"msg": "ex_server1"
}
Instead of "First", "Second" and "Third".
Is there a way to print variable content instead of variable name in this situation or in a jinja template ?
Use lookup vars plugin
- debug:
msg: "{{ lookup('vars', toto + ansible_hostname) }}"
gives
TASK [debug] ***********************************************************
ok: [server1] =>
msg: First
ok: [server2] =>
msg: Second
ok: [server3] =>
msg: Third
The details about the plugin are available from the command-line
shell> ansible-doc -t lookup vars

How to run an Ansible task if *any* host has a fact

I'm wrapping my head around something that I'm probably overcomplicating.
I need to check if any of my hosts has ansible_virtualization_type == "openvz"
If this is the case, ALL hosts should execute a specific task.
I'm now trying to set a fact (virt_list) containing a list of hosts with their virtualization_type on localhost:
- name: Set fuse on virtualization OpenVZ
set_fact:
virt_list:
host: "{{item}}"
type: "openvz"
when: hostvars[item].ansible_virtualization_type == "openvz"
with_items: "{{ groups['all'] }}"
delegate_to: localhost
delegate_facts: true
but this doesn't work (both hosts in this play are on openvz):
TASK [roles/testvirt : debug vars ansible_virtualization_type ] ****************************
ok: [host1] => {
"ansible_virtualization_type": "openvz"
}
ok: [host2] => {
"ansible_virtualization_type": "openvz"
}
TASK [roles/testvirt : debug vars virt_list ] **********************************************
ok: [host1] => {
"msg": [
{
"host": "host1",
"type": "openvz"
}
]
}
ok: [host2] => {
"msg": [
{
"host": "host2",
"type": "openvz"
}
]
}
There should be a simpler way, maybe using jinjia2 to combine the lists directly.
Anyone has advices?
Q: "If any of my hosts has ansible_virtualization_type == "openvz" ALL hosts should execute a specific task."
A: For example, given the inventory for testing
shell> cat hosts
host1 ansible_virtualization_type=xen
host2 ansible_virtualization_type=xen
host3 ansible_virtualization_type=openvz
extract the variables
- debug:
msg: "{{ ansible_play_hosts|
map('extract', hostvars, 'ansible_virtualization_type')|
list }}"
run_once: true
gives
msg:
- xen
- xen
- openvz
Test if the type is present
- debug:
msg: OK. ALL hosts should execute a specific task.
when: "'openvz' in vtypes"
vars:
vtypes: "{{ ansible_play_hosts|
map('extract', hostvars, 'ansible_virtualization_type')|
list }}"
run_once: true
gives
msg: OK. ALL hosts should execute a specific task.
If this is working as expected proceed with all hosts
- set_fact:
all_hosts_execute_specific_task: true
when: "'openvz' in vtypes"
vars:
vtypes: "{{ ansible_play_hosts|
map('extract', hostvars, 'ansible_virtualization_type')|
list }}"
run_once: true
- debug:
msg: Execute a specific task.
when: all_hosts_execute_specific_task|default(false)
gives
TASK [set_fact] ************************************************************
ok: [host1]
TASK [debug] ***************************************************************
ok: [host1] =>
msg: Execute a specific task.
ok: [host3] =>
msg: Execute a specific task.
ok: [host2] =>
msg: Execute a specific task.
The task will be skipped if the type is missing.

Ansible loop giving warning found a duplicate dict key (when). Using last defined value only

I am trying to iterate over an array and assign the value to variables hooks_enabled, workflow_artifact_id, workflow_version, one by one in every iteration and perform a specific task (currently debug, later change to Helm install command).
Code:
---
- name: Executing Ansible Playbook
hosts: localhost
become: yes
become_user: someuser
pre_tasks:
- include_vars: global_vars.yaml
- name: Print some debug information
set_fact:
all_vars: |
Content of vars
--------------------------------
{{ vars | to_nice_json }}
tasks:
- name: Iterate over an array
set_fact:
hooks_enabled: '{{ array_item1_hooks_enabled }}'
workflow_artifact_id: '{{ array_item1_workflow_artifact_id }}'
workflow_version: '{{ array_item1_workflow_version }}'
when: "item == 'array_item1'"
set_fact:
hooks_enabled: '{{ array_item2_hooks_enabled }}'
workflow_artifact_id: '{{ array_item2_workflow_artifact_id }}'
workflow_version: '{{ array_item2_workflow_version }}'
when: "item == 'array_item2'"
with_items: "{{ array}}"
# Change debug with helm install command
- debug:
msg: " id= '{{ workflow_artifact_id }}'"
The issue I am facing is, only the last when is considered and others are skipped
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
[WARNING]: While constructing a mapping from /c/ansible-test/second.yaml, line 16, column 7, found a duplicate dict key (set_fact). Using last defined value only.
[WARNING]: While constructing a mapping from /c/ansible-test/second.yaml, line 16, column 7, found a duplicate dict key (when). Using last defined value only.
PLAY [Executing Ansible Playbook] *********************************************************************************************************************************************************************************
TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
ok: [localhost]
TASK [include_vars] ***********************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Print some debug information] *******************************************************************************************************************************************************************************
ok: [localhost]
TASK [Iterate over an array] **************************************************************************************************************************************************************************************
skipping: [localhost] => (item=array_item1)
ok: [localhost] => (item=array_item2)
skipping: [localhost] => (item=array_item3)
skipping: [localhost] => (item=array_item4)
skipping: [localhost] => (item=array_item5)
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": " id= 'algorithm-Workflow'"
}
PLAY RECAP ********************************************************************************************************************************************************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
How do I modify the block to enable all the when statement execute and later use helm install command to take the variables one by one.
I would go with a dynamic variable construction using the vars lookup.
Something along the lines of:
- set_fact:
hooks_enabled: "{{ lookup('vars', item ~ '_hooks_enabled') }}"
workflow_artifact_id: "{{ lookup('vars', item ~ '_workflow_artifact_id') }}"
workflow_version: "{{ lookup('vars', item ~ '_workflow_version') }}"
when: "item in ['array_item1', 'array_item2']"
with_items: "{{ array }}"

selectattr on ansible_facts always returns an empty list

The idea is to add an item into the array if the item does not exist. The select attribute function returns an empty list even though there are items. Therefore the length of an empty is always 0, which in turn will create a new list every time in my case. So the union function in add a new comment section is returning a list with the only new comment erasing all the old ones.
I understand that the issue is with t_array_exists["ansible_facts"]|selectattr("common_motd_qsc_comments_array", "defined")|list|length == 0 conditional statement but I am not sure what I am doing wrong. I have tried many variations of this command. Few of them are commented. Any advice/suggestion is appreciated :)
main.yml:
- name: Get comment array from facts
set_fact:
common_motd_qsc_comments_array: "{{ ansible_local['snps']['motd']['comment_array'] }}"
register: t_array_exists
when:
- ansible_local['snps']['motd'] is defined
- ansible_local['snps']['motd']['comment_array'] is defined
- debug:
var: t_array_exists
- debug:
var: t_array_exists["ansible_facts"]|selectattr("common_motd_qsc_comments_array", "defined")|list
# var: ansible_facts|selectattr("common_motd_qsc_comments_array", "defined")|list
# var: t_array_exists|selectattr("common_motd_qsc_comments_array", "defined")|list
- name: Create an empty array if there is no array
set_fact:
common_motd_qsc_comments_array: []
when:
- t_array_exists["ansible_facts"]|selectattr("common_motd_qsc_comments_array", "defined")|list|length == 0
- debug:
var: t_array_exists["ansible_facts"]|selectattr("common_motd_qsc_comments_array", "defined")|list|length
- name: Deleting a comment if it exists
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | difference([t_new_entry]) }}"
loop: "{{ common_motd_qsc_delete_comment }}"
when: t_new_entry in common_motd_qsc_comments_array
vars:
t_new_entry: "{{ item | trim }}"
- name: Add a new comment if it doesn't exist
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | union([t_new_entry]) }}"
loop: "{{ common_motd_qsc_add_comment }}"
when: t_new_entry not in common_motd_qsc_comments_array
vars:
t_new_entry: "{{ item | trim }}"
- name: Saving comments to snps.fact file
ini_file:
dest: "/etc/ansible/facts.d/snps.fact"
section: 'motd' # header
option: 'comment_array' # key
value: "{{ common_motd_qsc_comments_array }}" # value
Ansible output:
TASK [common/motd_scratch/v1 : Get comment array from facts] ***************************************************************************************
ok: [ansible-poc-cos6]
TASK [common/motd_scratch/v1 : debug] **************************************************************************************************************
ok: [ansible-poc-cos6] => {
"t_array_exists": {
"ansible_facts": {
"common_motd_qsc_comments_array": [
"new comment 1"
]
},
"changed": false,
"failed": false
}
}
TASK [common/motd_scratch/v1 : debug] **************************************************************************************************************
ok: [ansible-poc-cos6] => {
"ansible_facts|selectattr(\"common_motd_qsc_comments_array\", \"defined\")|list": []
}
TASK [common/motd_scratch/v1 : Create an empty array if there is no array] *************************************************************************
ok: [ansible-poc-cos6]
TASK [common/motd_scratch/v1 : debug] **************************************************************************************************************
ok: [ansible-poc-cos6] => {
"t_array_exists[\"ansible_facts\"]|selectattr(\"common_motd_qsc_comments_array\", \"defined\")|list|length": "0"
}
TASK [common/motd_scratch/v1 : Add a new comment if it doesn't exist] ******************************************************************************
ok: [ansible-poc-cos6] => (item=new comment 22)
TASK [common/motd_scratch/v1 : Saving comments to snps.fact file] **********************************************************************************
changed: [ansible-poc-cos6]
TASK [common/motd_scratch/v1 : debug] **************************************************************************************************************
ok: [ansible-poc-cos6] => {
"common_motd_qsc_add_comment": [
"new comment 22"
]
}
snps.fact output:
Before:
[motd]
comment_array = ['new comment 1']
After passing new_comment as "new comment 2":
[motd]
comment_array = ['new comment 2']
Expected:
[motd]
comment_array = ['new comment 1', 'new comment 2']
You can do all of the above in one single easy step. Here is an MCVE playbook to illustrate.
Prior to running the playbook, I added the following file on my machine:
/etc/ansible/facts.d/snps.fact
{
"motd_example1": [
"One message",
"A message",
"Message to delete"
],
"motd_example2": [
"Some messages",
"Mandatory message",
"Other message"
]
}
The playbook
---
- name: Add/Remove list elements demo
hosts: localhost
gather_facts: true
vars:
# The (list of) custom message(s) I want to add if not present
my_custom_mandatory_messages:
- Mandatory message
# The (list of) custom message(s) I want to remove if present
my_custom_messages_to_delete:
- Message to delete
# The list of custom vars I'm going to loop over for demo
# In your case, you can simply use your single motd var
# directly in the below task without looping. I added this
# for convenience as the expression is exactly the same
# in all cases. Only the input data changes
my_motd_example_vars:
- motd_example1
- motd_example2
- motd_example3 # This one does not even exist in ansible_local.snps
tasks:
- name: Show result using local facts for each demo test var
debug:
msg: "{{ ansible_local.snps[item] | default([])
| union(my_custom_mandatory_messages)
| difference(my_custom_messages_to_delete) }}"
loop: "{{ my_motd_example_vars }}"
- name: Proove it works with a totaly undefined var
debug:
msg: "{{ totally.undefined.var | default([])
| union(my_custom_mandatory_messages)
| difference(my_custom_messages_to_delete) }}"
The result
TASK [Gathering Facts] **********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Show result using local facts for each demo test var] *********************************************************************************************************************************************************************************
ok: [localhost] => (item=motd_example1) => {
"msg": [
"One message",
"A message",
"Mandatory message"
]
}
ok: [localhost] => (item=motd_example2) => {
"msg": [
"Some messages",
"Mandatory message",
"Other message"
]
}
ok: [localhost] => (item=motd_example3) => {
"msg": [
"Mandatory message"
]
}
TASK [Simple proof that it works with a totaly undefined var] *******************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
"Mandatory message"
]
}
PLAY RECAP **********************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Not sure I understood exactly what the problem is, but if you are trying to merge two dictionaries (with one overwriting the other if the key already exists), then you might want to take a look at:
https://docs.ansible.com/ansible/latest/user_guide/playbooks_advanced_syntax.html#yaml-anchors-and-aliases-sharing-variable-values
The very first example shows exactly that, with just YAML. If that doesn't do the trick, then you might want to try the combine filter:
https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#combining-hashes-dictionaries
HTH
Alex
I was able to fix it with the help of my colleague. I just had to check if comment_array is not defined instead of trying to find its length. A simple alternate idea which works :)
- name: Create an empty array if there is no array
set_fact:
common_motd_qsc_comments_array: []
when:
- ansible_local['snps']['motd']['comment_array'] is not defined
Thank you all for your help :)

Iterating via nested loops

The packages.yml file defined as:
---
- packages:
- name: Some description 1,
packageList:
- package1,
- package2,
- package3
- name: Some description 2,
package: package4
The first item contains a field packageList, the 2nd item does not have it, but only package field.
Playbook:
---
- hosts: all
become: yes
vars_files:
- packages.yml
How to iterate via all packageList items of the packages list only if this packageList is defined for an item.
Here is how I can iterate through items which contain package field:
- name: iteration
debug:
msg: "name: {{ item.package }}"
when: item.package is defined
with_items: "{{ packages }}"
As noted in my comment, if you simply want to install multiple yum/apt packages, it is usually more efficient to simply pass the list to the apt/yum/package module. As the docs state:
"When used with a loop: each package will be processed individually, it is much more efficient to pass the list directly to the name option."
However, if you really need a loop, here is a possible solution:
playbook.yml:
---
- hosts: all
gather_facts: false
vars_files:
- packages.yml
tasks:
- name: iteration over single items
debug:
msg: "name: {{ item.package }}"
when: item.package is defined
with_items: "{{ packages }}"
- name: iteration over lists
debug:
msg: "name: {{ item.packageList }}"
when: item.packageList is defined
with_items: "{{ packages }}"
- name: Do something with individual packages in the list
include_tasks: process_list.yml
vars:
mylist: "{{outer.packageList}}"
when: outer.packageList is defined
loop: "{{ packages }}"
loop_control:
loop_var: outer
process_list.yml:
- name: See what we have received
debug:
var: item
loop: "{{mylist}}"
result:
PLAY [all] *******************************************************************************************************************************
TASK [iteration over single items] *******************************************************************************************************
skipping: [localhost] => (item={u'packageList': [u'package1,', u'package2,', u'package3'], u'name': u'Some description 1,'})
ok: [localhost] => (item={u'name': u'Some description 2,', u'package': u'package4'}) => {
"msg": "name: package4"
}
TASK [iteration over lists] **************************************************************************************************************
ok: [localhost] => (item={u'packageList': [u'package1,', u'package2,', u'package3'], u'name': u'Some description 1,'}) => {
"msg": "name: [u'package1,', u'package2,', u'package3']"
}
skipping: [localhost] => (item={u'name': u'Some description 2,', u'package': u'package4'})
TASK [Do something with individual packages in the list] *********************************************************************************
skipping: [localhost] => (item={u'name': u'Some description 2,', u'package': u'package4'})
included: /root/tmp/process_list.yml for localhost
TASK [See what we have received] *********************************************************************************************************
ok: [localhost] => (item=package1,) => {
"ansible_loop_var": "item",
"item": "package1,"
}
ok: [localhost] => (item=package2,) => {
"ansible_loop_var": "item",
"item": "package2,"
}
ok: [localhost] => (item=package3) => {
"ansible_loop_var": "item",
"item": "package3"
}
PLAY RECAP *******************************************************************************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The loop_control/loop_var part is used because otherwise both the outer and inner loop will use {{item}} as the loop variable - and this will cause... interesting results :)
You can define a default value with an empty list for the cases, where the packageList is undefined.
{{ item.packageList | default ([]) }}
If the packageList is undefined, the job iterates over an empty list, which means, it does not do anything.
You can use default, as #ceving mentioned:
---
- hosts: localhost
connection: local
vars_files:
- packages.yml
tasks:
- name: iteration
debug:
msg: "name: {{ item.packageList | default([item.package]) }}"
with_items: "{{ packages }}"
If packageList exists, it will use that, else package put into a single-element array to match the form of packageList:
PLAY [localhost] **********************************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [localhost]
TASK [iteration] **********************************************************************************************
ok: [localhost] => (item=None) => {
"msg": "name: [u'package1,', u'package2,', u'package3']"
}
ok: [localhost] => (item=None) => {
"msg": "name: [u'package4']"
}
PLAY RECAP ****************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0

Resources