Create a constant from a lookup in Ansible - ansible

I am trying to create a variable from a lookup in Ansible. I then want to use that variable as a constant throughout the rest of the role. Here are a couple of lines from defaults/main.yml.
job_start_time: "{{ lookup('pipe','date +%Y%m%d_%H%M%S') }}"
new_virtual_machine_name: "{{ template_name }}-pkrclone-{{ job_start_time }}"
The result of running the role causes an error as the lookup happens again when the new_virtual_machine_name variable is used in tasks/main.yml. You can see the second increment in subsequent tasks.
TASK [update_vsphere_template : Write vsphere source file] changed: [localhost] => {"changed": true, "checksum": "c2ee7ed6fad0b26aea825233f3e714862de3ac28", "dest": "packer-DevOps-Windows2019-Base-DevOps-Windows2019-Base-pkrclone-20210819_**093122**/vsphere-source-DevOps-Windows2019-Base-DevOps-Windows2019-Base-pkrclone-20210819_**093122**.pkr.hcl", 148765781020136/source", "state": "file", "uid": 1000}
TASK [update_vsphere_template : Write the template update file] *fatal: [localhost]: FAILED! => {"changed": false, "checksum": "6fd97c1d2373b9c71ad6b5246529c46a38f55c48", "msg": "Destination directory packer-DevOps-Windows2019-Base-DevOps-Windows2019-Base-pkrclone-20210819_**093123** does not exist"}
Here is how I am referencing the new_virtual_machine_name variable in tasks/main.yml
- name: Write vsphere source file
template:
src: ../templates/vsphere-source.j2
dest: 'packer-{{template_name}}-{{ new_virtual_machine_name }}/vsphere-source-{{ template_name }}-{{ new_virtual_machine_name }}.pkr.hcl'
- name: Write the template update file
template:
src: ../templates/update-template.j2
dest: 'packer-{{template_name}}-{{ new_virtual_machine_name }}/update-{{ template_name }}-{{ new_virtual_machine_name }}.pkr.hcl'
How can I keep Ansible from running the lookup every time I reference the variable that is created from the lookup at the start of the job? Thank you.

Q: "How can I keep Ansible from running the lookup every time I reference the variable?"
A: Use strftime, you'll get the current date each time you call the filter, e.g.
- set_fact:
job_start_time: "{{ '%Y%m%d_%H%M%S'|strftime }}"
- debug:
msg: "pkrclone-{{ job_start_time }}"
- wait_for:
timeout: 3
- debug:
msg: "pkrclone-{{ job_start_time }}"
- set_fact:
job_start_time: "{{ '%Y%m%d_%H%M%S'|strftime }}"
- debug:
msg: "pkrclone-{{ job_start_time }}"
gives
msg: pkrclone-20210819_174748
msg: pkrclone-20210819_174748
msg: pkrclone-20210819_174753

Yes, variables are evaluated every time you call them. A way to work around this is to store a fact using set_facts which will be evaluated only at time it is set and stored as a static values for the host.
Edit: Note that, as pointed out in #vladimir's answer (and discussed in comments), using the pipe lookup to get such an info is not really a good practice when you have the strftime filter available in core Ansible. So I changed it in my below example.
To illustrate, the following playbook:
---
- hosts: localhost
gather_facts: false
vars:
_job_start_time: "{{ '%Y%m%d_%H%M%S'| strftime }}"
tasks:
- name: Force var into a fact that will be constant throughout play
set_fact:
job_start_time: "{{ _job_start_time }}"
- name: Wait a bit making sure time goes on
pause:
seconds: 1
- name: Show that fact is constant whereas inital var changes
debug:
msg: "var is: {{ _job_start_time }}, fact is: {{ job_start_time }}"
- name: Wait a bit making sure time goes on
pause:
seconds: 1
- name: Show that fact is constant whereas inital var changes
debug:
msg: "var is: {{ _job_start_time }}, fact is: {{ job_start_time }}"
Gives:
PLAY [localhost] ***********************************************************************************************************************************************************************************************************************
TASK [Force var into a fact that will be constant throughout play] *********************************************************************************************************************************************************************
ok: [localhost]
TASK [Wait a bit making sure time goes on] *********************************************************************************************************************************************************************************************
Pausing for 1 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]
TASK [Show that fact is constant whereas inital var changes] ***************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "var is: 20210819_172528, fact is: 20210819_172527"
}
TASK [Wait a bit making sure time goes on] *********************************************************************************************************************************************************************************************
Pausing for 1 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]
TASK [Show that fact is constant whereas inital var changes] ***************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "var is: 20210819_172529, fact is: 20210819_172527"
}
PLAY RECAP *****************************************************************************************************************************************************************************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Related

How to ignore specific errors in an Ansible task

