Ansible - Versionfile check - file

I want to be able to read a versionfile if it exists, and check its contents. Then return True if the version changed or the file does not exists, False if versionfile exists and the version matches the content.
Basically this:
# setup test data
- set_fact:
version_expected: "0001"
version_path: "/path/to/version"
version_owner: "root"
version_group: "root"
# this block is used to check for version changes
- name: check version change
block:
- name: check version file
stat:
path: "{{version_path}}"
register: version_file
- set_fact:
version_remote: "{{ lookup('file', version_path) | default('') }}"
when: version_file.stat.exists
- set_fact:
version_changed: not version_file.stat.exists or version_remote != version_expected
# test writing new version
- name: write file
copy:
dest: "{{version_path}}"
content: "{{version_expected}}"
owner: "{{version_owner}}"
group: "{{version_group}}"
when: version_changed
My problem is: This is somewhat ugly and becoming quite redundant in my roles.
Is there a more elegant way to do this?
Is there maybe a module for this? (though I found none)
Or should I just write a module for this?
Best regards,
2d4r
EDIT:
im only meaning the "check version change" block, the surrounding code is for debugging only.
To be more specific, I want to download a server binary, but only if my expectet version differs from the content of the versionfile.
I want to write the new version to file, if (and only if) the download was successfull, but that is not part of my question.
EDIT2:
I got this by now:
# roles/_helper/tasks/version_check.yml
- name: check if file exists
stat:
path: "{{version_path}}"
register: version_file
- name: get remote version
slurp:
src: "{{version_path}}"
register: version_changed
when: version_file.stat.exists
# (False if versionfile exists and version is expected; True else)
- name: set return value
set_fact:
version_changed: "{{ not version_file.stat.exists or ((version_changed.content | b64decode) is version_compare(version_expected, 'ne')) }}"
used like this:
# /roles/example/tasks/main.yml
- include_role:
name: _helper
tasks_from: version_check
vars:
version_path: "{{file_version_path}}"
version_expected: "{{file_version_expected}}"
- name: doing awesome things
when: version_changed
block:
- name: download server
[...]
- name: write version
copy:
dest: "{{file_version_path}}"
content: "{{file_version_expected}}"
It kills the redundancy, but is still not what I want.
Sadly I can not register a return value from a role.

Delete everything except for write file task and remove the condition.
Ansible does this automatically for you.
- name: write file
copy:
dest: "{{version_path}}"
content: "{{version_expected}}"
owner: "{{version_owner}}"
group: "{{version_group}}"
After you changed the question, given the information provided, the only thing I can point to is to use slurp module instead of lookup, as an lookup plugins work locally in the control machine.
Compare versions using your logic or built-in version_compare filter/test.

Related

Ansible: Create sublist from list using "startswith" comparison

