Ansible playbook loop from site yaml or template? - ansible

I'm trying to use my Ansible playbook to call upon a site YAML reference to create a filename that increment for multiple switches. What am I doing wrong? I believe the playbook is pulling from the host YAML?
Format: <switch>-<site>-<floor><stackid>.txt
e.g.: with two switches:
swi-lon-101.txt
swi-lon-202.txt
host_vars/host.yaml
project_name: test
device_name: swi
site_abbrev: lon
device_type: switch
switch_stacks:
- id: 01
installation_floor: 1
- id: 02
installation_floor: 2
templates/switch-template.j2
{% for stack in switch_stacks %}
set system host-name {{ device_name }}-{{ site_abbrev }}-{{ stack.installation_floor }}{{ stack.id }}
{% endfor %}
The playbook, in which the problem lies, how do I get the hostname to create correctly for each of the 2 switches?
My playbook:
- name: Create Folder Structure
hosts: junos
gather_facts: false
tasks:
- name: Create Site Specific Folder
file:
path: /home/usr/complete_config/{{ project_name }}
state: directory
mode: 0755
- name: Set Destination Directory & Filename for Switch Configurations
set_fact:
dest_dir: /home/usr/complete_config/{{ project_name }}
full_device_name: "{{ device_name|lower }}-{{ site_abbrev|lower }}-{{ switch_stacks.installation_floor }}{{ switch_stacks.id }}.txt"
when: device_type == 'switch'
Ansible error, running:
ansible-playbook playbooks/site-playbook.yaml
TASK [Set Destination Directory & Filename for Switch Configurations] **************************************************
fatal: [site-switch]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'list object' has no attribute 'installation_floor'\n\nThe error appears to be in '/home/usr/playbooks/switch-playbook.yaml': line 19, 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: Set Destination Directory & Filename for Switch Configurations\n ^ here\n"}

So, you do need a loop in order to set this fact, otherwise, you are trying to access a installation_floor on a list, which cannot be.
You will also face an issue with the id of your items in switch_stacks, as 01 is an int and will end up displayed as 1, simply. So you either need to declare those as string, or to pad them with a format filter.
So, you end up with this task:
- set_fact:
full_device_name: >-
{{
full_device_name
| default([])
+ [
device_name | lower ~ '-' ~
site_abbrev | lower ~ '-' ~
item.installation_floor ~
"%02d" | format(item.id) ~ '.txt'
]
}}
loop: "{{ switch_stacks }}"
when: device_type == 'switch'
Which will create a list:
full_device_name:
- swi-lon-101.txt
- swi-lon-202.txt
Given the playbook:
- hosts: localhost
gather_facts: false
tasks:
- set_fact:
full_device_name: >-
{{
full_device_name
| default([])
+ [
device_name | lower ~ '-' ~
site_abbrev | lower ~ '-' ~
item.installation_floor ~
"%02d" | format(item.id) ~ '.txt'
]
}}
loop: "{{ switch_stacks }}"
when: device_type == 'switch'
vars:
device_name: swi
site_abbrev: lon
device_type: switch
switch_stacks:
- id: 01
installation_floor: 1
- id: 02
installation_floor: 2
- debug:
var: full_device_name
This yields:
TASK [set_fact] ************************************************************
ok: [localhost] => (item={'id': 1, 'installation_floor': 1})
ok: [localhost] => (item={'id': 2, 'installation_floor': 2})
TASK [debug] ***************************************************************
ok: [localhost] =>
full_device_name:
- swi-lon-101.txt
- swi-lon-202.txt

Let's keep the attribute id as a string
switch_stacks:
- id: '01'
installation_floor: 1
- id: '02'
installation_floor: 2
The task
- set_fact:
full_device_name: "{{ [prefix]|product(sw_fl_id)|map('join')|
product([postfix])|map('join')|list }}"
vars:
prefix: "{{ device_name }}-{{ site_abbrev }}-"
postfix: ".txt"
sw_id: "{{ switch_stacks|map(attribute='id')|list }}"
sw_fl: "{{ switch_stacks|map(attribute='installation_floor')|list }}"
sw_fl_id: "{{ sw_fl|zip(sw_id)|map('join')|list }}"
gives you the list
full_device_name:
- swi-lon-101.txt
- swi-lon-202.txt

#β.εηοιτ.βε
Thanks for your reply, it worked.
I have however slightly tweeked it as you will see below.
Once again thanks for showing the way
name: Set Destination Directory & Filename for Switch Configurations
set_fact:
dest_dir: /home/usr/complete_config/{{ project_name }}
full_device_name: >-
{{
device_name | lower ~ '-' ~
site_abbrev | lower ~ '-' ~
item.installation_floor ~
"%02d" | format(item.id)
}}
loop: "{{ switch_stacks }}"
when: device_type == 'switch'