If have an Ansible task, that can fails sometimes, due to some error during the creation of an user account. Especially if the user account is already in use and the user is logged in. If the task fails with a specific error message, like "user account in use" the play must continue. There is no need to fail then, but only on predefined error messages. The task looks like this.
- name: modify user
user:
state: "{{ user.state | default('present') }}"
name: "{{ user.name }}"
home: "{{ user_base_path }}/{{ user.name }}"
createhome: true
Since it's not a shell command, I cannot simply register a var and check the output of .rc. Also I don't get stderr or stdout, when i register a var and print it in debug mode. That was my first approach on check for the error message. I am running out of ideas, how to filter for a specific error and passing the task, but failing on everything else. ignore_errors: yes is not a good solution, because the task should fail in some cases.
As per ansible doc we get stdout and stderr as return fields.
I would suggest to use flag ignore_errors: yes and catch the return as per this example
---
- hosts: localhost
vars:
user:
name: yash
user_base_path: /tmp
tasks:
- name: modify user
user:
state: "{{ user.state | default('present') }}"
name: "{{ user.name }}"
home: "{{ user_base_path }}/{{ user.name }}"
createhome: true
register: user_status
ignore_errors: yes
- name: stdout_test
debug:
msg: "{{ user_status.err }}"
- name: Fail on not valid
fail:
msg: failed
when: '"user account in use" not in user_status.err'
Output:
PLAY [localhost] *************************************************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************************************
ok: [localhost]
TASK [modify user] ***********************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "err": "<main> attribute status: eDSPermissionError\n<dscl_cmd> DS Error: -14120 (eDSPermissionError)\n", "msg": "Cannot create user \"yash\".", "out": "", "rc": 40}
...ignoring
TASK [stdout_test] ***********************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "<main> attribute status: eDSPermissionError\n<dscl_cmd> DS Error: -14120 (eDSPermissionError)\n"
}
TASK [Fail on not valid] *****************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "failed"}
PLAY RECAP *******************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=1
You can use when, save the return value by the register and set_fact and then bet what will happen according to that value.

Ansible working with multiple loops in conditions

how can I add a condition to ansible task that is based on a loop, when the task itself also based on a loop?
For example, here's my code:
- hosts: all
gather_facts: False
vars:
current_version: 826
versions:
- 805
- 821
- 824
- 826
tasks:
- name: First Task
find:
paths: /Users/tomer/projects/personal/ansible/test
patterns: snapshot*
register: files
when:
- current_version == item
loop: "{{versions}}"
- name: Second task
set_fact:
test_work: "{{ true if item > 0 else false}}"
loop:
- "{{ files | json_query('results[*].matched') }}"
So far, this is working as expected.
The first task is looking for any file with the name snapshot if the current_version is matching one of the versions in the list.
The second task iterates over the dictionary result from the first task and based on each item it is setting the fact. (In my case, only one item has this attribute).
I wanted to run the second task, only when the first task did run, however, the changed status is always false, so this condition is not useful.
I wanted to add the same condition of current_version == item but I can't use item twice here.
Any idea how to achieve that?
The find command is not really going to change anything, it just queries the file system without doing any modification, so it will indeed always give you a false.
On the other hand, you can definitely use the skipped field of the item.
This said, I would simplify the loop on your set_fact, because there is no real need to use json_query here.
This task would do the job perfectly fine:
- set_fact:
test_work: "{{ item.matched > 0 }}"
loop: "{{ files.results }}"
when: item is not skipped
Another extra tip is to not do things like
true if condition_that_evaluates_to_true else false
But rather do right away
condition_that_evaluates_to_true
Here would be a made up example playbook
- hosts: all
gather_facts: no
tasks:
- find:
path: /tmp
pattern: dummy*
when: item == current_version
register: files
loop: "{{ versions }}"
vars:
current_version: 826
versions:
- 805
- 821
- 824
- 826
- debug:
msg: "{{ item.matched > 0 }}"
loop: "{{ files.results }}"
when: item is not skipped
loop_control:
label: "{{ item.item }}"
This would yield the result
PLAY [all] *******************************************************************************************************
TASK [find] ******************************************************************************************************
skipping: [localhost] => (item=805)
skipping: [localhost] => (item=821)
skipping: [localhost] => (item=824)
ok: [localhost] => (item=826)
TASK [debug] *****************************************************************************************************
skipping: [localhost] => (item=805)
skipping: [localhost] => (item=821)
skipping: [localhost] => (item=824)
ok: [localhost] => (item=826) => {
"msg": true
}
PLAY RECAP *******************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ansible loop giving warning found a duplicate dict key (when). Using last defined value only

