Generate Ansible template variable only once - ansible

Context
I have an Ansible template that creates a configuration file. Most variables used by this template come from some stable source, such as variables defined in the playbook. However one variable contains a secret key which somehow needs to be generated if it does not already exist.
I want Ansible to generate the key on an as-needed basis. I do not want the key to be stored on the OS running Ansible, like is done with the password module. And naturally I want idempotence.
I currently have a working solution but I'm not happy with it, hence my question here. My current solution uses an extra file which is include by the configuration file created by my template. This is possible as it is a PHP file. I have a task using the shell module that generates the secret when this extra file does not exist, and then another that then creates it using the registered variable.
- name: Generate secret key
shell: openssl rand -hex 32
register: secret_key_command
when: not SecretKey.stat.exists
- name: Create SecretKey.php
template:
src: "SecretKey.php.j2"
dest: "{{ some_dir }}/SecretKey.php"
when: not SecretKey.stat.exists
Surely there is a less contrived way to do this?
Question
Is there a nice way to have a variable that gets generated only once in an Ansible template?

I am not sure, but I understood correctly, we want to generate your template if it doesn't already exists. So you can just do as follow:
- name: Create SecretKey.php
template:
src: "SecretKey.php.j2"
dest: "{{ some_dir }}/SecretKey.php"
force: no
force: no tells to don't overwrite a file if it already exists. No need to do extra check.

Related

Are Ansible patterns a feature of YAML?

Ansible patterns is a useful feature when targeting a playbook for a subset hosts of an inventory.
Here is an example:
---
- hosts: all
tasks:
- name: create a file on a remote machine
file:
dest: /tmp/file
state: '{{file_state}}'
- hosts: web
tasks:
- name: create file on web machines
file:
dest: /tmp/web-file
state: '{{file_state}}'
- hosts: all:!db
tasks:
- name: create file on web machines
file:
dest: /tmp/web-not-db-file
state: '{{file_state}}'
- hosts: all:&backup:!web
tasks:
- name: create file on web machines
file:
dest: /tmp/backup-file
state: '{{file_state}}'
Credits: Robert Starmer
So far, I know Ansible utilizes YAML and Jinja2 for parsing its configuration files.
Ansible patterns are not using Jinja2 because they are not surrounded by {{ }} blocks.
Now my question is, are they a custom modification done by Ansible itself?
If yes, is using such modifications documented by Ansible anywhere?
Now my question is, are they a custom modification done by Ansible itself? If yes, is using such modifications documented by Ansible anywhere?
I think your question may be based on some fundamental misunderstandings.
YAML is just way of encoding data structures -- such as dictionaries and lists -- in a text format, so that it's easy (-ish) for people to read/edit them. What an application does with the data presented in a YAML file is entirely application dependent.
So when you write something like:
- hosts: all
This does "mean" anything in YAML other than "here's a list of one item, which is a dictionary with a single key hosts with the value all".
Of course, when Ansible reads a playbook and finds a top-level list, it knows that this is a play, and it expects the play to have a hosts key. It interprets the value of the hosts key using the ansible pattern logic.
Any other application could read that YAML file and decide to do something entirely different with the content.
you're looking for the host pattern documentation:
https://docs.ansible.com/ansible/latest/user_guide/intro_patterns.html#intro-patterns
edit: I did not notice you already linked to this page.
Yes, this is neither yaml nor jinja2, and it's fully documented on the page linked, so I'm not sure we need more than that.

Save vars back in a file automatically after including and modifying it

Is there anything like save_vars in Ansible that would automatically save modified var/fact imported with include_vars?
The following code serves the purpose but it is not an elegant solution.
# vars/my_vars.json
{
"my_password": "Velicanstveni",
"my_username": "Franjo"
}
# playbook.yaml
---
- hosts: localhost
gather_facts: no
tasks:
- name: fetch vars
include_vars:
file: my_vars.json
name: playbook_var
- name: change var
set_fact:
playbook_var: "{{ playbook_var|combine({'my_username': 'Njofra' }) }}"
- name: save var to file
copy:
content: "{{ playbook_var | to_nice_json }}"
dest: vars/my_vars.json
I was wondering if there is an option in Ansible we can turn on so that whenever the var/fact is changed, during the playbook execution, this is automatically updated in the file var was imported from. The background problem I am trying to solve is having global variables in Ansible. The idea is to save global vars in a file. Ansible has system of hostavars which is collection of per-hostname variables but not all data is always host-related.
I think a fact cache will plus or minus do that, although it may have some sharp edges to getting it right, since that's not exactly the problem it is trying to solve. The json cache plugin sounds like it is most likely what you want, but there is a yaml cache plugin, too.
The bad news is that, as far as I can tell, one cannot configure those caching URIs via the playbook, rather one must use either the environment variable or an ansible.cfg setting.

Using different files for different hosts

