Multiple Set Facts not working with conditional set - ansible

I created roles for getting different passwords from CyberArk. I noticed that everything in the roles was the same except for the query parameter to find the password, so I decided to make the query parameter a variable, and I send it to a new single CyberArk role as a variable.
Example from Playbook:
- name: Get AVI Dev Or Prod password
import_role:
name: /Users/n0118883/python/ansible/roles/cyberark_creds
vars:
query_parm: "Username=admin;Address=avi;Environment=Development"
- name: Get Venafi password
import_role:
name: /Users/n0118883/python/ansible/roles/cyberark_creds
vars:
query_parm: "Username=sahsp-avi-venafi;Address=LM"
I was hoping to call the role, pass the query parameter, and use multiple "set_fact" with a when clause to have the correct password assigned to the correct fact.
Example from Role:
- name: Get PW from cyberark
cyberark_credential:
api_base_url: "https://cyberark.lmig.com"
app_id: "AVI_Cyberark_Automation"
query: "{{ query_parm }}"
register: cyberark_command_output
- set_fact:
admin_password: "{{ cyberark_command_output | json_query('result.Content')}}"
when: '"Address=avi" in query_parm'
- set_fact:
venafi_password: "{{ cyberark_command_output | json_query('result.Content')}}"
when: '"sahsp-avi-venaf" in query_parm'
- name: "Return"
debug:
msg: "{{ admin_password }}"
- name: "Return"
debug:
msg: "{{ venafi_password }}"
No matter what I change when I run the playbook, I either get the first password twice or I get the password for the first and the error "The error was: 'venafi_password' is undefined" for the second.
In one playbook, I go to CyberArk to get three different passwords. I'm not sure if I'm trying to do too much with a role and should go back to three separate roles, or is this possible, but I'm just doing it wrong.

Related

Ansible run script on all hosts, gather output and send once to an API

I have the following task set:
- name: Initialise inventory_data variable
set_fact:
inventory_data: ""
- name: Get Instance Inventory
remote_user: me
ansible.builtin.script: scripts/inventory.sh
register: inventory
- name: Set inventory variable
set_fact:
inventory_data: "{{ inventory_data }} {{ inventory.stdout_lines | join('\n')}}"
- name: Send to API
remote_user: me
ansible.builtin.uri:
url: https://myapi.com/endpoint
method: POST
body: "{{ inventory_data }}"
status_code: 200
The desired result is that i need to gather the results from inventory.sh and send them only once at the end of the run.
I've tried different variations, with run_once, delegate_to etc.. but i cannot seem to get this!
Edit:
I am trying to gather some data from my script which is ran on every host, however i wish to aggregate the results from all hosts, and send it once to an API.
First, if your play looks something like this:
- hosts: all
tasks:
- name: Initialise inventory_data variable
set_fact:
inventory_data: ""
- name: Get Instance Inventory
remote_user: me
ansible.builtin.script: scripts/inventory.sh
register: inventory
- name: Set inventory variable
set_fact:
inventory_data: "{{ inventory_data }} {{ inventory.stdout_lines | join('\n')}}"
It's not going to do you any good. Your inventory.sh script will run on each host, which will set the inventory variable for that host, and the subsequent task will append inventory.stdout_lines to inventory_data for that host. This won't collect the output from multiple hosts. You need to restructure your playbook. First, run the inventory script on each host:
- hosts: all
gather_facts: false
tasks:
- name: Get Instance Inventory
ansible.builtin.script: scripts/inventory.sh
register: inventory
Then in a second play targeting localhost, build your merged inventory variable and send the data to the API:
- hosts: localhost
gather_facts: false
tasks:
- name: create merged inventory
set_fact:
inventory_data: "{{ inventory_data + hostvars[item].inventory.stdout }}"
vars:
inventory_data: ""
loop: "{{ groups.all }}"
- name: Send to API
remote_user: me
ansible.builtin.uri:
url: https://myapi.com/endpoint
method: POST
body: "{{ inventory_data }}"
status_code: 200
This way, (a) you build the inventory_data variable correctly and (b) you only make a single API call.
I've made a complete, runnable example of this solution available here.

'gather_facts' seems to break 'set_fact' and 'hostvars'

