Concatenating files with Ansible - ansible

Using Ansible, how would I go about copying the contents of the example file:
/tmp/example.txt
cat example.txt
1234
And adding its contents to the below file:
/tmp/example2.txt
cat example2.txt
5678
So, in the end, example2.txt has the contents of:
12345678

The below task will accomplish this:
- hosts: localhost
connection: local
gather_facts: false
tasks:
- name: update example2.txt
lineinfile:
path: example2.txt
regexp: "^{{ lookup('file', 'example2.txt') }}"
line: "{{ lookup('file', 'example.txt') }}{{ lookup('file', 'example2.txt') }}"

Related

Can I copy text from remote to local with Ansible playbook?

I know the Ansible fetch-module can copy a file from remote to local, but what if I only need the contents (in my case a tmp file holding the ip address) appended into a local file?
Fetch module does this:
- name: Store file into /tmp/fetched/
ansible.builtin.fetch:
src: /tmp/somefile
dest: /tmp/fetched
I need it to do something like this:
- name: Store file into /tmp/fetched/
ansible.builtin.fetch:
src: /tmp/somefile.txt
dest: cat src >> /tmp/fetched.txt
In a nutshell:
- name: Get remote file content
ansible.builtin.slurp:
src: /tmp/somefile.txt
register: somefile
- name: Append remote file content to a local file
vars:
target_file: /tmp/fetched.txt
ansible.builtin.copy:
content: |-
{{ lookup('file', target_file) }}
{{ somefile.content | b64decode }}
dest: "{{ target_file }}"
# Fix write concurrency when running on multiple targets
throttle: 1
delegate_to: localhost
Notes:
the second task isn't idempotent (will modify the file on each run even with the same content to append)
this will work for small target files. If that file becomes huge and you experience high execution times / memory consumptions, you might want to switch to shell for the second task:
- name: Append remote file content to a local file
ansible.builtin.shell:
cmd: echo "{{ somefile.content | b64decode }}" >> /tmp/fetched
# You might still want to avoid concurrency with multiple targets
throttle: 1
delegate_to: localhost
Alternatively, you could write all contents from all fetched files from all your targets in one go to avoid the concurrency problem and gain some time.
# Copy solution
- name: Append remote files contents to a local file
vars:
target_file: /tmp/fetched.txt
fetched_content: "{{ ansible_play_hosts
| map('extract', hostvars, 'somefile.content')
| map('b64decode')
| join('\n') }}"
ansible.builtin.copy:
content: |-
{{ lookup('file', target_file) }}
{{ fetched_content }}
dest: "{{ target_file }}"
delegate_to: localhost
run_once: true
# Shell solution
- name: Append remote files contents to a local file
vars:
fetched_content: "{{ ansible_play_hosts
| map('extract', hostvars, 'somefile.content')
| map('b64decode')
| join('\n') }}"
ansible.builtin.shell:
cmd: echo "{{ fetched_content }}" >> /tmp/fetched
delegate_to: localhost
run_once: true

Create Local File With Ansible Template From Variables