I am trying to iterate over an array and assign the value to variables hooks_enabled, workflow_artifact_id, workflow_version, one by one in every iteration and perform a specific task (currently debug, later change to Helm install command).
Code:
---
- name: Executing Ansible Playbook
hosts: localhost
become: yes
become_user: someuser
pre_tasks:
- include_vars: global_vars.yaml
- name: Print some debug information
set_fact:
all_vars: |
Content of vars
--------------------------------
{{ vars | to_nice_json }}
tasks:
- name: Iterate over an array
set_fact:
hooks_enabled: '{{ array_item1_hooks_enabled }}'
workflow_artifact_id: '{{ array_item1_workflow_artifact_id }}'
workflow_version: '{{ array_item1_workflow_version }}'
when: "item == 'array_item1'"
set_fact:
hooks_enabled: '{{ array_item2_hooks_enabled }}'
workflow_artifact_id: '{{ array_item2_workflow_artifact_id }}'
workflow_version: '{{ array_item2_workflow_version }}'
when: "item == 'array_item2'"
with_items: "{{ array}}"
# Change debug with helm install command
- debug:
msg: " id= '{{ workflow_artifact_id }}'"
The issue I am facing is, only the last when is considered and others are skipped
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
[WARNING]: While constructing a mapping from /c/ansible-test/second.yaml, line 16, column 7, found a duplicate dict key (set_fact). Using last defined value only.
[WARNING]: While constructing a mapping from /c/ansible-test/second.yaml, line 16, column 7, found a duplicate dict key (when). Using last defined value only.
PLAY [Executing Ansible Playbook] *********************************************************************************************************************************************************************************
TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
ok: [localhost]
TASK [include_vars] ***********************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Print some debug information] *******************************************************************************************************************************************************************************
ok: [localhost]
TASK [Iterate over an array] **************************************************************************************************************************************************************************************
skipping: [localhost] => (item=array_item1)
ok: [localhost] => (item=array_item2)
skipping: [localhost] => (item=array_item3)
skipping: [localhost] => (item=array_item4)
skipping: [localhost] => (item=array_item5)
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": " id= 'algorithm-Workflow'"
}
PLAY RECAP ********************************************************************************************************************************************************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
How do I modify the block to enable all the when statement execute and later use helm install command to take the variables one by one.
I would go with a dynamic variable construction using the vars lookup.
Something along the lines of:
- set_fact:
hooks_enabled: "{{ lookup('vars', item ~ '_hooks_enabled') }}"
workflow_artifact_id: "{{ lookup('vars', item ~ '_workflow_artifact_id') }}"
workflow_version: "{{ lookup('vars', item ~ '_workflow_version') }}"
when: "item in ['array_item1', 'array_item2']"
with_items: "{{ array }}"

Syntax for retrieving variable using another variable