I am using set_fact and hostvars to pass variables between plays within a playbook. My code looks something like this:
- name: Staging play
hosts: localhost
gather_facts: no
vars_prompt:
- name: hostname
prompt: "Enter hostname or group"
private: no
- name: vault
prompt: "Enter vault name"
private: no
- name: input
prompt: "Enter input for role"
private: no
tasks:
- set_fact:
target_host: "{{ hostname }}"
target_vault: "{{ vault }}"
for_role: "{{ input }}"
- name: Execution play
hosts: "{{ hostvars['localhost']['target_host'] }}"
gather_facts: no
vars_files:
- "vault/{{ hostvars['localhost']['target_vault'] }}.yml"
tasks:
- include_role:
name: target_role
vars:
param: "{{ hostvars['localhost']['for_role'] }}"
This arrangement has worked without issue for months. However, our environment has changed and now I need to take a timestamp and pass that to the role as well as the other variable, so I made the following changes (denoted by comments):
- name: Staging play
hosts: localhost
gather_facts: yes # Changed from 'no' to 'yes'
vars_prompt:
- name: hostname
prompt: "Enter hostname or group"
private: no
- name: vault
prompt: "Enter vault name"
private: no
- name: input
prompt: "Enter input for role"
private: no
tasks:
- set_fact:
target_host: "{{ hostname }}"
target_vault: "{{ vault }}"
for_role: "{{ input }}"
current_time: "{{ ansible_date_time.iso8601 }}" # Added fact for current time
- name: Execution play
hosts: "{{ hostvars['localhost']['target_host'] }}"
gather_facts: no
vars_files:
- "vault/{{ hostvars['localhost']['target_vault'] }}.yml"
tasks:
- include_role:
name: target_role
vars:
param: "{{ hostvars['localhost']['for_role'] }}"
timestamp: "{{ hostvars['localhost']['current_time'] # Passed current_time to
Execution Play via hostvars
Now, when I execute, I get the error that the vault hostvars variable is undefined in the Execution Play. After some experimenting, I've found that setting gather_facts: yes in the Staging Play is what is triggering the issue.
However, I need gather_facts enabled in order to use ansible_time_date. I've already verified via debug that the facts are being recorded properly and can be called by hostvars within the Staging Play; just not in the following Execution Play. After hours of research, I can't find any reasoning for why gathering facts in the Staging Play should affect hostvars for the Execution Play or any idea on how to fix it.
At the end of the day, all I need is the current time passed to the included role. Anyone who can come up with a solution that actually works in this use case wins Employee of the Month. Bonus points if you can explain the initial issue with gather_facts.
Thanks!
So, I had to reinvent the wheel a bit, but came up with a much cleaner solution. I simply created a default value for a timestamp in the role itself and added a setup call for date/time at the appropriate point, conditional on there being no existing value for the variable in question.
- name: Gather date and time.
setup:
gather_subset: date_time
when: timestamp is undefined and ansible_date_time is undefined
I was able to leave gather_facts set to no in the dependent playbook but I still have no idea why setting it to yes broke anything in the first place. Any insight in this regard would be appreciated.
... if you can explain the initial issue with gather_facts ... Any insight in this regard would be appreciated.
This is caused by variable precedence and because Ansible do not "overwrite or set a new value" for a variable. So it will depend on when and where they become defined.
You may test with the following example
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Show Gathered Facts
debug:
msg: "{{ hostvars['localhost'].ansible_facts }}" # will be {} only
- name: Gather date and time only
setup:
gather_subset:
- 'date_time'
- '!min'
- name: Show Gathered Facts
debug:
msg: "{{ ansible_facts }}" # from hostvars['localhost'] again
and "try to break it" by adding
- name: Set Fact
set_fact:
ansible_date_time:
date: '1970-01-01'
- name: Show Facts
debug:
msg: "{{ hostvars['localhost'] }}"
Just like to note that for your use case you should use
gather_subset:
- 'date_time'
- '!min'
since your are interested in ansible_date_time only. See what is the exact list of Ansible setup min?.
Be also aware of caching facts since "When created with set_facts’s cacheable option, variables have the high precedence in the play, but are the same as a host facts precedence when they come from the cache."

Ansible AWX/Tower - Access a variable saved as artifact from another play

