I use blockinfile to add some lines to a file (for example .bashrc) which works as expected (with a custom marker).
I can print a message if the block changed and the file changed (updated), but how can I get the previous value?
The use case is: I have several systems and I want this file to be the same everywhere; if a change is done on one box, I need to get the (locally changed) value back to distribute it later to the other boxes.
How to get the current value (i.e. the value before the change) back?
The current solution, which can be improved, is a list of tasks which is included with include_tasks. It is looped over all user accounts.
- name: make local copy of current bashrc
become: true
copy:
remote_src: yes
src: "/home/{{item}}/.bashrc"
dest: "/home/{{item}}/.bashrc.current"
- name: Add text at end of bashrc
become: true
blockinfile:
path: "/home/{{item}}/.bashrc"
block: "{{lookup('file', 'bashrc_additions.sh')}}"
backup: yes # what is the name?
create: no
state: present
marker: "# {mark} ANSIBLE {{ansible_role_name}} MANAGED BLOCK"
register: bashrc_updated
- name: action when changed
block:
- debug:
var: bashrc_updated
- debug:
msg:
- bashrc_updated - original is fetched to role.
- compare if changed, delete if same as previous target
- file "{{roleFilesDir}}bashrc_currentChanged_{{ ansible_hostname }}_{{ item }}"
- name: fetch original bashr if it was changed
fetch:
src: "/home/{{item}}/.bashrc.current" # isthis bak?
dest: "{{roleFilesDir}}bashrc_currentChanged_{{ ansible_hostname }}_{{ item }}"
# in ansiblehost on controller
flat: yes
when: bashrc_updated.changed |bool
Additionally, one could save the updated file and compare it the next time with the current one to detect if there are any local updates.
Related
I need to use Ansible modules to check if a the file exist, and if it doesn't, then create it and write some data in to it.
If the file exists then check the content, which I am trying to write, is present in that file.
If the content is not present, write the content into it.
If the content is present then do nothing.
My playbook below is not working.
Any suggestions on this?
- hosts: all
tasks:
- name: check for file
stat:
path: "{{item}}"
register: File_status
with_items:
- /etc/x.conf
- /etc/y.conf
- name: Create file
file:
path: "{{item}}"
state: touch
with_items:
- /etc/x.conf
- /etc/y.conf
when: not File_status.stat.exists
- name: add content
blockinfile:
path: /etc/x.conf
insertafter:EOF
block: |
mydata=something
Can you help me with the modules and conditions which can achieve my desired output?
The following will:
Create the file if it does not exist and report changed
Add the block at the end of file if it does not exists and report changed, i.e.
# BEGIN ANSIBLE MANAGED BLOCK
mydata=something
mydata2=somethingelse
# END ANSIBLE MANAGED BLOCK
Update the block wherever it is in the file if the content changed and report changed (see the marker option if you have several blocks to manage in the same file, and dont forget {mark} in there if you change it).
Do nothing if the block is up-to-date anywhere in the file and report ok.
Please read the module documentation for more info
---
- name: blockinfile example
hosts: localhost
gather_facts:false
tasks:
- name: Update/create block if needed. Create file if not exists
blockinfile:
path: /tmp/testfile.conf
block: |
mydata=something
mydata2=somethingelse
create: true
Here is the possible way to achieve your requirements.
- hosts: localhost
tasks:
- name: Create file
copy:
content: ""
dest: "{{item}}"
force: no
with_items:
- /etc/x.conf
- /etc/y.conf
- name: add content
blockinfile:
path: "{{ item.file_name }}"
insertafter: EOF
block: |
"{{ item.content }}"
loop:
- { file_name: '/etc/x.conf', content: 'mydata=something' }
- { file_name: '/etc/y.conf', content: 'mydata=hey something' }
Hoping with some help on the following. I'm trying to make reference to an xml file within the work_dir location:
---
- include_vars:
file: var_file.yml
- name: Locate audit results file
find:
paths: "{{ item }}"
recurse: no
patterns: '*.xml'
with_items: "{{ work_dir }}"
register: audit_file
- name: Copy audit results file to local destination
fetch:
src: "{{ item }}/{{ audit_file }}"
dest: /home/bob/audit_results/
flat: yes
validate_checksum: no
with_items: "{{ work_dir }}"
var_file.yml:
---
work_dir:
- /var/tmp/audit
However the above code keeps erroring with:
"msg": "unable to calculate the checksum of the remote file"}
You find files on a loop of paths and register the result. Therefore your registered var audit_file contains a results attribute which is a list.
Each element of the results list contains a files attribute which is again a list. Each files element contains a result of your find run with all the info about the found file. In those info, there is a path attribute pointing to the exact path of the file on the remote server.
Before going further, I strongly suggest your read the above documentation and issue a debug of your var to understand its structure and content
- debug:
var: audit_file
If you want to fetch each found file, you need to make a loop on each results with a sub-loop on each files element. This can be done with a subelements lookup although we will only use here the sub element (i.e. item.1) of the loop.
This is how you can fix your second task:
- name: Copy audit results file to local destination
fetch:
src: "{{ item.1.path }}"
dest: /tmp/test/
flat: yes
validate_checksum: no
with_subelements:
- "{{ audit_file.results }}"
- files
Note: this is not the only solution, I went to the easiest and most obvious.
How to register a variable when using loop with stat module?
I am working on a project where I wish to run comparisons against the known value of a collection of files (checksum), which I will then take action if a change is detected (EG: notify someone, have not written this part yet).
If this were purely a CLI matter, I would have this sorted with some easy SH scripting.
That said, I have Ansible (2.7.5) available within my ENV and am keen to use it!
In reading the vendor documents, using the stat module felt the "Ansible way" to go on this one.
Currently just *NIX servers (Linux, Solaris, and possibly AIX) are in scope, but eventually this might also apply to Windows, where I expect I would use win_stat instead with suitable parameters.
At present I plan to dump the results of the scan to a file (EG: CSV), which I would then iterate / match against, for the purposes of a comparison (to detect if a file has been somehow changed).
This is another part I have not written yet (the read a file and compare portions), but expect to hit those once I get this present matter sorted.
My current challenge, is that I can get "one-off" stat checks to work fine.
However, I expect to be targeting a whole directory worth of files, and thus want to presumably:
"discover" the contents of the target directory, and retain this in memory
iterate (loop) through the list in memory
performing a stat check upon each file
retaining the checksum of each file
building some sort of dict or list?
write the collective results (or one line at a time) out to a log file of sorts (CSV.log: file_path,file_checksum)
I would welcome your feedback on what I might be missing (aside from some hair at this point)?
I have tried a few different approaches to looping within the playbook (loop, with_items, etc.), however the challenge remains the same.
The stat loop runs fine, but the trailing register statement fails to commit the output to memory (resulting in a variety of "undefined variable" errors).
Am I somehow missing something in my loop definition?
Looking at the vendor docs on "Using register with a loop", it would appear I am doing this correctly (in my view anyway).
Simple "target files" I am checking against within a directory.
/tmp/app/targets/file1.txt
Some text.
/tmp/app/targets/file2.cfg
cluster=0
cluster_id=app_pool_00
/tmp/app/targets/file3.sh
#!/bin/sh
printf "Hello world\n"
exit 0
My prototyping playbook as it exists currently.
---
- name: check file integrity
hosts: localhost
become: no
vars:
TARGET: /tmp/app/targets
LOG: /tmp/app/archive/scan_results.log
tasks:
- name: discover target files
find:
paths: "{{ TARGET }}"
recurse: yes
file_type: file
register: TARGET_FILES
- name: scan target
stat:
path: "{{ item.path }}"
get_checksum: yes
loop: "{{ TARGET_FILES.files }}"
register: TARGET_RESULTS
- name: DEBUG
debug:
var: "{{ TARGET_RESULTS }}"
- name: write findings to log
copy:
content: "{{ TARGET_RESULTS.stat.path }},{{ TARGET_RESULTS.stat.checksum }}"
dest: "{{ LOG }}"
...
My "one-off" playbook that worked.
---
- name: check file integrity
hosts: localhost
become: no
vars:
TARGET: /tmp/app/targets/file1.txt
LOG: /tmp/app/archive/scan_results.log
tasks:
- name: scan target
stat:
path: '{{ TARGET }}'
checksum_algorithm: sha1
follow: no
get_attributes: yes
get_checksum: yes
get_md5: no
get_mime: yes
register: result
- name: write findings to log
copy:
content: "{{ result.stat.path }},{{ result.stat.checksum }}"
dest: "{{ LOG }}"
...
The output was not exciting, but useful.
Would expect to build this up with multi-line output (one line per file stat checked) if I could figure out how to loop / register loop output correctly.
/tmp/app/archive/scan_results.log
/tmp/app/targets/file1.txt,8d06cea05d408d70c59b1dbc5df3bda374d869a4
You can use the set_fact module to register a variable like you want.
I don't use it in my test for you, it maybe useless in your case :
---
- name: check file integrity
hosts: localhost
vars:
TARGET: /tmp/app/targets
LOG: /tmp/app/archive/scan_results.log
tasks:
- name: 'discover target files'
find:
paths: "{{ TARGET }}"
recurse: yes
file_type: file
register: TARGET_FILES
- debug:
var: TARGET_FILES
- name: 'scan target'
stat:
path: "{{ item.path }}"
get_checksum: yes
loop: "{{ TARGET_FILES.files }}"
register: TARGET_RESULTS
- debug:
var: TARGET_RESULTS
- name: 'write findings to log'
lineinfile:
line: "{{ item.stat.path }},{{ item.stat.checksum }}"
path: "{{ LOG }}"
create: yes
loop: '{{ TARGET_RESULTS.results }}'
result:
# cat /tmp/app/archive/scan_results.log
/tmp/app/targets/file3.sh,bb4b0ffe4b5d26551785b250c38592b6f482cab4
/tmp/app/targets/file1.txt,8d06cea05d408d70c59b1dbc5df3bda374d869a4
/tmp/app/targets/file2.cfg,fb23292e06f91a0e0345f819fdee34fac8a53e59
Best Regards
Using blockinfile: which is using with_items from an exernal file. When I run the playbook I can see all itmes being processed but the resulting end file only has the last item updated.
Apologies, bit of a noob to this, so might be missing something obvious.
Tried various permutations
I have an external yaml config file with following contents - which is included as include_vars:
yaml props file:
ds_props:
- prop: dataSource.initialSize=8
- prop: dataSource.maxActive=50
- prop: dataSource.maxIdle=20
- prop: dataSource.minIdle=5
- prop: dataSource.maxWait=1000
Ansible tasks:
- name: update DS settings
blockinfile:
path: /app.properties
insertafter: "##### Data Source Properties"
block: |
"{{ item.prop }}"
with_items: "{{ ds_props }}"
Expected:
##### Data Source Properties #####
# BEGIN ANSIBLE MANAGED BLOCK
dataSource.initialSize=8
dataSource.maxActive=50
dataSource.maxIdle=20
dataSource.minIdle=5
dataSource.maxWait=1000
# END ANSIBLE MANAGED BLOCK
Actual:
##### Data Source Properties #####
# BEGIN ANSIBLE MANAGED BLOCK
dataSource.maxWait=1000
# END ANSIBLE MANAGED BLOCK
blockinfile uses a marker to keep track of the blocks it manages in a file. By default, this marker is ANSIBLE MANAGED BLOCK.
What happens in your case since you use the default marker is that a block is created after the line "##### Data Source Properties" for the first item then edited for the next items.
One solution would be to change the marker for each item. An other is to use lineinfile as reported by #Larsk
I would prefer in this case creating the full block at once:
- name: update DS settings
blockinfile:
path: /app.properties
insertafter: "##### Data Source Properties"
marker: "Custom ds props - ansible managed"
block: "{{ ds_props | json_query('[].prop') | join('\n') }}"
If your intent is to do more complicated stuff with your config file, follow #Larsk advice and use a template.
blockinfile is behaving exactly as designed: it adds a block of text to a target file, and will remove the matching block before adding a modified version. So for every iteration of your loop, blockinfile is removing the block added by the previous iteration and adding a new one.
Given that you're adding single lines to the file, rather than a block, you're probably better off using the lineinfile module, as in:
---
- hosts: localhost
gather_facts: false
vars:
ds_props:
- prop: dataSource.initialSize=8
- prop: dataSource.maxActive=50
- prop: dataSource.maxIdle=20
- prop: dataSource.minIdle=5
- prop: dataSource.maxWait=1000
tasks:
- name: update DS settings using lineinfile
lineinfile:
path: /app.properties-line
line: "{{ item.prop }}"
insertafter: "##### Data Source Properties"
with_items: "{{ ds_props }}"
While this works, it's still problematic: if you change the value of one of your properties, you'll end up with multiple entries in the file. E.g, if we were to change dataSource.maxWait from 1000 to 2000, we would end up with:
dataSource.maxWait=1000
dataSource.maxWait=2000
We can protect against that using the regexp option to the lineinfile module, like this:
- name: update DS settings using lineinfile
lineinfile:
path: /app.properties-line
line: "{{ item.prop }}"
insertafter: "##### Data Source Properties"
regexp: "{{ item.prop.split('=')[0] }}"
with_items: "{{ ds_props }}"
This will cause the module to remove any existing lines for the particular property before adding the new one.
Incidentally, you might want to consider slightly restructuring your data, using a dictionary rather than a list of "key=value" strings, like this:
---
- hosts: localhost
gather_facts: false
vars:
ds_props:
dataSource.initialSize: 8
dataSource.maxActive: 50
dataSource.maxIdle: 20
dataSource.minIdle: 5
dataSource.maxWait: 1000
tasks:
- name: update DS settings using lineinfile
lineinfile:
path: /app.properties-line
line: "{{ item.key }}={{ item.value }}"
insertafter: "##### Data Source Properties"
regexp: "{{ item.key }}"
with_items: "{{ ds_props|dict2items }}"
And lastly, rather than using lineinfile or blockinfile, you might want to consider using ansible's template module to create your /app.properties file rather than trying to edit it.
I'm using the module stat for checking if some files exists in a remote server and registering their result in a variable. i.e. config files I'm looking for: conf_dev.conf, conf_pred.conf, conf_pro.conf.
Later I'm using the copy module for transfering only the files that are missing using a conditional based on an attribute stored in
variable.results.{{(ITEM)index}}.stat.exists.
This returnsTRUE if file exists and FALSE if does not.
For running properly As the register variable is an array there are an index storing the result for each file I'm asking so I don't know how to convert the item in a index (0,1,2,3)
Does anyone know how to get the index of an item? I've tried this (look at the last line):
- name: Checking if common configuration files exists
stat:
path: "{{HOME_COMUN_CONFIG}}/{{item}}"
with_items: "{{LIST_COMMON_CONFIGURATION_ARTIFACTS}}"
register: store_results
- name: debug existe_app_comun
debug:
var: store_results
- name: Deploying missing files
copy:
src: "{{DIRTEMP_COMUN_CONFIG}}/{{item}}"
dest: "{{HOME_COMUN_CONFIG}}/{{item}}"
with_items: "{{LIST_COMMON_CONFIGURATION_ARTIFACTS}}"
when: existe_app_comun.results.{{index(item)}}.stat.exists is defined
Why do you loop over LIST_COMMON_CONFIGURATION_ARTIFACTS if you want to loop over registered variable?
- name: Deploying missing files
copy:
src: "{{DIRTEMP_COMUN_CONFIG}}/{{ item.item }}"
dest: "{{HOME_COMUN_CONFIG}}/{{ item.item }}"
with_items: "{{ existe_app_comun.results }}"
when: item.stat.exists
Here item is an element of results and item.item is an element of original loop.
P.S. if files at destination folder are not modified and should be same as in DIRTEMP_COMUN_CONFIG, then you must not use stat+copy, but just use copy – because it's idempotent and will not copy the same file twice.