Building vars dynamically - ansible

I'm trying to leverage ovirt-ansible (https://github.com/oVirt/ovirt-ansible) but it's vars are a bit verbose and I'd like to generate them off some simpler ones to reduce the chance of human error.
For example, something like this which obviously doesn't work:
vars:
cluster_name: test
domain_suffix: blah
subnet_mgmt: 1.2.3
myhosts:
- id: 001
ip_suffix: 101
hosts:
{% for host in myhosts %}
- name: "{{ cluster_name }}-{{ host[id] }}.{{ domain_suffix }}"
address: "{{ subnet_mgmt }}.{{ host[ip_suffix] }}"
{% endfor %}
Can anyone advise best way to go about doing this, without simply forking their repo and rewriting the playbook to read the variables in my own format? I'm hoping to avoid having to maintain a fork going forwards.

Is this the code that you're looking for?
> cat dynamic_var.yml
- hosts: localhost
vars:
cluster_name: test
domain_suffix: blah
subnet_mgmt: 1.2.3
myhosts:
- { id: "001", ip_suffix: "101" }
tasks:
- set_fact:
hosts:
- name: "{{ cluster_name }}-{{ item.id }}.{{ domain_suffix }}"
address: "{{ subnet_mgmt }}.{{ item.ip_suffix }}"
loop: "{{ myhosts }}"
- debug:
msg: "{{ hosts }}"
> ansible-playbook dynamic_var.ym
(abridged)
TASK [debug]
"address": "1.2.3.101",
"name": "test-001.blah"

Related

Looping over ansible_hosts from inventory

I have a bunch of root servers with different IP addresses. I'm trying to configure the ufw firewall on the server with MySQL server to only allow access from my servers (might change to webservers later) with Ansible. Initially, I only had the FQDNs in the inventory but added the ansible_host IPs because the firewall is not going to resolve the host names (makes sense).
Unfortunately I do not know how to access the ansible_host in the loop query("inventory_hostnames", "all")
My inventory:
all:
vars:
ansible_ssh_user: me
children:
sqlserver:
hosts:
sqlserver.my-domain.de:
ansible_ssh_user: mysql_me_user
ansible_host: 1.1.1.1
webserver:
hosts:
webserver1.my-domain.de:
ansible_host: 2.2.2.2
webserver2.my-domain.de:
ansible_host: 3.3.3.3
now I am trying to loop in the playbook:
- hosts: '{{target|default("sqlserver")}}'
roles:
- { name: oefenweb.ufw, become: yes } # needs root but does not define become by itself...
vars:
ufw_logging: true
ufw_rules:
- rule: allow
to_port: 22
protocol: tcp
tasks:
- name: open MySQL for servers in my
ufw:
rule: allow
to_port: 3306
protocol: tcp
from_ip: '{{ item }}'
loop: '{{ hosts|default(query("inventory_hostnames", "all")) }}'
tags: test
become: true
There are many options. Make your choice depending on the use case.
For example, the play below
shell> cat pb.yml
- hosts: '{{ target|default("sqlserver") }}'
tasks:
- debug:
msg: "{{ item }}: {{ ansible_host }}"
loop: "{{ clients|default(groups.webserver) }}"
vars:
ansible_host: "{{ hostvars[item].ansible_host }}"
gives
shell> ansible-playbook pb.yml
PLAY [sqlserver] *****************************************************************************
TASK [debug] *********************************************************************************
ok: [sqlserver.my-domain.de] => (item=webserver1.my-domain.de) =>
msg: 'webserver1.my-domain.de: 2.2.2.2'
ok: [sqlserver.my-domain.de] => (item=webserver2.my-domain.de) =>
msg: 'webserver2.my-domain.de: 3.3.3.3'
PLAY RECAP ***********************************************************************************
sqlserver.my-domain.de: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Example of the project for testing
shell> tree .
.
├── ansible.cfg
├── hosts
└── pb.yml
0 directories, 3 files
shell> cat ansible.cfg
[defaults]
gathering = explicit
inventory = $PWD/hosts
stdout_callback = yaml
shell> cat hosts
all:
vars:
ansible_ssh_user: me
children:
sqlserver:
hosts:
sqlserver.my-domain.de:
ansible_ssh_user: mysql_me_user
ansible_host: 1.1.1.1
webserver:
hosts:
webserver1.my-domain.de:
ansible_host: 2.2.2.2
webserver2.my-domain.de:
ansible_host: 3.3.3.3
shell> cat pb.yml
- hosts: '{{ target|default("sqlserver") }}'
tasks:
- debug:
msg: "{{ item }}: {{ ansible_host }}"
loop: "{{ clients|default(groups.webserver) }}"
vars:
ansible_host: "{{ hostvars[item].ansible_host }}"
Create the list and the dictionary below
clients: "{{ groups.webserver }}"
ah_list: "{{ clients|map('extract', hostvars, 'ansible_host')|list }}"
ah_dict: "{{ dict(clients|zip(ah_list)) }}"
give
ah_list:
- 2.2.2.2
- 3.3.3.3
ah_dict:
webserver1.my-domain.de: 2.2.2.2
webserver2.my-domain.de: 3.3.3.3
Then, all tasks below give the same results
2a)
- debug:
msg: "{{ item.key }}: {{ item.value }}"
with_dict: "{{ ah_dict }}"
2b)
- debug:
msg: "{{ item.0 }}: {{ item.1 }}"
with_together:
- "{{ clients }}"
- "{{ ah_list }}"
2c)
- debug:
msg: "{{ item }}: {{ ah_dict[item] }}"
loop: "{{ clients }}"
Example of a complete playbook for testing
- hosts: '{{ target|default("sqlserver") }}'
vars:
clients: "{{ groups.webserver }}"
ah_list: "{{ clients|map('extract', hostvars, 'ansible_host')|list }}"
ah_dict: "{{ dict(clients|zip(ah_list)) }}"
tasks:
- debug:
var: ah_list
- debug:
var: ah_dict
- debug:
msg: "{{ item.key }}: {{ item.value }}"
with_dict: "{{ ah_dict }}"
- debug:
msg: "{{ item.0 }}: {{ item.1 }}"
with_together:
- "{{ clients }}"
- "{{ ah_list }}"
- debug:
msg: "{{ item }}: {{ ah_dict[item] }}"
loop: "{{ clients }}"
As I was writting the question I understood that should google for accessing inventory property and found a solution in Accessing inventory host variable in Ansible playbook - use
from_ip: '{{ hostvars[item].ansible_host }}'
for my task.
I hope that this the way to go.