I have system (Zabbix) that uses crude group/subgroup definition based on "/" delimiter which are defined in plain list.
For example:
"Grp(1)"
"Grp(1)/Subgrp(A)"
"Grp(1)/Subgrp(B)"
"Grp(2)"
"Grp(2)/Subgrp(X)"
This defines two groups, Grp(1) with two subgroups (A and B) and Grp(2) with one subgroup (X)
If I logically assign user to "Grp(1)" it is expected that user also automatically have right to "Grp(1)/Subgrp(A)" and "Grp(1)/Subgrp(B)"
Example vars file looks like:
---
groups_vars:
- "Grp(1)"
- "Grp(1)/Subgrp(A)"
- "Grp(1)/Subgrp(B)"
- "Grp(2)"
- "Grp(2)/Subgrp(X)"
The vars are used in ansible galaxy module community.zabbix, there is simplified usage for Grp(1):
- name: Ensure user groups are created and right to itself and subgroups are assigned
community.zabbix.zabbix_usergroup:
name: Grp(1)
rights:
- {host_group: ["Grp(1)","Grp(1)/Subgrp(A)","Grp(1)/Subgrp(B)"], permission: "read-write" }
I have tried to achieve "set_fact" transformation of input vars into format more suitable for ansible loop:
---
groups_vars:
-
name: Grp(1)
rights:
host_group:
- Grp(1)
- Grp(1)/Subgrp(A)
- Grp(1)/Subgrp(B)
permission: read-write
-
name: Grp(1)/Subgrp(A)
rights:
host_group:
- Grp(1)/Subgrp(A)
permission: read-write
-
name: Grp(1)/Subgrp(B)
rights:
host_group:
- Grp(1)/Subgrp(B)
permission: read-write
-
name: Grp(2)
rights:
host_group:
- Grp(2)
- Grp(1)/Subgrp(X)
permission: read-write
-
name: Grp(2)/Subgrp(X)
rights:
host_group:
- Grp(2)/Subgrp(X)
permission: read-write
But I failed to define the transformation. The select('match', ) function that I try to use for filtering is regex based but itself can contain regex directives (name "Grp(1)" contains parenthesis that are regex directives) and I cannot fing any "startswith" method for finding subgroups.
My idea was, that for each group from original group_vars defined above I find all items, that begins with the group name (so for "Grp(2)" I will find "Grp(2)" and "Grp(2)/Subgrp(X)", for "Grp(2)/Subgrp(X)" I will find only "Grp(2)/Subgrp(X)" itself)
Please any ideas how to solve the problem?
Maybe my approach is complete wrong, if there is any more elegant solution, please help.
I finally found working approach for the problem.
I created simple Python script that generates data usable for simple plain ansible loop.
The inner loop is emulated by generation of complex structure (array of dictionaries)
There is source YML file:
---
groups_vars:
- "z_JC(015)"
- "z_JC(015)/Pisek(022)"
- "z_HK(055)"
There is trasformed YML file, the inner loop is emulated by array under "rights:"
usergroups_vars:
- group: z_JC(015)
rights:
- host_group: z_JC(015)
permission: read-write
- host_group: z_JC(015)/Pisek(022)
permission: read-write
- group: z_JC(015)/Pisek(022)
rights:
- host_group: z_JC(015)/Pisek(022)
permission: read-write
- group: z_HK(055)
rights:
- host_group: z_HK(055)
permission: read-write
The playbook works simply with trasformed YML file usin simple plain loop:
- name: Ensure z_ prefixed Zabbix UserGroups are present and linked to eponymous HostGroups and subhostgroups
community.zabbix.zabbix_usergroup:
server_url: "{{ static_hostvars.server_url }}"
login_user: "{{ static_hostvars.login_user }}"
login_password: "{{ static_hostvars.login_password }}"
state: "present"
name: "{{ item.group }}"
rights: "{{ item.rights }}"
loop: "{{ usergroups_vars }}"
There is example of the Python transformation script (using pyyaml library):
import yaml
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
# Load and parse host group var yaml file
hostgroups = None
with open('groups_vars.yaml') as f:
hostgroups = yaml.load(f, Loader=yaml.FullLoader)
# Create eponymous usergroups for hostgroups with prefix 'z_'
usergroups = []
for hostgroup in hostgroups.get('groups_vars'):
if hostgroup.startswith('z_'):
usergroups.append(hostgroup)
# Find subgroups ut of list og groups delimited by '/'
# (E.g array ['grp1','grp1/subgrp1'] defined one group 'grp1' and one subgroup 'subgrp1')
usergrpsubgrps = []
for onegrp in usergroups:
# Find subgroups (including the group itself)
subgrps = []
for onesubgroup in usergroups:
if onesubgroup.startswith(onegrp):
subgrps.append({'host_group': onesubgroup, 'permission': 'read-write'})
usergrpsubgrps.append({'group': onegrp, 'rights': subgrps})
out_yaml = yaml.dump({'usergroups_vars' : usergrpsubgrps})
print(out_yaml)
# Write output yaml to the output ansible vars file
out_file = open('usergroups_vars.yaml','w')
out_file.write(out_yaml)
out_file.close()