I am using Ansible (with Ansible Tower) and want to create a task which runs a test.
I have a properties file for (SoapUI tests) which is of the form key=value. The value will change according to the host that the test is running on.
Although there is a way to specify different variables per host there does not seem to be a way to specify different files for different hosts.
The best way there seems to be (to my knowledge) is to create a template (http://docs.ansible.com/ansible/template_module.html) instead of the properties file and populate the template values using host variables. Is there an alternative or better way?
You can simply set the file path to be a variable.
So if you have a task that is copying a file to the box that looks like:
- name: Copy properties file to box
copy:
src: path/to/file
dest: path/to/dest
You could simply change this to be:
- name: Copy properties file to box
copy:
src: "{{ properties_file }}"
dest: path/to/dest
And then you simply need to set your properties_file variable for the appropriate environment group or host like so:
group_vars/dev:
properties_file: path/to/dev/file
group_vars/prod:
properties_file: path/to/prod/file

Is it possible to use inline templates?

I need to create a single file with the contents of a single fact in Ansible. I'm currently doing something like this:
- template: src=templates/git_commit.j2 dest=/path/to/REVISION
My template file looks like this:
{{ git_commit }}
Obviously, it'd make a lot more sense to just do something like this:
- inline_template: content={{ git_revision }} dest=/path/to/REVISION
Puppet offers something similar. Is there a way to do this in Ansible?
Another option to the lineinfile module (as given by udondan's answer) would be to use the copy module and specify the content rather than a source local to the Ansible host.
An example task would look something like:
- name: Copy commit ref to file
copy:
content: "{{ git_commit }}"
dest: /path/to/REVISION
I personally prefer this to lineinfile as for me lineinfile should be for making slight changes to files that are already there where as copy is for making sure a file is in a place and looking exactly like you want it to. It also has the benefit of coping with multiple lines.
In reality though I'd be tempted to make this a template task and just have a the template file be:
"{{ git_commit }}"
Which gets created by this task:
- name: Copy commit ref to file
template:
src: path/to/template
dest: /path/to/REVISION
It's cleaner and it's using modules for exactly what they are meant for.
Yes, in that simple case it is possible with the lineinfile module.
- lineinfile:
dest=/path/to/REVISION
line="{{ git_commit }}"
regexp=".*"
create=yes
The lineinfile module usually is used to ensure that a specific line is contained inside a file. The create=yes option will crete the file if it does not exist. The regexp=.* option makes sure you do not add content to the file if git_commit changes, because it would by default simply make sure the new content is added to the file and not replace the previous content.
This only works since you only have one line in your file. If you'd had more lines this obviously would not work with this module.
This issue seems to be resolved. However, if the template file was more than one variable, i.e. a json file, it is possible to use copy module with content parameter, with a lookup, i.e.:
# playbook.yml
---
- name: deploy inline template
copy:
content: '{{ lookup("template", "inlinetemplate.yml.j2") }}'
dest: /var/tmp/inlinetempl.yml
# inlinetemplate.yml.j2
---
- name: {{ somevar }}
abc: def
If you need insert the template to the exist file, you can insert through the lineinfile module.
- name: Insert jinja2 template to the file
lineinfile:
path: /path/file.conf
insertafter: "after this line"
line: "{{ lookup('template', 'template.conf.j2') }}"

Finding file name in files section of current Ansible role

I'm fairly new to Ansible and I'm trying to create a role that copies a file to a remote server. The local file can have a different name every time I'm running the playbook, but it needs to be copied to the same name remotely, something like this:
- name: copy file
copy:
src=*.txt
dest=/path/to/fixedname.txt
Ansible doesn't allow wildcards, so when I wrote a simple playbook with the tasks in the main playbook I could do:
- name: find the filename
connection: local
shell: "ls -1 files/*.txt"
register: myfile
- name: copy file
copy:
src="files/{{ item }}"
dest=/path/to/fixedname.txt
with_items:
- myfile.stdout_lines
However, when I moved the tasks to a role, the first action didn't work anymore, because the relative path is relative to the role while the playbook executes in the root dir of the 'roles' directory. I could add the path to the role's files dir, but is there a more elegant way?
It looks like you need access to a task that looks up information locally, and then uses that information as input to the copy module.
There are two ways to get local information.
use local_action:. That's shorthand for running the task agains 127.0.0.1, more info found here. (this is what you've been using)
use a lookup. This is a plugin system specifically designed for getting information locally. More info here.
In your case, I would go for the second method, using lookup. You could set it up like this example:
vars:
local_file_name: "{{ lookup('pipe', 'ls -1 files/*.txt') }}"
tasks:
- name: copy file
copy: src="{{ local_file_name }}" dest=/path/to/fixedname.txt
Or, more directly:
tasks:
- name: copy file
copy: src="{{ lookup('pipe', 'ls -1 files/*.txt') }}" dest=/path/to/fixedname.txt
With regards to paths
the lookup plugin is run from the context of the task (playbook vs role). This means that it will behave differently depending on where it's used.
In the setup above, the tasks are run directly from a playbook, so the working dir will be:
/path/to/project -- this is the folder where your playbook is.
If you where to add the task to a role, the working dir would be:
/path/to/project/roles/role_name/tasks
In addition, the file and pipe plugins run from within the role/files folder if it exists:
/path/to/project/roles/role_name/files -- this means your command is ls -1 *.txt
caveat:
The plugin is called every time you access the variable. This means you cannot trust debugging the variable in your playbook, and then relying on the variable to have the same value when used later in a role!
I do wonder though, about the use-case for a file that resides inside a projects ansible folders, but who's name is not known in advance. Where does such a file come from? Isn't it possible to add a layer in between the generation of the file and using it in Ansible... or having a fixed local path as a variable? Just curious ;)
Just wanted to throw in an additional answer... I have the same problem as you, where I build an ansible bundle on the fly and copy artifacts (rpms) into a role's files folder, and my rpms have versions in the filename.
When I run the ansible play, I want it to install all rpms, regardless of filenames.
I solved this by using the with_fileglob mechanism in ansible:
- name: Copy RPMs
copy: src="{{ item }}" dest="{{ rpm_cache }}"
with_fileglob: "*.rpm"
register: rpm_files
- name: Install RPMs
yum: name={{ item }} state=present
with_items: "{{ rpm_files.results | map(attribute='dest') | list }}"
I find it a little bit cleaner than the lookup mechanism.

Resources