Related

Create var based on list in dict

Imagine this dict on 4 different hosts.
# on host 1
my_dict:
ip: 10.0.0.111
roles:
- name: something
observer: false
# on host 2
my_dict:
ip: 10.0.0.112
roles:
- name: something
observer: false
# on host 3
my_dict:
ip: 10.0.0.113
roles:
- name: something
observer: true
# on host 4
my_dict:
ip: 10.0.0.114
roles:
- name: whatever
When Ansible runs for all 4 hosts I want it to build a variable for each host having the roles name 'something'. The desired output is:
10.0.0.111 10.0.0.112 10.0.0.113:observer
There are 2 requirements:
when my_dict.roles.name == 'something' it must add the ip to the var
but when my_dict.roles.observer , it must add the ip + ':observer'
I eventually want to use the var in a Jinja template, so to me, the var can be either set via an Ansible task or as a jinja template.
This doesn't work:
- name: set fact for ip
debug:
msg: >-
{{ ansible_play_hosts |
map('extract', hostvars, ['my_dict', 'ip'] ) |
join(' ') }}
when: ???
You could create two lists:
one with what should be postfixed to the IPs with the condition based on the observer property
the other one with the IPs
And then zip them back together.
Given:
- debug:
msg: >-
{{
_ips | zip(_is_observer) | map('join') | join(' ')
}}
vars:
_hosts: >-
{{
hostvars
| dict2items
| selectattr('key', 'in', ansible_play_hosts)
| selectattr('value.my_dict.roles.0.name', '==', 'something')
}}
_is_observer: >-
{{
_hosts
| map(attribute='value.my_dict.roles.0.observer')
| map('replace', false, '')
| map('replace', true, ':observer')
}}
_ips: >-
{{
_hosts
| map(attribute='value.my_dict.ip')
}}
This yields:
TASK [debug] *************************************************************
ok: [localhost] =>
msg: 10.0.0.111 10.0.0.112 10.0.0.113:observer

Get Odd or Even Index Value from Variable in Ansible Playbook

I need to get odd or even index value from a variable list:
For example:
- hosts: myhost
vars:
- var1: ["test1","test2","test3","test4","test5"]
- odd_var: []
- even_var: []
I need odd_var to be ["test1","test3","test5"] and even_var to be ["test2","test4"] and also concatenate each variable string of odd_var and even_var to be one string like:
odd_string: "test1,test3,test5"
even_string: "test2,test4"
What should i do to achieve this?
I have tried :
- name: test
set_fact:
odd_list: "{{ odd_list | default([]) + [item] }}"
loop: "{{ var1 }}"
when: "{{ lookup('ansible.utils.index_of', var1, 'eq', item) is even }}
it works but i wonder if i can get more eficient way to do this
Since you said "index value", I'm going to take you at your word and base it on position in the list, not the numbers contained in the strings.
- hosts: localhost
vars:
var1:
- test1
- test2
- test3
- test4
- test5
odd_var: "{{ var1[::2] | join(',') }}"
even_var: "{{ var1[1::2] | join(',') }}"
tasks:
- debug:
msg: "{{ odd_var }} / {{ even_var }}"
Output:
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "test1,test3,test5 / test2,test4"
}
Using regex, checking the last character.
- var1: ["test1","test2","test3","test4","test5"]
- even: "{{ var1 | select('search','.*[02468]$') | join(',') }}"
- odd: "{{ var1 | select('search','.*[13579]$') | join(',') }}"

Ansible merge dictionaries using with_items and vars stores only last item

