Ansible : appending data of file using template module - ansible

Is there a way to append data using a template module in ansible. There are options for lineinfile and blockinfile in ansible. But I have to append data to the host's file. I want to preserve original hosts and add new hosts to the existing file.

When using the template module, a complete file is templated.
What you could do is dynamically build your file in a way like this in the Jinja2 template:
This is content of my file, not being replaced.
{% for item in some_variable %}
{% {{ item }} %}
{% endfor %}
And here is the end of the file, not being replaced.
However, it feels like you're trying to edit a /etc/hosts file or something similar. You shouldn't use template for this use case.
Using lineinfile is perfect for those scenarios. Try something along the lines of:
- name: Add the hosts names and IPs to /etc/hosts
lineinfile:
dest: /etc/hosts
regexp: '.*{{ item }}$'
line: "{{ hostvars[item]['ansible_default_ipv4']['address'] }} {{item}}"
state: present
when: hostvars[item]['ansible_facts']['default_ipv4'] is defined
with_items:
- "{{ groups['all'] }}"

You can use blockinfile to accomplish above as below:
- name: Add the below DNS records
blockinfile:
path: /etc/hosts
marker: "------"
insertafter: '^yourlinepattern'
state: present
block: |
lines to append

Related

Can slurp be used as a direct replacement for lookup?

