Ansible and nested variables in loops - loops

I there a way in Ansible to access variables in variables in a loop? For instance, configuring debconf to install a MySQL/MariaDB server needs two instructions like so:
- name: Define maria root password
shell: echo mysql-server mysql-server/root_password password {{ mysqlRootPass }} | debconf-set-selections
- name: Define maria root password again
shell: echo mysql-server mysql-server/root_password_again password {{ mysqlRootPass }} | debconf-set-selections
But that would be way more compact if I could do that:
- name: Define maria root password
shell: {{ item }}
with_items:
- "{ echo mysql-server mysql-server/root_password password {{ mysqlRootPass }} | debconf-set-selections }"
- "{ echo mysql-server mysql-server/root_password_again password {{ mysqlRootPass }} | debconf-set-selections }"
Obviously, that doesn't work.
so is there a way to make it work? Is there a better way of doing it? Am I missing something?

The {{ }} notation in Ansible comes from Jinja2 templates, it essentially directs Ansible to replace this with the contents of that variable.
with_items introduces the special variable item, which is the particular item of the current loop.
Therefore, you instruct the command module to execute { echo..., namely whatever it is you have in with_items. You only need the command itself: echo....
Reference: Ansible docs: using variables and Jinja2

Here is my complete working MySQL role, that might help you.
vars/main.yml
mysql_root_pass: mypassword #MySQL Root Password
tasks/main.yml
---
- name: Install the MySQL packages
apt:
name: "{{ item }}"
state: installed
update_cache: yes
with_items:
- mysql-server-5.6
- mysql-client-5.6
- python-mysqldb
- libmysqlclient-dev
- name: Update MySQL root password for all root accounts
mysql_user:
name: root
host: "{{ item }}"
password: "{{ mysql_root_pass }}"
state: present
with_items:
- "{{ ansible_hostname }}"
- 127.0.0.1
- ::1
- localhost
- name: Copy the root credentials as .my.cnf file
template:
src: root.cnf.j2
dest: "~/.my.cnf"
mode: 0600
- name: Ensure Anonymous user(s) are not in the database
mysql_user:
name=''
host: "{{ item }}"
state: absent
with_items:
- localhost
- "{{ ansible_hostname }}"
- name: Remove the test database
mysql_db:
name: "test"
state: absent
notify:
- Restart MySQL
templates/root.cnf.j2
[client]
user=root
password={{ mysql_root_pass }}
handlers/main.yml
---
- name: Restart MySQL
service:
name: mysql
state: restarted
My site.yml look like this:
---
- hosts: all
become: yes
gather_facts: yes
roles:
- mysql
If you are just looking to update the password of already configured MySQL:
In vars/main.yml
mysql_old_root_pass: olmysqldpassword
mysql_root_pass: newmysqlpassword
In tasks/main.yml
- name: Set root user password
mysql_user:
name: root
host: "{{ item }}"
password: "{{ mysql_root_pass }}"
check_implicit_admin=yes
login_user: root
login_password: "{{ mysql_old_root_password }}"
state: present
with_items:
- "{{ ansible_hostname }}"
- 127.0.0.1
- ::1
- localhost
Hope this will help you. If you need any help, please check this github link.

Related

Ansible: loop with using collection and role

I´m doing the first steps in Ansible this week and I break on include_tasks for looping ofer a role.
The needed task is to create Letsencrypt certificates for a bunch of domains, thanks to T-Systems-MMS, there is already a collection to do this via APIs of letsencrypt and AutoDNS (see https://github.com/T-Systems-MMS/ansible-collection-acme/blob/master/docs/dns-challenge/autodns.md).
Filling this playbook with my settings, it is working fine for one domain. My try to loop over is (hopefully there was no mistake while anonymising the code):
playbook_getsslcert_main.yml:
---
- hosts: localhost
connection: local
vars:
ansible_python_interpreter: auto
tasks:
- name: Get SSL certificate
include_tasks: playbook_getsslcert_task.yml
loop:
- sub1.domain1.com
- sub2.domain1.com
playbook_getsslcert_task.yml:
---
- name: Doing letsencrypt ACME with AutoDNS
collections:
- t_systems_mms.acme
roles:
- acme
vars:
nbb_emailadress: my.email#example.com
nbb_autodnsuser: login.user#other.com
acme_domain:
certificate_name: "{{ item }}"
zone: "domain1.com"
email_address: "{{ nbb_emailadress }}"
subject_alt_name:
- "{{ item }}"
acme_challenge_provider: autodns
acme_use_live_directory: true
acme_conf_dir: /etc/letsencrypt
acme_account_email: "{{ nbb_emailadress }}"
acme_dns_user: "{{ nbb_autodnsuser }}"
acme_dns_password: "supersecret"
The error I get is
fatal: [localhost]: FAILED! => {"reason": "conflicting action statements: hosts, roles\n\nThe error appears to be in 'playbook_getsslcert_task.yml': line 2, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n---\n- name: Doing letsencrypt ACME with AutoDNS\n ^ here\n"}
My collegues and me are experienced Linux guys, we tested a lot; also we checked the YAML with formatcheckers and so on, did different styles for looping, tried an example tasks.ym just for writing a message, checked file formats (for linefeeds, correct HEX values,...) and so on.
But Ansible doesnt like the playbook.
Thanks for all your suggestions.
Edit:
Ubuntu 18.04 LTS, Python 3.6.9, Ansible 2.9.27
Thanks to #Zeitounator (sorry for overlooing your first link), a suitable and working solution have been found:
---
- hosts: all
connection: local
vars:
ansible_python_interpreter: auto
tasks:
- name: "Doing letsencrypt ACME with AutoDNS for {{ nbb_domain }}"
collections:
- t_systems_mms.acme
include_role:
name: acme
vars:
nbb_emailadress: my.email#example.com
nbb_autodnsuser: login.user#other.com
acme_domain:
certificate_name: "{{ nbb_domain }}"
zone: "domain1.com"
email_address: "{{ nbb_emailadress }}"
subject_alt_name:
- "{{ nbb_domain }}"
acme_challenge_provider: autodns
acme_use_live_directory: true
acme_conf_dir: /etc/letsencrypt
acme_account_email: "{{ nbb_emailadress }}"
acme_dns_user: "{{ nbb_autodnsuser }}"
acme_dns_password: "supersecret"
loop:
- sub1.domain1.com
- sub2.domain1.com
loop_control:
loop_var: nbb_domain

Pass ansible synchronize module file permission attribute upon user input

I have ansible variable "changeperm" set to true or false and decides if the file being copied from source to destination get its permissioned changed.
If changeperm is false the file permissions should be retained.
If changeperm is true another variable file_perm=774 decides what permissions the copied destination file should have.
Below is my approach playbook post suggestion by #Matt P which does not work.
tasks:
- set_fact:
final_file_perm: "--chmod=F{{ file_perm }}"
when: changeperm
- set_fact:
ret_perm: 'yes'
when: not changeperm
- debug:
msg: "Permission is {{ final_file_perm | default(omit) }} and retain_permission is {{ ret_perm | default(omit) }}"
- name: Copying from "{{ inventory_hostname }}" to this ansible server.
synchronize:
src: "{{ item }}"
dest: "{{ playbook_dir }}/tmpfiles/{{ Latest_Build_Number }}"
perms: "{{ ret_perm | default(omit) }}"
rsync_opts: "{{ final_file_perm | default(omit) }}"
mode: pull
with_items:
- "{{ source_file.split(',') }}"
This is how I run the playbook:
ansible-playbook -i /var/inventory.hosts /var/test.yml -e changeperm=false -e file_perm=774 -e source_file=/tmp/sixhundredperm.txt
This works fine.
However, the below does not work
rsync_opts:
- "{{ final_file_perm | default(omit) }}"
Can you please suggest how can I get this to work when the attribute of rsync_opts is in the next new line ?

how to pass split values into a loop in ansible based on input range?

I am pretty new to Ansible loops.
I am trying to replace a set of ip addresses (oldip to newip) in a file.
It needs to accept the input ip address as a dynamic list in the form oldip1:newip1,oldip2:newip2,oldip3:newip3...
I want to split the above ip address pair (old:new) and then pass them over to Ansible replace module in a loop based on input list.
I am trying something like this, but am stuck on how to pass the dynamic range of ip address pair into the loop.
My input variable is like 192.123.12.11;192.123.12.20, 192.123.12.12;192.123.12.19 , 192.123.12.13;192.123.12.18 ... (dynamic ip pairs range)
vars:
ip_pairs: oldIP_newIP
tasks:
- debug: "{{ oldIP_newIP.split (',') }}"
- replace:
path: /home/ubuntu/cassandra.properties
regexp: "{{ item.regexp }}"
replace: "{{ item.replace }}"
backup: yes
with_items:
- { regexp = "{{ oldIP }}", replace = "{{ newIP }}"
- { regexp = "{{ oldIP }}", replace = "{{ newIP }}"
.
.
.
.
This loop should continue based on input ip pairs entry (5 times for 5 ip pairs (old;new).
Something really useful you can use in order to achieve this is the fact that you can register variables at the task level, via the vars property.
For exemple:
- name: This will display 'bar', the content of foo
debug:
msg: "{{ foo }}"
vars:
foo: 'bar'
Base on this, you can achieve what you want quite easily doing:
- name: Replace IPs
replace:
path: /home/ubuntu/cassandra.properties
regexp: "{{ oldNewIp[0] | trim }}"
replace: "{{ oldNewIp[1] | trim }}"
backup: yes
vars:
oldNewIp: "{{ item.split(';') }}"
with_items: "{{ ipPairs.split(',') }}"
Please mind that I added some extra Jinja trim in order to cope with the fact that, in your example input, you do have some space after your commas.
Here is a full example play to demo this:
Given the playbook
- hosts: local
vars:
ipPairs: '192.123.12.11;192.123.12.20, 192.123.12.12;192.123.12.19, 192.123.12.13;192.123.12.18'
tasks:
- name: Create an example file to replace in
copy:
dest: /tmp/ips.txt
content: ''
- name: Populate a file with old IPs
lineinfile:
path: /tmp/ips.txt
line: "{{ oldNewIp[0] | trim }}"
vars:
oldNewIp: "{{ item.split(';') }}"
with_items: "{{ ipPairs.split(',') }}"
- name: Replace IPs
replace:
path: /tmp/ips.txt
regexp: "{{ oldNewIp[0] | trim }}"
replace: "{{ oldNewIp[1] | trim }}"
backup: yes
vars:
oldNewIp: "{{ item.split(';') }}"
with_items: "{{ ipPairs.split(',') }}"
I have the following play:
/ # ansible-playbook -i inventory.yaml play.yaml
PLAY [local] *********************************************************************************************
TASK [Gathering Facts] ***********************************************************************************
[WARNING]: Platform linux on host local is using the discovered Python interpreter at /usr/bin/python3,
but future installation of another Python interpreter could change this. See
https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more
information.
ok: [local]
TASK [Create an example file to replace in] **************************************************************
changed: [local]
TASK [Populate a file with old IPs] ***********************************************************************
changed: [local] => (item=192.123.12.11;192.123.12.20)
changed: [local] => (item= 192.123.12.12;192.123.12.19)
changed: [local] => (item= 192.123.12.13;192.123.12.18)
TASK [Replace IPs] ***************************************************************************************
changed: [local] => (item=192.123.12.11;192.123.12.20)
changed: [local] => (item= 192.123.12.12;192.123.12.19)
changed: [local] => (item= 192.123.12.13;192.123.12.18)
PLAY RECAP ***********************************************************************************************
local : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
/ # cat /tmp/ips.txt
192.123.12.20
192.123.12.19
192.123.12.18

Is there any ansible module to change the existing server ip and reboot it

I have a playbook, which replace the IP and hostname. The playbook works fine till it reboots the machine, but unable to connect it back as now the IP of that server is changed to something else.
Is there any better way to handle this problem.
---
- hosts: test
remote_user: root
vars:
IP_TO_REPLACE: '192.168.1.15'
IP_TO_REPLACE_WITH: '192.168.1.16'
HOSTNAME_TO_REPLACE: 'devops-15'
HOSTNAME_TO_REPLACE_WITH: 'devops-16'
tasks:
- name: modifying the ifcfg-BR0 file
replace:
path: "{{ item.path }}"
regexp: "{{ item.regexp1 }}"
replace: "{{ item.replace }}"
backup: yes
with_items:
- { path: '/etc/sysconfig/network/ifcfg-br0', regexp1: "{{ IP_TO_REPLACE }}", replace: "{{ IP_TO_REPLACE_WITH }}" }
- { path: '/etc/hosts', regexp1: "{{ IP_TO_REPLACE }}", replace: "{{ IP_TO_REPLACE_WITH }}" }
- { path: '/etc/hosts', regexp1: "{{ HOSTNAME_TO_REPLACE }}", replace: "{{ HOSTNAME_TO_REPLACE_WITH }}" }
- { path: '/etc/hostname', regexp1: "{{ HOSTNAME_TO_REPLACE }}", replace: "{{ HOSTNAME_TO_REPLACE_WITH }}" }
register: task_result
- name: Reboot immediately the server to take latest changes
shell: "sleep 15 && reboot"
async: 1
poll: 0
when: task_result is changed
- name: Wait for the reboot to complete if there was a changed
wait_for:
port: 22
host: '{{ (ansible_ssh_host |default(ansible_host))|default(inventory_hostname) }}'
search_regex: OpenSSH
delay: 10
timeout: 90
when: task_result is changed
- name: Check the uptime of the server
shell: "uptime"
register: uptime_result
- debug: var=uptime_result
Actual Output
TASK [Reboot immediately the server to take latest changes] *****************************************************************************
fatal: [192.168.1.15]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Shared connection to 192.168.1.15 closed.\r\n", "unreachable": true}
Expected Output
It should connect to the new IP 192.168.1.16
My research lead me to the following solution
Inspire by the suggestion of "drenthe73" here:
https://www.reddit.com/r/ansible/comments/n6xfyv/change_current_ip_address_and_reboot_into_the_new/
playbook with 2 plays
---
# play 1
hosts:
- host_ip_initial
# ...
tasks:
- name: "change ip address"
# ...
- name: "reboot target"
ansible.builtin.shell: "reboot"
async: 1
poll: 0
# play2
gather_facts: no
hosts:
- host_ip_final
# ...
tasks:
- name: "Wait for the target to reboot before probing it is up and running"
ansible.builtin.wait_for:
timeout: 300
delegate_to: localhost
- name: "wait for boot up"
ansible.builtin.wait_for_connection:
connect_timeout: 5
sleep: 5
delay: 30
timeout: 600
You need to know the final IP address and creates in your inventory the proper homolog entries.
Ansible various way to restart target:
https://www.redhat.com/sysadmin/automate-reboot-ansible
I am a bit late, but it might help someone else...

Ansible: filter a merged yaml file

My current setup is as follows:
ansible.cfg
[defaults]
retry_files_enabled = false
hash_behaviour = merge
hosts
[sample]
sampleserver
group_vars
content of group_vars/1.yml:
tenants:
1:
name: name1
ip: ip1
content of group_vars/2.yml:
tenants:
2:
name: name2
ip: ip2
content of group_vars/3.yml:
tenants:
3:
name: name3
ip: ip3
playbook.yml
- hosts: sample
tasks:
- set_fact:
mytenants: "{{ {customtenants: tenants[customtenants]} }}"
when: customtenants is defined
- name: Output tenantname
debug:
msg: "mytenant: {{ mytenant }} name: {{ mytenant.name }}"
with_dict: "{{ mytenants|default(tenants) }}"
loop_control:
loop_var: mytenant
command
run for all tenants (works fine)
ansible-playbook playbook.yml -i inventory/hosts
run only for one defined tenant (works fine)
ansible-playbook tenants.yml -i inventory/hosts -e "customtenants=1"
run for multiple defined tenants
ansible-playbook playbook.yml -i inventory/hosts -e '{"customtenants": ["1", "3"]}'
Can anyone help me, so that it works for multiple tenants (last command example)?
Or is there a better way to achieve such deynamic solutions?
What I have done in the past is put that variable in the hosts line, e.g.:
- hosts: "{{ customtenants | default( 'all' ) }}"
Then you can delete the when and with_dict lines.

Resources