Ansible search sublists for value

A webhook triggers an AWX job and I want to run the deployment on a certain host depending on the service, since they run on different servers. I need to know which server uses that service to set is as a var so it can be used as a host in the following play.
My variable inside vars.yaml looks like this:
staging_hosts:
server1: ['service1', 'service2', 'service3']
server2: ['service4', 'service5', 'service6']
server3: ['service7', 'service8', 'service9']
Playbook:
- name: write deployment hosts
hosts: localhost
vars:
deployment_hosts: absent
vars_files:
- ./group_vars/vars.yaml
tasks:
- set_fact:
modified_repos: (small regex filter to find modified repository)
- set_fact:
deployment_hosts: "{{ item }}"
when: '{{ modified_repos }} in {{ item }}'
with_list:
- "{{ staging_hosts }}"
- name: connect to Cluster
hosts: "{{ hostvars['localhost']['deployment_hosts'] }}"
What can I do against this warning and error?
[WARNING]: conditional statements should not include jinja2 templating
delimiters such as {{ }} or {% %}. Found: {{ modified_repos }} in {{ item }}
fatal: [localhost]: FAILED! => {"msg": "The conditional check '{{ modified_repos }} in {{ item }}' failed. True {% else %} False {% endif %}): unhashable type: 'list'
Oh I forgot to mention. It is important, that deployment_hosts could also contain two hosts if modified repos include for example service1 and service4.
Q: "deployment_hosts could also contain two hosts if modified repos include for example service1 and service4."
A: Use intersect filter. For example, the playbook
- hosts: localhost
vars:
staging_hosts:
server1: ['service1', 'service2', 'service3']
server2: ['service4', 'service5', 'service6']
server3: ['service7', 'service8', 'service9']
modified_repos: ['service1', 'service4']
tasks:
- set_fact:
deployment_hosts: "{{ deployment_hosts|default([]) + [item.key] }}"
loop: "{{ staging_hosts|dict2items }}"
when: modified_repos|intersect(item.value)|length > 0
- debug:
var: deployment_hosts
gives
deployment_hosts:
- server1
- server2

Conditional to add_host module

I have an inventory that i need to add a conditional to.
My code:
- name: Create memory inventory
add_host:
name: "{{ item.0.key }}"
group: target_hosts
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"
But I want something like:
- name: Create memory inventory
add_host:
name: "{{ item.0.key }}"
{% if item.0.value.OS_Choice[:3] == 'win' %}
group:
- target_hosts
- win
{% else %}
group:
- target_hosts
- linux
{% endif %}
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"
With this configuration, Ansible errors:
The offending line appears to be:
{% if item.0.value.OS_Choice[:3] == 'win' %}
^ here
Any ideas on how I can implement this conditional?
You're mixing up Jinja2 with YAML. Here you go:
- name: Create memory inventory when win
add_host:
name: "{{ item.0.key }}"
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"
when: item.0.value.OS_Choice[:3] == 'win'
vars:
group:
- target_hosts
- win
- name: Create memory inventory when not win
add_host:
name: "{{ item.0.key }}"
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"
when: item.0.value.OS_Choice[:3] != 'win'
vars:
group:
- target_hosts
- linux
However, Ansible gather facts about the OS already. Perhaps you want to use those, instead of configuring something like this yourself.
Building up on Kevin's answer (and fixing some wrongly placed parameters)
You should definitely do this differently, like creating dynamic groups based on detected OS in facts. See:
the group_by module
the ansible_distribution* facts that you can explore as an example with
ansible localhost -m setup -a filter="ansible_distribution*"
Meanwhile, with your current logic, you can still do this in a single task:
- name: Create memory inventory
vars:
additional_group: >-
{{ (item.0.value.OS_Choice[:3] == 'win') | ternary('win', 'linux') }}
add_host:
name: "{{ item.0.key }}"
groups:
- target_hosts
- "{{ additional_group }}"
with_nested:
- "{{ lookup ('dict', hosts, wantlist=True) }}"

How to extract the output from stdout.lines in ansible

---
- name: Mikrotik info
hosts: mikrotik
connection: network_cli
remote_user: root
gather_facts: false
tasks:
- name: show info
routeros_command:
commands: /system routerboard print
register: rb_info
- name: Debug info
debug:
msg: "{{ rb_info.stdout_lines }}"
Output:
routerboard: yes
model: 751G-2HnD
serial-number: 3A6502B2A2E7
firmware-type: ar7240
factory-firmware: 3.0
current-firmware: 6.42.3
upgrade-firmware: 6.43.4
I need to filter it for "upgrade-firmware" string and get output like this:
upgrade-firmware: 6.43.4
I should use regex_replace? Or I can use grep or something like that?
Any thoughts are greatly appreciated.
Thank you
(update)
Use from_yaml and combine a dictionary. For example
- set_fact:
minfo: "{{ minfo|default({})|combine(item|from_yaml) }}"
loop: "{{ rb_info.stdout_lines }}"
- debug:
var: minfo['upgrade-firmware']
give
minfo['upgrade-firmware']: 6.43.4
(for the record)
Robust solution is to write the data to template and include_vars. The tasks below
- tempfile:
register: tempfile
- template:
src: minfo.j2
dest: "{{ tempfile.path }}"
- include_vars:
file: "{{ tempfile.path }}"
name: minfo
- debug:
var: minfo
with the template
shell> cat minfo.j2
{% for item in rb_info.stdout_lines %}
{{ item }}
{% endfor %}
should give
"minfo": {
"current-firmware": "6.42.3",
"factory-firmware": 3.0,
"firmware-type": "ar7240",
"model": "751G-2HnD",
"routerboard": true,
"serial-number": "3A6502B2A2E7",
"upgrade-firmware": "6.43.4"
}
The tasks below creates variable upgrade_firmware
- set_fact:
upgrade_firmware: "{{ item.split(':').1|trim }}"
loop: "{{ rb_info.stdout_lines|map('quote')|map('trim')|list }}"
when: item is search('^upgrade-firmware')
- debug:
var: upgrade_firmware
It is possible to put all the parameters into the dictionary
- set_fact:
minfo: "{{ minfo|default({})|
combine({item.split(':').0: item.split(':').1|trim}) }}"
loop: "{{ rb_info.stdout_lines|map('quote')|map('trim')|list }}"
- debug:
var: minfo['upgrade-firmware']