I have a playbook which contains more than one plays. One of the plays generates a variable and stores it using the set_stats module as an artifact. The subsequent plays need to access the variable, but an error occurs that the given variable is undefined. How can I access a variable in the artifacts? (Btw using a workflow which would result in saving the variable in the extra_variables instead of the artifacts container is no option in this scenario)
The Problem in detail:
I have the following playbook which includes 2 plays which get executed on different hosts:
---
- hosts: ansible
roles:
- role_parse_strings
- hosts: all, !ansible
roles:
- role_setup_basics
- role_create_accounts
The role "role_parse_strings" in the first play generates the variable "users" which gets stored because of the set_stats module as an artifact. The following content lands in the artifact section of ansible awx:
users:
- username: user1
admin: true
- username: user2
admin: false
When the role "role_create_accounts" gets executed which tries to access the variable "users" in the following way...
- user: name={{ item.username }}
shell=/bin/bash
createhome=yes
groups=user
state=present
with_items: "{{ users }}"
..this error gets displayed:
{
"msg": "'users' is undefined",
"_ansible_no_log": false
}
You can use set_fact to share variable between hosts. Below example show how to share a file content via set_fact.
- hosts: host1
pre_tasks:
- name: Slurp the public key
slurp:
src: /tmp/ssh_key.pub
register: my_key_pub
- name: Save the public key
set_fact:
my_slave_key: >-
{{ my_key_pub['content'] | b64decode }}
- hosts: host2
vars:
slave_key: "{{ my_slave_key }}"
pre_tasks:
- set_fact:
my_slave_key: >-
{{ hostvars[groups["host1"][0]].my_slave_key | trim }}
We saved the content of public key as a fact name called my_slave_key and
assgined it another variable as slave_key in host2 with:
hostvars[groups["host1"][0]].my_slave_key

Adding users in ansible and enforcing password change, resets user's password when adding new user

I am having problems with Ansible and adding new users to servers.
Firstly I check if the users are present on the system and if not, then I proceed to create them.
I found a somewhat similar problem here: ansible user module always shows changed
and I was able to fix the changed status when adding a new user in the file userlist with adding a simple salt.
However, the last part, which is the handler, is always performed.
This is how my playbook looks like:
---
- hosts: all
become: yes
vars_files:
- /home/ansible/userlist
tasks:
# check if user exists in system, using the username
# and trying to search inside passwd file
- name: check user exists
getent:
database: passwd
key: "{{ item }}"
loop: "{{ users }}"
register: userexists
ignore_errors: yes
- name: add user if it does not exit
user:
name: "{{ item }}"
password: "{{ 'password' | password_hash('sha512', 'mysecretsalt') }}"
update_password: on_create
loop: "{{ users }}"
when: userexists is failed
notify: change password
handlers:
- name: change user password upon creation
shell: chage -d 0 "{{ item }}"
loop: "{{ users }}"
listen: change password
And here is the simple file called userlist:
users:
- testuser1
- testuser2
- testuser3
- testuser22
When I am running the playbook without changes to the userlist file, everything is fine.
However, if I add a new user to the file, then, when an existing user tries to log in, the system enforces them to change their password, because the handler is always called.
Is there any way to alter the code in a way that the enforcing of changing the password immediately is only performed on newly created users?
There are 2 main issues in your playbook:
userexists is registering each result individually, but you are referencing only the overall "failure" result. Use a debug statement to show the variable to see what I mean
If the handler is notified, you are looping on all users, rather than only those that have "changed" (i.e. have been created)
There's a couple of different approaches to fix this, but I think this might be the most succinct:
tasks:
- name: add user if it does not exist
user:
name: "{{ item }}"
password: "{{ 'password' | password_hash('sha512', 'mysecretsalt') }}"
update_password: on_create
loop: "{{ users }}"
register: useradd
notify: change password
handlers:
- name: change user password upon creation
shell: chage -d 0 {{ item.name }}
loop: "{{ useradd.results }}"
when: item.changed
listen: change password

How to create one common playbook-wide writeable variable in ansible?

I'm writing a playbook to create many user accounts across many servers. At the end I want to get output with credentials sorted by username.
I used set_fact with run_once but it seems that defined variable is not playbook-wide.
main.yml
- name: Create users
import_tasks: creation_task.yml
creation_task.yml
- name: Init variable for creds
set_fact:
creds: []
delegate_to: localhost
run_once: true
- name: Create specific users
include: create.yml
with_items:
- input_data
- .......
- name: Print output creds
debug: var=creds
run_once: true
create.yml
- name: some actions that actually create users
....
- name: add creds to list
set_fact:
creds: "{{ creds + [ {'hostname': inventory_hostname,'username':item.name,'password':password.stdout} ]}}"
- name: add splitter to list
set_fact:
creds: "{{ creds + [ '-----------------------------------------------------' ]}}"
This is actually working but i get output sorted by server because (as I think) every host reports his version of "creds" variable.
I'd like to create one variable that will be visible and writeable across all nested plays. So output would be sorted by input data but not hostname. Is it possible?
I'd use the following syntax, to fetch a variable set from a specific host via hostvars:
- debug:
msg: "{{ groups['my_host_name']|map('extract',hostvars,'my_variable_name')|list|first }}"
when: groups['my_host_name']|map('extract',hostvars,'my_variable_name')|list|first|length > 0
Then, you can loop over your hostnames to create an array of values and sort them.
Though, printing all servers hostnames, users & plain text passwords in a text file seems to be a security risk.

Resources