I'm running an ansible playbook against a number of ec2 instances to check if a directory exists.
---
- hosts: all
become: true
tasks:
- name: Check if foo is installed
stat:
path:
/etc/foo
register: path
- debug: msg="{{path.stat.exists}}"
And I would like to generate a localfile that lists the private IP addresses of the ec2 instances and states whether or not the directory foo does exist.
I can get the private IP addresses of the instances with this task
- name: Get info from remote
shell: curl http://169.254.169.254/latest/meta-data/local-ipv4
register: bar
- debug: msg="{{bar.stdout}}"
How do I create a local file with content
IP address: 10.100.0.151 directory foo - false
IP address: 10.100.0.152 directory foo - true
I've tried adding a block for this as such
- hosts: localhost
become: false
vars:
installed: "{{bar.stdout}}"
status: "{{path.stat.exists}}"
local_file: "./Report.txt"
tasks:
- name: Create local file with info
copy:
dest: "{{ local_file }}"
content: |
"IP address {{ installed }} foo - {{ status }}"
But it doesn't look like I can read values of variables from earlier steps.
What am I doing wrong please?
A similar question has been answered here.
Basically what you want is to reference it through the host var variable.
This should work.
- hosts: localhost
become: false
vars:
local_file: "./Report.txt"
tasks:
- name: Create local file with info
lineinfile:
path: "{{ local_file }}"
line:
"IP Address: {{ hostvars[item]['bar'].stdout }} - Installed: {{ hostvars[item]['path'].stat.exists }}"
with_items: "{{ query('inventory_hostnames', 'all') }}"
And this should populate your local ./Report.txt file, with the info you need.
I've used the ec2_metadata_facts module to get the IP address us ingansible_ec2_local_ipv4
I've also created the directory /tmp/testdir on the second host.
- hosts: test_hosts
gather_facts: no
vars:
directory_name: /tmp/testdir
tasks:
- ec2_metadata_facts:
- name: check if directory '{{ directory_name }}' exsists
stat:
path: "{{ directory_name }}"
register: path
# I make the outputfile empty
# because the module lineinfile(as I know) can't overwrite a file
# but appends line to the old content
- name: create empty output file
copy:
content: ""
dest: outputfile
delegate_to: localhost
- name: write output to outputfile
lineinfile:
dest: outputfile
line: "IP Address: {{ ansible_ec2_local_ipv4 }} {{ directory_name }} - {{ path.stat.exists }}"
state: present
with_items: "{{ groups.all }}"
# with_items: "{{ ansible_play_hosts }}" can also be used here
delegate_to: localhost
The outputfile looks like:
IP Address: xxx.xx.x.133 /tmp/testdir - False
IP Address: xxx.xx.x.45 /tmp/testdir - True

ansible lineinfile replace a line begin containing a pattern with default ports in multiple files based on input array

---
-
gather_facts: true
hosts: abc,pqr,xyz
name: "playbook"
serial: 1
tasks:
-
block:
-
shell: "ls /etc/maria/" # could be n number of ports like 123, 456, 789
register: ports
-
lineinfile:
line: PORT={{ new_ports }}
path: "/etc/instance_{{ item }}/mariadb.conf"
regexp: '^PORT='
with_items: "{{ ports.stdout_lines }}"
new_ports: # length of this would be based on number of ports, should create an array based on array size of `ports`
- 8880
- 8881
- 8882
Say for example I've some 3 ports from ls /etc/maria/ 123456789 and 3 corresponding files /etc/instance_123/mariadb.conf,
/etc/instance_456/mariadb.conf,
/etc/instance_789/mariadb.conf.
could be n number of ports & files.
I would like to replace line starting with ^PORT= in /etc/instance_123/mariadb.conf to PORT=8880 and so on until the length of array ports.
Results getting with suggested code:
Before
/etc/instance_1234/mariadb.conf | grep port
port=1234
playbook
- hosts: abc
gather_facts: false
vars:
ansible_python_interpreter: /usr/bin/python3
serial: 1
tasks:
- shell: ls /etc/maria/
register: ports
- debug:
msg: "{{ ports.stdout_lines|length }}"
- lineinfile:
path: "/etc/instance_{{ item }}/mariadb.conf"
regexp: "^port=.*$"
line: "port={{ new_ports[ansible_loop.index0] }}"
loop: "{{ ports.stdout_lines }}"
loop_control:
extended: true
vars:
new_ports: "{{ range(8880, 8880 + ports.stdout_lines|length) }}"
After
/etc/instance_1234/mariadb.conf | grep port
port=x
For example, given the files
shell> ssh admin#test_11 ls /tmp/mongo
123
456
shell> ssh admin#test_12 ls /tmp/mongo
123
456
789
The playbook
- hosts: test_11,test_12
gather_facts: false
serial: 1
tasks:
- command: ls /tmp/mongo
register: ports
- lineinfile:
path: "/tmp/etc/instance_{{ item }}/mongodb.conf"
regexp: "^PORT=.*$"
line: "PORT={{ new_ports[ansible_loop.index0] }}"
create: true
loop: "{{ ports.stdout_lines }}"
loop_control:
extended: true
vars:
new_ports: "{{ range(8880, 8880 + ports.stdout_lines|length) }}"
creates, or updates the configuration files
shell> ssh admin#test_11 ls /tmp/etc/
instance_123
instance_456
shell> ssh admin#test_11 cat /tmp/etc/instance_123/mongodb.conf
PORT=8880
shell> ssh admin#test_11 cat /tmp/etc/instance_456/mongodb.conf
PORT=8881
shell> ssh admin#test_12 ls /tmp/etc/
instance_123
instance_456
instance_789
shell> ssh admin#test_12 cat /tmp/etc/instance_123/mongodb.conf
PORT=8880
shell> ssh admin#test_12 cat /tmp/etc/instance_456/mongodb.conf
PORT=8881
shell> ssh admin#test_12 cat /tmp/etc/instance_789/mongodb.conf
PORT=8882