ansible register with loops

I want to create user and gather their info in local file but with loop register is now working as expected.
I thought it was an indentation problem but no luck.
My playbook
---
- hosts: localhost
tasks:
- name: Clearing Local file
local_action: shell echo "Zone,docode,doname,testuser Output" > user.csv
- hosts: app
tasks:
- name: Creating user Testuser
become: yes
user:
name: "{{ item }}"
state: present
shell: "/bin/bash"
password: "$6$mysecretsalt$qyctTVhRMS1ZSnCuzQNAM8Y7V/yqSEnyRbal0IYXSqSEVKkXF8ZmXBZoRIaN/PvzE/msq8iOJO830OOCG89va/"
update_password: always
groups: santosh
shell: id "{{item}}"
ragister: userout
loop:
- newuser1
- newuser2
- newuser3
- debug:
var=userout
which gives the following error when executed
ERROR! conflicting action statements: shell, user
The error appears to have been in '/home/santosh/ans-home/playbooks/Create_User_and_Gather_output.yml': line 12, column 7, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: Creating user Testuser
^ here
You cannot call several modules in one task, you need to separate each call in its own task, as reported by the error message.
I understand why you tried it: to take advantage of the same loop for several task. Unfortunately this is not possible. You could move your set of tasks to a separate file and include it in a loop if you really have to loop over a significant amount of tasks. This is not really required in your situation because:
you only have two tasks
you can compact writing of your loop by using a declared var for reuse
and most essentially because you don't need your second task
In fact, the user module will return the uid of the user it created or that is existing in its result. You just have to register the result of calling the user module.
Just try the following for your second play:
- name: Resgister application users
hosts: app
vars:
user_list:
- newuser1
- newuser2
- newuser3
tasks:
- name: Create the users if they don't exist
user:
name: "{{ item }}"
state: present
shell: "/bin/bash"
password: "$6$mysecretsalt$qyctTVhRMS1ZSnCuzQNAM8Y7V/yqSEnyRbal0IYXSqSEVKkXF8ZmXBZoRIaN/PvzE/msq8iOJO830OOCG89va/"
update_password: always
groups: santosh
register: create_users
loop: "{{ user_list }}"
- name: Show ids of users
debug:
msg: "The uid of user {{ item.name }} is: {{ item.uid }}"
loop: "{{ create_users.results }}"
And as a side note: for your first play, do yourself a favor and stop using the old local_action syntax in favor of delegate_to: localhost for a task. It is not even required in your case as your are already targeting your play to localhost only.

How to loop in Ansible $var number of times?

I want to run a loop in Ansible the number of times which is defined in a variable. Is this possible somehow?
Imagine a list of servers and we want to create some numbered files on each server. These values are defined in vars.yml:
server_list:
server1:
name: server1
os: Linux
num_files: 3
server2:
name: server2
os: Linux
num_files: 2
The output I desire is that the files /tmp/1, /tmp/2 and /tmp/3 are created on server1, /tmp/1 and /tmp/2 are created on server2. I have tried to write a playbook using with_nested, with_dict and with_subelements but I can't seem to find any way to to this:
- hosts: "{{ target }}"
tasks:
- name: Load vars
include_vars: vars.yml
- name: Create files
command: touch /tmp/{{ loop_index? }}
with_dict: {{ server_list[target] }}
loop_control:
loop_var: {{ item.value.num_files }}
If I needed to create 50 files on each server I can see how I could do this if I were to have a list variable for each server with 50 items in it list which is simply the numbers 1 to 50, but that would be a self defeating use of Ansible.
There is a chapter in the docs: Looping over Integer Sequences (ver 2.4)
For your task:
- file:
state: touch
path: /tmp/{{ item }}
with_sequence: start=1 end={{ server_list[target].num_files }}
Update: things has changed in Ansible 2.5. See separate docs page for sequence plugin.
New loop syntax is:
- file:
state: touch
path: /tmp/{{ item }}
loop: "{{ query('sequence', 'start=1 end='+(server_list[target].num_files)|string) }}"
Unfortunately sequence accepts only string-formatted parameters, so parameters passing to query looks quite clumsy.