I have the following in a template file foo.cfg.j2:
nameserver dns1 {{ lookup('file','/etc/resolv.conf') | regex_search('\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b') }}:53
The problem is it gets the value from the local /etc/resolv.conf. I wanted to get the value from the target, I learned that I have to use slurp, but the second "problem" is that I need to register an variable in the task, and then pass it to the template file, like this:
tasks:
- slurp:
src: /etc/resolv.conf
register: slurpfile
and the template file is now like this:
nameserver dns1 {{ slurpfile['content'] | b64decode | regex_search('\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b') }}:53
While this works, I feel a bit uneasy as the solution is split into two places: the task does something, and then the template file does the second part. Is there a remote version of lookup that is a direct replacement?
Instead of slurp I'd rather fetch the remote files
- fetch:
src: /etc/resolv.conf
dest: "{{ fetched_files }}"
,declare a variable, e.g.
my_etc_recolv_conf: "{{ fetched_files }}/{{ inventory_hostname }}/etc/resolv.conf"
,and use it in the template
nameserver dns1 {{ lookup('file', my_etc_recolv_conf) | ...

How can I nest a with_filetree in Ansible?

I'm using Ansible to push config files for various apps (based on group_names) & need to loop thru the config .j2 templates from a list variable. If I use a known list of config templates I can use a standard with_nested like this...
template:
src: '{{ playbook_dir }}/templates/{{ item[1] }}/configs/{{ item[0] }}.j2'
dest: /path/to/{{ item[1] }}/configs/{{ item[0] }}
with_nested:
- ['file.1', 'file.2', 'file.3', 'file.4']
- '{{ group_names }}'
However, since each app will have its own configs I can't use a common list for a with_nested. Every attempt to somehow use with_filetree nested fails. Is there any way to nest a with_filetree? Am I missing something painfully obvious?
The most straightforward way to deal with this is probably to imbricate loops through an include. I take for granted that your app directory only contains .j2 files. Adapt if this is not the case.
In e.g. push_templates.yml
---
- name: Copy templates for group {{ current_group }}
template:
src: "{{ item.src }}"
dest: /path/to/{{ current_group }}/configs/{{ (item.src | splitext).0 | basename }}
with_filetree: "{{ playbook_dir }}/templates/{{ current_group }}"
# Or using the latest loop syntax
# loop: "{{ query('filetree', playbook_dir + '/templates/' + current_group) }}"
when: item.src is defined
Note: on the dest line, I am removing the last found extension of the file and getting its name only without the leading directory path. Check the ansible doc on filters for splitext and basename for more info
Then in your e.g. main.yml
- name: Copy templates for all groups
include_tasks: push_templates.yml
loop: "{{ group_names }}"
loop_control:
loop_var: current_group
Note the loop_var in the control section to disambiguate the possible item overlap in the included file. The var name is of course aligned with the one I used in the above included file. See the ansible loops documentation for more info.
An alternative approach to the above would be to construct your own data structure looping over your groups with set_fact and calling the filetree lookup on each iteration (see example above with the newer loop syntax), then loop over your custom data structure to do the job.

How to insert a line not immediately after a match using Ansible?

I'm trying to insert a line after 2 lines of match.
- name : Update compute node under ipsec group
lineinfile:
backup: yes
state: present
path: /root/multinode
insertafter: '\[ipsec:children\]'
line: "hostname"
file:
[ipsec:children]
control
Result:
[ipsec:children]
hostname
control
Desired Result:
[ipsec:children]
control
hostname
Basically I want to insert 1 line after the match, not in the immediate next line.
Please let me know how to do this.
An option would be to use blockinfile (see example below), or template. Module lineinfile works best with non-structured data.
This is primarily useful when you want to change a single line in a file only. ... check blockinfile if you want to insert/update/remove a block of lines in a file. For other cases, see the copy or template modules.
- hosts: localhost
gather_facts: no
vars:
ipsec_children_conf:
- "control"
- "hostname"
tasks:
- blockinfile:
path : /root/multinode
create: yes
block: |
[ipsec:children]
{% for conf_item in ipsec_children_conf %}
{{ conf_item }}
{% endfor %}
> cat /root/multinode
# BEGIN ANSIBLE MANAGED BLOCK
[ipsec:children]
control
hostname
# END ANSIBLE MANAGED BLOCK

Ansible items in separate files

Is it possible to have few .yml files and then read them as separate items for task?
Example:
- name: write templates
template: src=template.j2 dest=/some/path
with_items: ./configs/*.yml
I have found pretty elegant solution:
---
- hosts: localhost
vars:
my_items: "{{ lookup('fileglob', './configs/*.yml', wantlist=True) }}"
tasks:
- name: write templates
template: src=template.j2 dest=/some/path/{{ (item | from_yaml).name }}
with_file: "{{ my_items }}"
And then in template you have to add {% set item = (item | from_yaml) %} at the beginning.
Well, yes and no. You can loop over files and even use their content as variables. But the template module does not take parameters. There is an ugly workaround by using an include statement. Includes do take parameters and if the template task is inside the included file it will have access to them.
Something like this should work:
- include: other_file.yml parameters={{ lookup('file', item) | from_yaml }}
with_fileglob: ./configs/*.yml
And in other_file.yml then the template task:
- name: write template
template: src=template.j2 dest=/some/path
The ugly part here, beside the additional include, is that the include statement only takes parameters in the format of key=value. that's what you see in above task as parameters=.... parameters here has no special meaning, it just is the name of the variable with which the content of the file will be available inside the include.
So if your vars files have a variable foo defined, you would be able to access it in the template as {{ parameters.foo }}.

Ansible: Set variable to file content

I'm using the ec2 module with ansible-playbook I want to set a variable to the contents of a file. Here's how I'm currently doing it.
Var with the filename
shell task to cat the file
use the result of the cat to pass to the ec2 module.
Example contents of my playbook.
vars:
amazon_linux_ami: "ami-fb8e9292"
user_data_file: "base-ami-userdata.sh"
tasks:
- name: user_data_contents
shell: cat {{ user_data_file }}
register: user_data_action
- name: launch ec2-instance
local_action:
...
user_data: "{{ user_data_action.stdout }}"
I assume there's a much easier way to do this, but I couldn't find it while searching Ansible docs.
You can use lookups in Ansible in order to get the contents of a file, e.g.
user_data: "{{ lookup('file', user_data_file) }}"
Caveat: This lookup will work with local files, not remote files.
Here's a complete example from the docs:
- hosts: all
vars:
contents: "{{ lookup('file', '/etc/foo.txt') }}"
tasks:
- debug: msg="the value of foo.txt is {{ contents }}"
You can use the slurp module to fetch a file from the remote host: (Thanks to #mlissner for suggesting it)
vars:
amazon_linux_ami: "ami-fb8e9292"
user_data_file: "base-ami-userdata.sh"
tasks:
- name: Load data
slurp:
src: "{{ user_data_file }}"
register: slurped_user_data
- name: Decode data and store as fact # You can skip this if you want to use the right hand side directly...
set_fact:
user_data: "{{ slurped_user_data.content | b64decode }}"
You can use fetch module to copy files from remote hosts to local, and lookup module to read the content of fetched files.
lookup only works on localhost. If you want to retrieve variables from a variables file you made remotely use include_vars: {{ varfile }} . Contents of {{ varfile }} should be a dictionary of the form {"key":"value"}, you will find ansible gives you trouble if you include a space after the colon.

Resources