I'm trying to find the UUID of a VM based on it's hostname. I'm not sure what is missing in my syntax here, but I've tried several different methods. Here is the sample playbook I'm working with currently:
---
- name: Test taking snapshot by UUID
hosts: tst000.company.com
vars_files:
- vars/credentials.yml
tasks:
- name: Gather all registered virtual machines
vmware_vm_facts:
hostname: 'vcenter.company.com'
username: '{{ vcenter.username }}'
password: '{{ vcenter.password }}'
validate_certs: False
delegate_to: localhost
register: vmfacts
- debug:
var: vmfacts.virtual_machines.{{ ansible_facts['hostname'] }}.uuid
- set_fact:
vm_uuid: "{{ lookup('vars', 'vmfacts.virtual_machines.' + ansible_facts['hostname'] + '.uuid') }}"
Results are as follows:
Identity added: /opt/tmp/awx_2507_ithHYD/credential_3
(/opt/tmp/awx_2507_ithHYD/credential_3) Vault password:
PLAY [Test taking snapshot by UUID]
********************************************
TASK [Gathering Facts]
********************************************************* ok: [tst000.company.com]
TASK [Gather all registered virtual machines]
********************************** ok: [tst000.company.com -> localhost]
TASK [debug]
******************************************************************* ok: [tst000.company.com] => {
"vmfacts.virtual_machines.tst000.uuid": "421d2491-8896-e52f-e4f5-5118687ce0e9" }
TASK [set_fact]
**************************************************************** fatal: [tst000.company.com]: FAILED! => {"msg": "The task
includes an option with an undefined variable. The error was: No
variable found with this name:
vmfacts.virtual_machines.tst000.uuid\\n\\nThe error appears to
have been in '/var/lib/awx/projects/quick-stuff/test_snapshot.yml':
line 21, column 7, but may\\nbe elsewhere in the file depending on the
exact syntax problem.\\n\\nThe offending line appears to be:\\n\\n\\n
- set_fact:\\n ^ here\\n"}
PLAY RECAP
********************************************************************* tst000.company.com : ok=3 changed=0 unreachable=0
failed=1
In the debug and set_fact modules, you can see that the ansible_facts['hostname]' properly places the hostname as needed, however, it returns the proper value in the debug module, but is claiming there is no variable found by that name in the set_fact module. I'm not sure what is wrong with my syntax here.
There's no need to use the lookup in that way, since vars and its hostvars friend are effectively dicts:
- set_fact:
vm_uuid: "{{ vmfacts.virtual_machines[ansible_facts['hostname']].uuid }}"

How to generate single reusable random password with ansible

That is to say: How to evaluate the password lookup only once?
- name: Demo
hosts: localhost
gather_facts: False
vars:
my_pass: "{{ lookup('password', '/dev/null length=15 chars=ascii_letters') }}"
tasks:
- debug:
msg: "{{ my_pass }}"
- debug:
msg: "{{ my_pass }}"
- debug:
msg: "{{ my_pass }}"
each debug statement will print out a different value, e.g:
PLAY [Demo] *************
TASK [debug] ************
ok: [localhost] => {
"msg": "ZfyzacMsqZaYqwW"
}
TASK [debug] ************
ok: [localhost] => {
"msg": "mKcfRedImqxgXnE"
}
TASK [debug] ************
ok: [localhost] => {
"msg": "POpqMQoJWTiDpEW"
}
PLAY RECAP ************
localhost : ok=3 changed=0 unreachable=0 failed=0
ansible 2.3.2.0
Use set_fact to assign permanent fact:
- name: Demo
hosts: localhost
gather_facts: False
vars:
pwd_alias: "{{ lookup('password', '/dev/null length=15 chars=ascii_letters') }}"
tasks:
- set_fact:
my_pass: "{{ pwd_alias }}"
- debug:
msg: "{{ my_pass }}"
- debug:
msg: "{{ my_pass }}"
- debug:
msg: "{{ my_pass }}"
I've been doing it this way and never had an issue.
- name: Demo
hosts: localhost
gather_facts: False
tasks:
- set_fact:
my_pass: "{{ lookup('password', '/dev/null length=15 chars=ascii_letters') }}"
- debug:
msg: "{{ my_pass }}"
The lookup password is nice, but what if you have password specification, like it have to contain specific characters, or must not contain uppercase... the lookup also does not guarantee that the password will have special characters if needed to have...
I have ended with custom jinja filter, that might help somebody ( works fine for me :) )
https://gitlab.privatecloud.sk/vladoportos/custom-jinja-filters
The problem is that you are using the password module wrong, or at least according to the latest documentation (maybe this a new feature on 2.5):
Generates a random plaintext password and stores it in a file at a given filepath.
By definition,the lookup password generates a random password AND stores it on the specified path for subsequent lookups. So, first time it checks if the specified path exists, and if not generates a random password and stores it on that path, subsequent lookups will just retrieve it. Because you are using /dev/null as store path, you are forcing ansible to generate a new random password because everytime it checks for existence it finds nothing.
If you want to have a random password per host + client or whatever
all you need to do to is use some templating and set the store path based on those parameters.
For example:
---
- name: Password test
connection: local
hosts: localhost
tasks:
- name: create a mysql user with a random password
ansible.builtin.debug:
msg: "{{ lookup('password', 'credentials/' + item.host + '/' + item.user + '/mysqlpassword length=15') }}"
with_items:
- user: joe
host: atlanta
- user: jim
host: london
- name: Another task that uses the password of joe
ansible.builtin.debug:
msg: "{{ lookup('password', 'credentials/atlanta/joe/mysqlpassword length=15') }}"
- name: Another task that uses the password of jim
ansible.builtin.debug:
msg: "{{ lookup('password', 'credentials/london/jim/mysqlpassword length=15') }}"
And this is the task execution, as you can see, the three tasks are getting the right generated passwords:
TASK [Gathering Facts] ***********************************************************************************************
ok: [localhost]
TASK [create a mysql user with a random password] ********************************************************************
ok: [localhost] => (item={'user': 'joe', 'host': 'atlanta'}) => {
"msg": "niwPf4tk9HWHhNc"
}
ok: [localhost] => (item={'user': 'jim', 'host': 'london'}) => {
"msg": "dHJdg,OjOEqdyrW"
}
TASK [Another task that uses the password of joe] ********************************************************************
ok: [localhost] => {
"msg": "niwPf4tk9HWHhNc"
}
TASK [Another task that uses the password of jim] ********************************************************************
ok: [localhost] => {
"msg": "dHJdg,OjOEqdyrW"
}
This has the advantage that, even if you play fails and you have to re-execute you will not get the same previous random password,that you can then store on a key-chain or just delete them.

Resources