Trying to create a dictionary per item and merge them
---
- name: TestingLab
hosts: localhost
gather_facts: False
tasks:
- name: Hello Vars
set_fact:
two_nums:
- 1
- 2
- name: create empty dict
set_fact:
ids: {}
- name: Merge all
vars:
single_entry: "{ '{{ item }}': {{ item }} }"
set_fact:
ids: "{{ ids | combine(single_entry) }}"
with_items: "{{ two_nums }}"
- name: Print Result
debug:
msg: "{{ ids }}"
I thought I followed the right guidelines but I seem to be getting only the last item afterwards:
ok: [localhost] => {
"msg": {
"2": 2
} }
I tried replacing the single_entry with the expression in vars but it does not run.
Is there a different syntax to get this done?
EDIT: version info
ansible-playbook 2.5.1
python version = 2.7.17 [GCC 7.5.0]
Try the filters dict and zip. The zip is available since 2.3, e.g.
- set_fact:
d2: "{{ dict(two_nums|zip(two_nums)) }}"
- debug:
var: d2
- debug:
var: d2|type_debug
gives
d2:
1: 1
2: 2
d2|type_debug: dict
If this does not work try Jinja and the filter from_yaml, e.g.
- hosts: localhost
vars:
two_nums:
- 1
- 2
l1: |-
{% for i in two_nums %}
{{ i }}: {{ i }}
{% endfor %}
tasks:
- set_fact:
d1: "{{ l1|from_yaml }}"
- debug:
var: d1
- debug:
var: d1|type_debug
gives the same result
d1:
1: 1
2: 2
d1|type_debug: dict
If you need the keys to be strings quote it, e.g.
l1: |-
{% for i in two_nums %}
"{{ i }}": {{ i }}
{% endfor %}
gives
d1:
'1': 1
'2': 2
In the first case, map the list's items to string, e.g.
- set_fact:
d2: "{{ dict(two_nums|map('string')|zip(two_nums)) }}"
gives the same result
d2:
'1': 1
'2': 2
I can't reproduce the behavior you're describing. Running your
playbook verbatim, I get as output:
TASK [Print Result] **************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"1": 1,
"2": 2
}
}
I'm using Ansible core 2.11.2, but I've also tested your playbook with Ansible 2.9.20 and I get the same output.
I would probably drop the set_fact task, and also change how you're
setting single_entry:
- name: TestingLab
hosts: localhost
gather_facts: False
tasks:
- name: Hello Vars
set_fact:
two_nums:
- 1
- 2
- name: Merge all
vars:
ids: {}
single_entry: "{{ {item: item} }}"
set_fact:
ids: "{{ ids | combine(single_entry) }}"
with_items: "{{ two_nums }}"
- name: Print Result
debug:
msg: "{{ ids }}"
In this version, the template expression is returning a dictionary,
and only requires a single set of Jinja template markers. I'm curious
if this version behaves any differently for you.

How to generate a list with a range and save it in a var of a playbook?

How can I create a list like this in an Ansible playbook:
['user1', 'user2', 'user3', '...', 'user20']
and save it to a var in that same playbook?
I tried Using set_facts and with_items together in Ansible and modified the answer of #serge a little bit.
My code looks like the following:
- name: preparation for list
set_fact:
userItem: userItem= "{{ item }}"
with_sequence: start=1 end=20 format=user%d
register: userResult
- name: make a list
set_fact: users= "{{ userResult.results | map(attribute= 'ansible_facts.userItem) | list }}"
- name resulted list
debug: var=users
But I get the following error:
TASK [make a list] *****************************************************************
fatal: [10.0.2.11]: FAILED! => {"msg": "template error while templating string: unexpected end of template, expected ',' .. String: \"{{ userResult.results | map(attribute"}
I want to use that list in a task where I change a line in a configuration file like this:
- name: allow users to ssh into jail
lineinfile:
path: /etc/ssh/shd_config
regexp: '^AllowUsers '
line: 'AllowUsers ansible {{ users }}'
notify: "restart ssh"
where {{ users }} should be replaced with the desired list.
Is there a way I can achieve getting the desired list saved in some kind of variable (not a file) so I can use it in other tasks?
You can create a list in a set_fact right away, there is no need for any intermediary task:
- set_fact:
users: "{{ users | default([]) + ['user%s' | format(item)] }}"
loop: "{{ range(1, 21) | list }}"
Which gives
{
"users": [
"user1",
"user2",
"user3",
"user4",
"user5",
"user6",
"user7",
"user8",
"user9",
"user10",
"user11",
"user12",
"user13",
"user14",
"user15",
"user16",
"user17",
"user18",
"user19",
"user20"
]
}
This is actually really close to the example found in the documentation: https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#with-sequence
This said you won't be able to fit a list into your lineinfile module, because you do need a string.
So you can either create a string right away:
- set_fact:
users: "{{ users | default('') + ' user%s' | format(item) }}"
loop: "{{ range(1, 21) | list }}"
- lineinfile:
path: /etc/ssh/shd_config
regexp: '^AllowUsers '
line: 'AllowUsers ansible {{ users }}'
notify: "restart ssh"
So, instead of creating a list we are creating a string:
{
"users": " user1 user2 user3 user4 user5 user6 user7 user8 user9 user10 user11 user12 user13 user14 user15 user16 user17 user18 user19 user20"
}
Or use the join filter when using the users list:
- set_fact:
users: "{{ users | default([]) + ['user%s' | format(item)] }}"
loop: "{{ range(1, 21) | list }}"
- lineinfile:
path: /etc/ssh/shd_config
regexp: '^AllowUsers '
line: 'AllowUsers ansible {{ users | join(' ') }}'
notify: "restart ssh"
For example
- set_fact:
users: "{{ ['user']|product(range(start,end))|map('join')|list }}"
vars:
start: 1
end: 5
gives
users:
- user1
- user2
- user3
- user4
The next option gives the same result
- set_fact:
users: "{{ query('sequence', user_range) }}"
vars:
start: 1
end: 4
user_range: "start={{ start }} end={{ end }} format=user%d"