In Ansible, how can I fetch file from multiple nodes and store this in one file centralized?

All what I need is in title, for example I want to know how I can do somethings like that :
---
- hosts: ansible-clients
tasks:
- name: Fetch source list from clients
fetch: src=/etc/apt/sources.list
dest=/tmp/allnodes.sourcelist
OR in simply way
echo remote#/etc/apt/sources.list >> local#/tmp/allnodes.sourcelist
I can create and run script in local but the only condition I have is to do all actions in one playbook.
You can use this play:
---
- hosts: ansible-clients
tasks:
- name: Fetch source list from clients
fetch:
src: /etc/apt/sources.list
flat: yes
dest: "/tmp/{{ inventory_hostname }}.sourcelist"
- name: Merge files
run_once: yes
delegate_to: localhost
shell: "cat /tmp/{{ item }}.sourcelist >> /tmp/allnodes.sourcelist"
with_items: "{{ groups['ansible-clients'] }}"
First task is used to fetch all files from remotes and store them in /tmp. inventory_hostname is used in filename to be sure it is unique.
Second task is run once on any host, and append all files (get list of hosts linked to group ansible-clients) in final file

ansible loop over list and dictionary at the same time

I am writing a playbook that ensure nodes appear in /etc/fstab.
I am using loops to prevent code duplication.
The logic is first to check if the line appears using grep (with perl regex because it is a multi line) and store the results in a register.
Then I want to add only the lines that are not in fstab file. To achieve that I need to loop over list (the register with the grep return codes) and a dictionary (that contains the fstab entries).
I am having errors with the parallel loop. I tried to follow these steps.
One or more undefined variables: 'str object' has no attribute 'item'
tasks/fstab.yaml:
---
- name: Make dirs
sudo: yes
file: path={{ item.value }} state=directory
with_dict:
"{{ fstab.paths }}"
- name: Check whether declared in fstab
sudo: no
command: grep -Pzq '{{ item.value }}' /etc/fstab
register: is_declared
with_dict:
"{{ fstab.regexs }}"
- name: Add the missing entries
sudo: yes
lineinfile: dest=/etc/fstab line="{{ item.1.item.value }}"
when: item.0.rc == 1
with_together:
- "{{ is_declared.results }}"
- "{{ fstab.entries }}"
vars/main.yml:
---
fstab:
paths:
a: "/mnt/a"
b: "/mnt/b"
regexs:
a: '\n# \(a\)\nfoo1'
b: '\n# \(b\)\nfoo2'
entries:
a: "\n# (a)\nfoo1"
b: "\n# (b)\nfoo2"
I am not using template on purpose (I want to add entries to existing files and not to over write them).
UPDATE: I see ansible has module "mount" which deals with fstab. However I am still looking for a solution to this issue because I might be needed it again later on.
I have a couple ideas as to why your original approach was failing, but let's scratch that for a moment. It looks like you're overcomplicating things- why not use a complex list var to tie it all together, and use the regexp arg to the lineinfile module instead of a separate regex task? (though your sample data should work fine even without the regexp param) Something like:
---
- name: Make dirs
sudo: yes
file: path={{ item.path }} state=directory
with_items: fstab
- name: Add the missing entries
sudo: yes
lineinfile: dest=/etc/fstab line={{ item.entry }} regexp={{ item.regex }}
with_items: fstab
fstab:
- path: /mnt/a
regex: '\n# \(a\)\nfoo1'
entry: "\n# (a)\nfoo1"
- path: /mnt/b
regex: '\n# \(b\)\nfoo2'
entry: '\n# (b)\nfoo2'

Resources