Ansible: Prompt for a Variable Only if it is Not Already Set

October 22nd, 2017 Permalink

Ansible's vars_prompt always did strike me as a strange design decision for a framework intended to automate deployment. Automation is all about removing the need for a human in the loop. Why then add ways to force user input?

---
- name: Provision
# ===============
  hosts: localhost
  connection: local
  gather_facts: yes

  vars_prompt:
    - name: example_var
      prompt: "Provide a value for example_var:"
      default: example_value
      private: no

Nonetheless, there it is. It seems to be widely used as an alternative to preliminary Bash scripts or other command line tooling among those groups who are in fact interested in user input as the primary way of defining a build. You can look at Streisand as an example of a system primarily aimed at less technically sophisticated end users, and which makes use of Ansible to organize questions and answers that will define the server provisioning details.

Rejected Approach #1: Satisfying Prompts via Piped Answers

What about the rest of us, however, who want systems to be quiet and read from config files? Faced with a vars_prompt that can't be changed, for whatever reason, there is always the fragile fallback of wrapping the Ansible invocation and passing in the answers you want to provide the various prompts. To do this, you have to know the order of the prompts, and write something like this:

# Sort out the answers to the various prompts.
cat > responses.txt <<EOF
example_value_1
y
example_value_2
n
EOF

# Feed the responses into the invocation of Ansible.
cat responses.txt | ansible-playbook "playbooks/example.yml"

This will break horribly, or worse, quietly, as soon as anyone changes the Ansible definitions without also changing the wrapper script appropriately, of course.

Rejected Approach #2: Optional Prompts using Pause and Register

A better course is to make the prompts optional. For example, only fire the prompt if the associated variable is not defined. Ansible makes this is a little challenging due to the order in which various activities take place. var_prompt occurs prior to gathering facts, so you can't check there to see if a variable is defined - it won't be defined yet regardless of your attempt to set it. An old, pre-Ansible-2.* standard approach was instead to use pause as shown here:

---
- name: Provision
# ===============
  hosts: localhost
  connection: local
  gather_facts: yes

  pre_tasks:
    - pause:
        prompt: "Provide a value for example_var. Enter defaults to 'example_value':"
      when: example_var is not defined
      register: example_var

    - name: Default for example_var
      set_fact:
        example_var: example_value
      when: example_var == ""

So now if you pass in a definition, no prompt will take place:

# Define variables. Prompts will be silenced by their existence.
cat > extra-vars.yml <<EOF
example_var: example_value
EOF

ansible-playbook \
  --extra-vars="@extra-vars.yml" \
  "playbooks/example.yml"

Another possibility is to set a single blanket force_noninteractive variable and have all uses of prompt respect it, which is probably simpler to manage in a larger Ansible codebase.

---
- name: Provision
# ===============
  hosts: localhost
  connection: local
  gather_facts: yes

  pre_tasks:
    - pause:
        prompt: "Provide a value for example_var_1. Enter defaults to 'example_value_1':"
      when: force_noninteractive is not defined
      register: example_var_1

    - name: Default for example_var_1
      set_fact:
        example_var: example_value_1
      when: example_var_1 == ""

    - pause:
        prompt: "Provide a value for example_var_2. Enter defaults to 'example_value_2':"
      when: force_noninteractive is not defined
      register: example_var_2

    - name: Default for example_var_2
      set_fact:
        example_var: example_value_2
      when: example_var_2 == ""
# Define variables. Prompts will be silenced by force_noninteractive.
cat > extra-vars.yml <<EOF
force_noninteractive: true
example_var_1: example_value
# Set this one to the default value by passing an empty string.
example_var_2: ""
EOF

ansible-playbook \
  --extra-vars="@extra-vars.yml" \
  "playbooks/example.yml"

Unfortunately the pause and register approach is now not so great in Ansible 2.*, as the user input is invisible. That makes it essentially useless for any text entry.

Desired Approach: Passing in Variables on the Command Line

The currently recommended approach is to put all of the variables requested in var_prompt blocks into extra-vars files and pass them on the command line. Any var_prompt variable that is defined already via the command line will not be prompted for. It is also possible to use the inventory to achieve the same outcome, but that quickly becomes clunky for any significant number of variables. No changes are needed to the playbooks, and you have the option to run Ansible either in an interactive mode without passing in additional variable definitions, or in a non-interactive mode with additional variable definitions provided via a config file.

# Run interactive, without passing in variables.
ansible-playbook "playbooks/example.yml"
# Define variables for the non-interactive run.
cat > extra-vars.yml <<EOF
example_var: example_value
EOF

# Run non-interactive, passing in variables.
ansible-playbook \
  --extra-vars="@extra-vars.yml" \
  "playbooks/example.yml"

What about prompts that are not intended to set variables, however? How to bypass these?

    - pause:
        prompt: "Press enter to continue:"

In this case, the best thing to do is change the runbook to wrap the prompt in a block keyed to a variable. Then pass in that variable to suppress the contents of the block from executing:

    - block
        pause:
          prompt: "Press enter to continue:"
      when: noninteractive is undefined
# Define variables for the non-interactive run.
cat > extra-vars.yml <<EOF
noninteractive: true
example_var: example_value
EOF

# Run non-interactive, passing in variables.
ansible-playbook \
  --extra-vars="@extra-vars.yml" \
  "playbooks/example.yml"