Ansible: any file from dictionary

Today I totally broke my brain with that. I have some structure with dictionary files and I need to make some text or json with variables which uses in dictionary:
> hosts_vars
varfile.yaml
> inventory
hosts.yml
tasks.yml
varfile.yaml
host_vars:
host1: { ip: 192.168.1.1 }
host2: { ip: 192.168.1.2 }
hosts.yml
all:
hosts:
host1:
ansible_host: 192.168.1.1
host2:
ansible_host: 192.168.1.2
tasks.yml
- hosts: all
become: no
gather_facts: false
vars_files:
- hosts_vars/varfile.yaml
vars:
temp_file: /tmp/out_file.txt
tasks:
- local_action:
module: ansible.builtin.lineinfile
path: '{{ temp_file }}'
line: |
- targets: ['{{ host_vars[inventory_hostname].ip }}']
state: present
When I run it: ansible-playbook tasks.yml -l all
I got:
~> cat /tmp/out_file.txt
- targets: ['192.168.1.1']
I didn't understand why it happened. When if I had 60 hosts I'll got ~54 lines in file. How it works?
You need to create the file in localhost and delegate the task to localhost looping hosts.
tasks:
- name: update host
lineinfile:
path: '{{ temp_file }}'
create: yes
line: |
- targets: ['{{ host_vars[item].ip }}'] # or ['{{ host_vars[item]['ip'] }}']
insertafter: EOF
delegate_to: localhost
run_once: true
with_inventory_hostnames:
- all
For example
tasks:
- ansible.builtin.lineinfile:
create: yes
path: "{{ temp_file }}"
line: "- targets: ['{{ host_vars[inventory_hostname].ip }}']"
delegate_to: localhost
gives
shell> cat /tmp/out_file.txt
- targets: ['192.168.1.2']
- targets: ['192.168.1.1']
Notes:
Don't use Yaml Literal Style block with line. It says what it is. You don't want any newlines \n in the line. If you for whatever reason have to use the block put dash - behind the pipe |. This will remove the trailing newline \n
- ansible.builtin.lineinfile:
create: true
path: "{{ temp_file }}"
line: |-
- targets: ['{{ host_vars[inventory_hostname].ip }}']
delegate_to: localhost
You can use delegate_to: localhost instead of local_action
Use create: yes. It is no by default and the task will fail Destination /tmp/out_file.txt does not exist ! if the file doesn't exist.
state: present is default
insertafter: EOF is default
Putting varfile.yaml into the directory host_vars is missing the point how host_vars work. There is no host with the name varfile. You should put the file, e.g. into vars/varfile.yaml instead.
The variable host_vars is redundant. The IP of the hosts is stored in the variable ansible_host. The task below gives the same result
- ansible.builtin.lineinfile:
create: true
path: "{{ temp_file }}"
line: "- targets: ['{{ ansible_host }}']"
delegate_to: localhost
If you'd like to create a list of all IP addresses the task below
- ansible.builtin.lineinfile:
create: true
path: "{{ temp_file }}"
line: "- targets: {{ ansible_play_hosts_all|
map('extract', hostvars, 'ansible_host') }}"
run_once: true
delegate_to: localhost
gives
shell> cat /tmp/out_file.txt
- targets: ['192.168.1.1', '192.168.1.2']
macOS with ansible [core 2.11.2] had issue with write files in /tmp If you want to use this you need to set the home folder or any other folder with root permissions.