Ansible: tasks to append number to name tag

I'm trying to append a number to the end of the instance name tag, i have the following code but there's a problem with the second task which i cannot find, and i've not been able to find an example of anyone else trying to solve this problem.
I'm also relatively new to Ansible and cannot find the relevant documentation to do certain things like how to lookup a value in a list like how i'm doing with my until: which is probably invalid syntax
Ansible is 2.9 and runs on the instance itself
The Tasks I have are setup to do the following
Get a list of running EC2 instances that belong to the same launch template
Loop the same amount of times as the instance list until the desired name based on item index is not found in the name tags of the ec2 list
Set the updated tag name
Current Code:
---
- name: "{{ role_name }} | Name Instance: Gather facts about current LT instances"
ec2_instance_info:
tags:
"aws:autoscaling:groupName": "{{ tag_asg }}"
"aws:ec2launchtemplate:id": "{{ tag_lt }}"
Application: "{{ tag_application }}"
filters:
instance-state-name: [ "running" ]
no_log: false
failed_when: false
register: ec2_list
- name: "{{ role_name }} | Name Instance: Generate Name"
with_sequence: count="{{ ec2_list.instances|length }}"
until: not "{{ tag_default_name }} {{ '%02d' % (item + 1) }}" in ec2_list.instances[*].tags['Name']
when: tag_name == tag_default_name
no_log: false
failed_when: false
register: item
- name: "{{ role_name }} | Name Instance: Append Name Tag"
ec2_tag:
region: eu-west-1
resource: "{{ instance_id }}"
state: present
tags:
Name: "{{ tag_default_name }} {{ '%02d' % (item + 1) }}"
when: tag_name == tag_default_name
no_log: false
failed_when: false
As requested here's the error I am getting:
ERROR! no module/action detected in task.
The error appears to be in '/app/deploy/Ansible/roles/Boilerplate/tasks/name_instance.yml': line 14, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: "{{ role_name }} | Name Instance: Generate Name"
^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:
with_items:
- {{ foo }}
Should be written as:
with_items:
- "{{ foo }}"
The error is not with the name: as i constantly get that error when there's some other error within the task body
You don't appear to have a module listed in the second task. You might be able to use debug as the module, or use set_fact and skip the register line.
I think it might also be possible to combine the last two tasks with some more advanced looping, but I'd have to play around with it to give you a working example.
Thanks to bnabbs answer i realised the problem with that version was the lack of module, after fixing that i then finished creating and testing it and now have a fully working set of tasks which has resulted in the following code.
---
- name: "{{ role_name }} | Name Instance: Gather facts about current LT instances"
ec2_instance_info:
filters:
"tag:aws:autoscaling:groupName": "{{ tag_asg }}"
"tag:aws:ec2launchtemplate:id": "{{ tag_lt }}"
"tag:Application": "{{ tag_application }}"
instance-state-name: [ "pending", "running" ]
register: ec2_list
- name: "{{ role_name }} | Name Instance: Set Needed Naming Facts"
set_fact:
tag_name_seperator: " "
start_index: "{{ (ec2_list.instances | sort(attribute='instance_id') | sort(attribute='launch_time') | map(attribute='instance_id') | list).index(instance_id) }}"
name_tag_list: "{{ (ec2_list.instances | map(attribute='tags') | list) | map(attribute='Name') | list }}"
# Generate Name from starting index of array and mod to the length of the amount of instances to help prevent conflicts when multiple instances are launched at the same time
- name: "{{ role_name }} | Name Instance: Generate Name"
set_fact:
desired_tag_name: "{{ tag_default_name }}{{ tag_name_seperator }}{{ '%02d' % (((item|int) + (start_index|int) % (name_tag_list|length)) + 1) }}"
loop: "{{ name_tag_list }}"
until: not "{{ tag_default_name }}{{ tag_name_seperator }}{{ '%02d' % (((item|int) + (start_index|int) % (name_tag_list|length)) + 1) }}" in name_tag_list
- name: "{{ role_name }} | Name Instance: Append Name Tag"
ec2_tag:
region: eu-west-1
resource: "{{ instance_id }}"
state: present
tags:
Name: "{{ desired_tag_name }}"

Resources