Adding field to dict items

Consider the following play. What I am trying to do is add a field, tmp_path which is basically the key and revision appended together to each element in the scripts dict.
---
- hosts: localhost
connection: local
gather_facts: no
vars:
scripts:
a.pl:
revision: 123
b.pl:
revision: 456
tasks:
- with_dict: "{{ scripts }}"
debug:
msg: "{{ item.key }}_{{ item.value.revision }}"
# - with_items: "{{ scripts }}"
# set_fact: {{item.value.tmp_path}}="{{item.key}}_{{item.value.revision}}"
# - with_items: "{{ scripts }}"
# debug:
# msg: "{{ item.value.tmp_path }}"
...
Obviously the commented code doesn't work, any idea how I can get this working? Is it possible to alter the scripts dict directly, or should I somehow be creating a new dict to reference instead?
By the way welcome to correct the terminology for what I am trying to do.
OK, I think I got a solution (below), at least to let me move forwards with this. Disadvantages are it has removed the structure of my dict and also seems a bit redundant having to redefine all the fields and use a new variable, If anyone can provide a better solution I will accept that instead.
---
- hosts: localhost
connection: local
gather_facts: no
vars:
scripts:
a.pl:
revision: 123
b.pl:
revision: 456
tasks:
- with_dict: "{{ scripts }}"
debug:
msg: "{{ item.key }}_{{ item.value.revision }}"
- with_dict: "{{ scripts }}"
set_fact:
new_scripts: "{{ (new_scripts | default([])) + [ {'name': item.key, 'revision': item.value.revision, 'tmp_path': item.key ~ '_' ~ item.value.revision}] }}"
# - debug:
# var: x
# - with_dict: "{{ scripts }}"
- with_items: "{{ new_scripts }}"
debug:
msg: "{{ item.tmp_path }}"
...
BTW credit to the following question which pointed me in the right direction:
Using Ansible set_fact to create a dictionary from register results

Resources