Place file contents in email body in Ansible

I have a playbook that checks hard drive space on a group of servers, sends out an email, and it attaches a file containing the output contents. Is there a way to place the contents of the file in the body itself? I would like the file contents to be able to be seen at a glance in the email. The content would be the registered variable, result.
The current tasks:
---
- name: Check for output file
stat:
path: /tmp/ansible_output.txt
register: stat_result
delegate_to: localhost
- name: Create file if it does not exist
file:
path: /tmp/ansible_output.txt
state: touch
mode: '0666'
when: stat_result.stat.exists == False
delegate_to: localhost
- name: Check hard drive info
become: yes
become_user: root
shell: cat /etc/hostname; echo; df -h | egrep 'Filesystem|/dev/sd'
register: result
- debug: var=result.stdout_lines
- local_action: lineinfile line={{ result.stdout_lines | to_nice_json }} dest=/tmp/ansible_output.txt
- name: Email Result
mail:
host: some_email_host
port: some_port_number
username: my_username
password: my_password
to:
- first_email_address
- second_email_address
- third_email_address
from: some_email_address
subject: My Subject
body: <syntax for file contents in body here> <--- What is the syntax?
attach:
/tmp/ansible_output.txt
run_once: true
delegate_to: localhost
- name: Remove Output File
file:
path: /tmp/ansible_output.txt
state: absent
run_once: true
delegate_to: localhost
Edit: I tried
body: "{{ result.stdout_lines | to_nice_json }}"
but it only sends me the output of the first host in the group.
Ok, I figured it out. I created a directory, files, and sent the output to a file in that directory using the {{ role_path }} variable. In the body portion of the email task, I used the lookup plugin to grab the contents of the file.
Here is the updated playbook with the original lines commented out:
---
- name: Check for output file
stat:
#path: /tmp/ansible_output.txt
path: "{{ role_path }}/files/ansible_output.txt"
register: stat_result
delegate_to: localhost
- name: Create file if it does not exist
file:
#path: /tmp/ansible_output.txt
path: "{{ role_path }}/files/ansible_output.txt"
state: touch
mode: '0666'
when: stat_result.stat.exists == False
delegate_to: localhost
- name: Check hard drive info
become: yes
become_user: root
shell: cat /etc/hostname; echo; df -h | egrep 'Filesystem|/dev/sd'
register: result
- debug: var=result.stdout_lines
#- local_action: lineinfile line={{ result.stdout_lines | to_nice_json }} dest=/tmp/ansible_output.txt
- local_action: lineinfile line={{ result.stdout_lines | to_nice_json }} dest="{{ role_path }}/files/ansible_output.txt"
- name: Email Result
mail:
host: some_email_host
port: some_port_number
username: my_username
password: my_password
to:
- first_email
- second_email
- third_email
from: some_email_address
subject: Ansible Disk Space Check Result
#body: "{{ result.stdout_lines | to_nice_json }}"
body: "{{ lookup('file', '{{ role_path }}/files/ansible_output.txt') }}"
#attach:
#/tmp/ansible_output.txt
attach:
"{{ role_path }}/files/ansible_output.txt"
run_once: true
delegate_to: localhost
- name: Remove Output File
file:
#path: /tmp/ansible_output.txt
path: "{{ role_path }}/files/ansible_output.txt"
state: absent
run_once: true
delegate_to: localhost
Now, my email contains the attachment, as well as the contents in the body, and I didn't have to change much in the playbook. :-)

Resources