I would like to select a specific variable based on user input in an Ansible playbook. Specifically, I would like to ask for user input on a location of a server, and then execute a specific action based on the input.
This is the current ansible playbook:
- hosts: all
remote_user: root
gather_facts: True
vars:
loc1: "10.13.1.140"
loc2: "10.13.1.141"
loc3: "10.13.1.142"
vars_prompt:
- name: location
prompt: "Location of server? Input options: loc1/loc2/loc3"
private: no
tasks:
- name: Test connectivity to user selected location
wait_for: host={{ vars.location }} port=9999 delay=0 timeout=10 state=started
Output when running the playbook:
[root#ansmgtpr-labc01 cfengine]# ansible-playbook testpoo.yaml -i /tmp/test
SSH password:
Location of server? Input options: loc1/loc2/loc3: loc2
PLAY ***************************************************************************
TASK [setup] *******************************************************************
ok: [hostname.domain.com]
TASK [Test connectivity to user selected location] *****************************
fatal: [hostname.domain.com]: FAILED! => {"changed": false, "elapsed": 10, "failed": true, "msg": "Timeout when waiting for loc2:9999"}
PLAY RECAP *********************************************************************
hostname.domain.com : ok=1 changed=0 unreachable=0 failed=1
I would like to know how or the best way to link the read-in user input of the location with the actual value (IP address) of the location that is defined at the top in the variables section. Possibly eval or something else?
Your task is waiting for loc2, hence the message Timeout when waiting for loc2:9999.
Use host={{ vars[location] }} instead.
Compare the output of the following tasks:
tasks:
- name: Show the value user entered
debug: var=vars.location
- name: Use the entered value as an index
debug: var=vars[location]
Result (abbreviated):
TASK [Show the value user entered] *********************************************
ok: [localhost] => {
"vars.location": "loc2"
}
TASK [Use the entered value as an index] ***************************************
ok: [localhost] => {
"vars[location]": "10.13.1.141"
}
Related
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.
There are several computers on the network, on each of them you need to create a user with a specific login and password.
I create users like this:
vars_prompt:
- name: "user_name"
prompt: "User name"
private: no
- name: "user_password"
prompt: "Enter a password for the user"
private: yes
encrypt: "md5_crypt"
confirm: yes
salt_size: 7
tasks:
- name: "add new user"
user:
name: "{{user_name}}"
password: "{{user_password}}"
shell: /bin/bash
Since there are many computers I don’t want to run a playbook a huge number of times. Ideally, I would like to implement the input of the list of hosts (computers) and the list of users. Password, in principle, you can do the same everywhere.
Loop the task
tasks:
- name: "add new user"
user:
name: "{{ item.user_name }}"
password: "{{ item.user_password }}"
shell: /bin/bash
loop: "{{ my_users }}"
and put the variable(s) my_users to host_vars
my_users:
- user_name: user1
user_password: password1
- user_name: user2
user_password: password2
Put common users to group_vars.
See Variable precedence: Where should I put a variable?
Use Ansible Vault to encrypt the passwords.
Here is an example of what you can try. Adapt to your needs.
Note: if the list of users is different for each host, just execute the playbook several times. Implementing this as a promptable play in ansible will just be a total pain and merely unusable.
In the example below, test1 and test2 are pointing to 2 docker containers I added in my demo_inventory.yml.
all:
hosts:
test1:
ansible_connection: docker
test2:
ansible_connection: docker
The hosts you enter will need to be correctly known by ansible for this to work.
This is the demo playbook test.yml
---
- name: Gather needed information
hosts: localhost
vars_prompt:
- name: hosts_entry
prompt: Enter comma separated list of hosts to target
private: false
- name: users_entry
prompt: Enter comma separated list of users to create
private: false
- name: user_password
prompt: Enter initial password applied to all users
encrypt: md5_crypt
confirm: true
salt_size: 7
tasks:
- name: Create a dynamic whatever_group with entered hosts
add_host:
name: "{{ item | trim }}"
groups:
- whatever_group
loop: "{{ hosts_entry.split(',') }}"
- name: Create a list of host for later reuse. Will be scoped to localhost
set_fact:
users_list: "{{ users_entry.split(',') }}"
- name: Store password for later reuse as vars_prompt are limited to play
set_fact:
user_password: "{{ user_password }}"
- name: Do the actual work
hosts: whatever_group
tasks:
- name: Make sure users are present
user:
name: "{{ item | trim }}"
password: "{{ hostvars['localhost'].user_password }}"
shell: /bin/bash
loop: "{{ hostvars['localhost'].users_list }}"
I created a play on localhost to gather the info from vars_prompt. In this play, I used add_host to create a whatever_group dynamically. Note the use of split to create list from a string with comma seperated elements in the input and of trim to remove the leading/trailing spaces (if user entered them). Since vars_prompt are limited in scope to the current play, I also used set_fact to get the users list and the password for future use.
On the next play, I target the whatever_group and run the user task. Note that since set_fact used previously scoped the variables to localhost, we have to use the hostvars magic variable to get the relevant information for the user loop and the password.
Here is the example run
$ ansible-playbook -i demo_inventory.yml test.yml
Enter comma separated list of hosts to target: test1, test2
Enter comma separated list of users to create: user1, user2, user3
Enter initial password applied to all users:
confirm Enter initial password applied to all users:
PLAY [Gather needed information] ***************************************************************
TASK [Gathering Facts] *************************************************************************
ok: [localhost]
TASK [Create a dynamic whatever_group with entered hosts] **************************************
changed: [localhost] => (item=test1)
changed: [localhost] => (item= test2)
TASK [Create a list of host for later reuse. Will be scoped to localhost] **********************
ok: [localhost]
TASK [Store password for later reuse as vars_prompt are limited to play] ***********************
ok: [localhost]
PLAY [Do the actual work] **********************************************************************
TASK [Gathering Facts] *************************************************************************
ok: [test1]
ok: [test2]
TASK [Make sure users are present] *************************************************************
changed: [test2] => (item=user1)
changed: [test1] => (item=user1)
changed: [test2] => (item= user2)
changed: [test1] => (item= user2)
changed: [test2] => (item= user3)
changed: [test1] => (item= user3)
PLAY RECAP *************************************************************************************
localhost : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
In my Ansible git repo, I have a var file with contents like this
vault_users:
alex:
password: $6$PwhqORmvn$tXctAkh9RLs60ZFhn9Cxz/eLZEx1UhQkbDIoM6xWsk7M18TApDd9/b8CHJnEiaiQE2YJ8mqu6kvsGuImDt4dy/
danny:
password: $6$PwhqORmvn$tXctAkh9RLs60ZFhn9Cxz/eLZEx1UhQkbDIoM6xWsk7M18TApDd9/b8CHJnEiaiQE2YJ8mqu6kvsGuImDt4dy/
gary:
password: $6$PwhqORmvn$tXctAkh9RLs60ZFhn9Cxz/eLZEx1UhQkbDIoM6xWsk7M18TApDd9/b8CHJnEiaiQE2YJ8mqu6kvsGuImDt4dy/
Now, I want to check if the password hashes from this var file matches the ones from the /etc/shadow file on a remote server. I know it is possible to mix Ansible and a bash/python script to get what I want. I would like to know if it is possible to do this using pure Ansible playbooks only (no bash/python scripts) using the lookup plugin or some other Ansible feature.
You can use line in file to check if line has changed, register result and store it in another variable if lineinfile module returned "changed".
Unfortunately, due to this bug you can't simply use with_items and backrefs in lineinfile module to check if strings are valid, so i used a little include hack.
So we have a playbook called playbook.yml and task called checkpasswords.yml, let's explain each of them.
playbook.yml
- hosts: localhost
tasks:
# execute checkpasswords.yml for each user in vault_users dict
# and pass each user (or item) as {{ user }} variable to included task
- include: checkpasswords.yml user="{{ item }}"
with_items: "{{ vault_users }}"
- debug: msg="{{ changed_users|default([]) }}"
checkpasswords.yml
- name: check for user and hash
lineinfile:
dest: /etc/shadow
regexp: '{{ user }}:([^:]+):(.*)'
# replace sting with user:hashed_password:everything_that_remains
line: '{{ user }}:{{ vault_users[user].password }}:\2'
state: present
backrefs: yes
register: u
- name: changed users
set_fact:
# set changed_users list to [] if not present and add [user] element
# when user password has changed
changed_users: "{{ changed_users|default([]) + [user] }}"
when: u.changed
hashvars.yml
vault_users:
root:
password: "nothing to see here"
my_user:
password: "nothing here"
I included variables to hashvars.yml file and changed hashes for my_user and root inside it. So the result of executing this playbook will be something like output below, don't forget --check!
ansible-playbook playbook.yml -e #hashvars.yml --check
PLAY [localhost] ***************************************************************
TASK [setup] *******************************************************************
ok: [localhost]
TASK [include] *****************************************************************
included: /home/my_user/workspace/so/checkpasswords.yml for localhost
included: /home/my_user/workspace/so/checkpasswords.yml for localhost
TASK [check for user and hash] *************************************************
changed: [localhost]
TASK [changed users] ***********************************************************
ok: [localhost]
TASK [check for user and hash] *************************************************
changed: [localhost]
TASK [changed users] ***********************************************************
ok: [localhost]
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": [
"my_user",
"root"
]
}
PLAY RECAP *********************************************************************
localhost : ok=8 changed=2 unreachable=0 failed=0
Background information:
I need to dynamically set a variable on a set of hosts (web1) and then check the same on a different set of hosts. Once they match, I can perform further actions.
Code
My hosts file looks like this:
[web1]
web1.ttv.mydomain.com
[web1:vars]
primary_count=0
[web2]
web2.ttv.mydomain.com
[web2:vars]
secondary_count=0
[web]
web1
web2
And this is the playbook:
- hosts: web1
tasks:
- name: query primary servers
shell: psql -U widget widget -c 'SELECT COUNT(*) FROM test' -t
register: result
- set_fact: primary_count={{result.stdout}}
- hosts: web
tasks:
- name: retrieve variable from previous play
shell: echo hello
- debug: var=primary_count
This playbook produces the following results:
TASK [setup] *******************************************************************
ok: [web1.ttv.mydomain.com]
TASK [query primary servers] ****************************************************
changed: [web1.ttv.mydomain.com]
TASK [debug] *******************************************************************
ok: [web1.ttv.mydomain.com] => {
"primary_count": 0
}
TASK [set_fact] ****************************************************************
ok: [web1.ttv.mydomain.com]
PLAY ***************************************************************************
TASK [setup] *******************************************************************
ok: [web1.ttv.mydomain.com]
ok: [web2.ttv.mydomain.com]
TASK [retrieve variable from previous play] ************************************
changed: [web1.ttv.mydomain.com]
changed: [web2.ttv.mydomain.com]
TASK [debug] *******************************************************************
ok: [web2.ttv.mydomain.com] => {
"primary_count": "VARIABLE IS NOT DEFINED!"
}
ok: [web1.ttv.mydomain.com] => {
"primary_count": " 2"
}
Problem
Now I need a way to do the following in the second play:
run the same select statement on web2.ttv.mydomain.com
save the value to secondary_count variable
check if secondary_count matches the value of the "primary_count" on web1.mydomain.com. (Notice how right now, since I'm looping through more than just the web1 servers in play 2, I get an error about the "primary_count" not being defined on web2 servers.)
when the values match then restart various services on secondary
Questions:
How do I evaluate the "primary_count" variable on the web1 host with the matching web2 host name on? In the future my hosts file will look like this:
[web1]
web1.ttv.mydomain.com
web1.ttx.mydomain.com
[web2]
web2.ttv.mydomain.com
web2.ttx.mydomain.com
[web]
web1
web2
So I need to write some sort of an eval statement that does this:
(pseudocode)
while looping through ***ALL*** web servers
if primary_count on web1.ttv.mydomain.com matches secondary_count on web2.ttx.mydomain.com then
restart service x on web2.ttx.mydomain.com
else
wait a few seconds and repeat
end
end loop
I think the solution lies with my hosts / inventory file. Somehow I need this playbook to run on all web1 servers and all web2 servers... but I also need a way to associate web1.ttv with just web2.ttv and web1.ttx with just web2.ttx and so on.
I'm just learning ansible as I go along, so if this approach is entirely wrong, please let me know!
Thanks.
EDIT 1
On doing some research about group_vars, it looks like group_vars doesn't really help me because I still have the same problem. While looping through all web servers (play 2), the variables I set on web1 servers in play 1 are not visible from web2 servers.
EDIT 2:
- hosts: web1
tasks:
- name: query primary servers
shell: psql -U widget widget -c 'SELECT COUNT(*) FROM widget' -t
register: result
- local_action: shell echo {{ result.stdout }} > varacrossplay.txt
That fails on the local_action line with this error:
fatal: [web1.ttv.mydomain.com -> localhost]: FAILED! => {"changed": true, "cmd": "echo 2 > varacrossplay.txt", "delta": "0:00:00.001641", "end":
": "echo 2 > varacrossplay.txt", "_uses_shell": true, "chdir": null, "creates": null, "executable": null, "removes": null, "warn": true}, "mod
1: cannot create varacrossplay.txt: Permission denied", "stdout": "", "stdout_lines": [], "warnings": []}
Try with this example playbook:
[jenkins#batman ansible]$ cat testplaybook.yml
- hosts: web1
tasks:
- name: query primary servers
shell: echo "TEST"
register: result
- local_action: shell echo {{ result.stdout }} > varacrossplay.txt
- hosts: web
tasks:
- local_action: shell cat varacrossplay.txt
register: result
- set_fact: other_fact="{{ result.stdout }}"
- debug: var=other_fact
With my servers all works fine xD
[jenkins#batman ansible]$ ansible-playbook -i inventory testplaybook.yml
PLAY ***************************************************************************
TASK [setup] *******************************************************************
ok: [10.0.0.100]
TASK [query primary servers] ***************************************************
changed: [10.0.0.100]
TASK [command] *****************************************************************
changed: [10.0.0.100 -> localhost]
PLAY ***************************************************************************
TASK [setup] *******************************************************************
ok: [10.0.0.2]
ok: [10.0.0.1]
TASK [command] *****************************************************************
changed: [10.0.0.1 -> localhost]
changed: [10.0.0.2 -> localhost]
TASK [set_fact] ****************************************************************
ok: [10.0.0.1]
ok: [10.0.0.2]
TASK [debug] *******************************************************************
ok: [10.0.0.2] => {
"other_fact": "TEST"
}
ok: [10.0.0.1] => {
"other_fact": "TEST"
}
PLAY RECAP *********************************************************************
10.0.0.100 : ok=3 changed=2 unreachable=0 failed=0
10.0.0.1 : ok=4 changed=1 unreachable=0 failed=0
10.0.0.2 : ok=4 changed=1 unreachable=0 failed=0
I'm trying to access the variable called "count" from the first "play" in my playbook in the second playbook. I found some other posts here about the same issue and I thought I was following the right steps, but the code below is still failing.
The Code
- hosts: group1
tasks:
- name: count registrations on primary node
shell: psql -U widgets widgets -c 'SELECT COUNT(*) FROM location' -t
register: count
- debug: var=count.stdout
- hosts: group2
tasks:
#the line below works...
# - debug: msg={{ hostvars['myserver1.mydomain.com']['count']['stdout'] }}
# but this one fails
- debug: msg={{ hostvars['group1']['count']['stdout'] }}
This produces the following output:
PLAY ***************************************************************************
TASK [setup] *******************************************************************
ok: [myserver1.mydomain.com]
TASK [count registrations on node] **************************************
changed: [myserver1.mydomain.com]
TASK [debug] *******************************************************************
ok: [myserver1.mydomain.com] => {
"count.stdout": " 2"
}
PLAY ***************************************************************************
TASK [setup] *******************************************************************
ok: [myserver2.mydomain.com]
TASK [debug] *******************************************************************
fatal: [myserver1.mydomain.com]: FAILED! => {"failed": true, "msg": "'ansible.vars.hostvars.HostVars object' has no attribute 'can_sip1'"}
NO MORE HOSTS LEFT *************************************************************
[ERROR]: Could not create retry file 'playbooks/test.retry'. The error was: [Errno 13] Permission denied: 'playbooks/test.retry'
PLAY RECAP *********************************************************************
myserver1.mydomain.com : ok=3 changed=1 unreachable=0 failed=0
myserver2.mydomain.com : ok=1 changed=0 unreachable=0 failed=1
The other post that I referring to is found here:
How do I set register a variable to persist between plays in ansible?
It's probably something simple, but I can't see where the bug lies.
Thanks.
EDIT 1
I've also tried to use set_fact like this:
- hosts: group1
tasks:
- name: count registrations on primary node
shell: psql -U widget widget -c 'SELECT COUNT(*) FROM location' -t
register: result
- debug: var=result.stdout
- set_fact: the_count=result.stdout
- debug: var={{the_count}}
- hosts: group2
tasks:
- name: retrieve variable from previous play
shell: echo hello
- debug: var={{hostvars}}
The results I get are:
PLAY ***************************************************************************
TASK [setup] *******************************************************************
ok: [myserver1.mydomain.com]
TASK [count reg on primary] ****************************************************
changed: [myserver1.mydomain.com]
TASK [debug] *******************************************************************
ok: [myserver1.mydomain.com] => {
"result.stdout": " 2"
}
TASK [set_fact] ****************************************************************
ok: [myserver1.mydomain.com]
TASK [debug] *******************************************************************
ok: [myserver1.mydomain.com] => {
"result.stdout": " 2"
}
PLAY ***************************************************************************
TASK [setup] *******************************************************************
ok: [myserver2.mydomain.com]
TASK [retrieve variable from previous play] ************************************
changed: [myserver2.mydomain.com]
TASK [debug] *******************************************************************
ok: [myserver2.mydomain.com] => {
"<ansible.vars.hostvars.HostVars object at 0x7f3b6602b290>": "VARIABLE IS NOT DEFINED!"
}
PLAY RECAP *********************************************************************
myserver1.mydomain.com : ok=5 changed=1 unreachable=0 failed=0
myserver2.mydomain.com : ok=3 changed=1 unreachable=0 failed=0
So It looks like there are no objects in the hostvars...
EDIT 3
This is what the playbook looks like this morning.
- hosts: group1
tasks:
- name: count reg on primary
shell: psql -U widgets widgets -c 'SELECT COUNT(*) FROM location' -t
register: result
- debug: var=result.stdout
- set_fact: the_count={{result.stdout}}
- debug: var={{the_count}}
- hosts: group2
tasks:
- name: retrieve variable from previous play
shell: echo hello
- debug: var={{hostvars}}
The "debug: var={{the_count}}" line from the first play prints out the correct value for the count but it also says the VARIABLE IS NOT DEFINED... like so:
TASK [set_fact] ****************************************************************
task path: /etc/ansible/playbooks/test.yml:8
ok: [myserver1.mydomain.com] => {"ansible_facts": {"the_count": " 2"}, "changed": false, "invocation": {"module_args": {"the_count": " 2"}, "module_name": "set_fact"}}
TASK [debug] *******************************************************************
task path: /etc/ansible/playbooks/test.yml:10
ok: [myserver1.mydomain.com] => {
" 2": "VARIABLE IS NOT DEFINED!"
}
And then once I hit the second play, I still get the message
TASK [debug] *******************************************************************
task path: /etc/ansible/playbooks/test.yml:16
ok: [myserver2.mydomain.com] => {
"<ansible.vars.hostvars.HostVars object at 0x7fb077fdc310>": "VARIABLE IS NOT DEFINED!"
}
In your example, you are suggestion that I use "debug: var={{hostlers}}". If you can clarify that for me please. It looks like it's a typo.
EDIT 4:
If you take a look at Edit 3 carefully, you will see that I have implemented "debug:var={{hostvars}}" as you suggest in your answer. But it gives me the same error that the variable is not defined.
I'm not just trying to pass variables from one play to another.. but from one set of hosts to another. Notice how play 1 uses group1 and play two applies only to group2.
Register variables, like facts, are per host. The values can differ depending on the machine. So you can only use host/ip defined in the inventory as key, not the group name. I think you have already knowed this, as you marked this in code snippet 1.
In the code snippet 2, the set_fact line (- set_fact: the_count=result.stdout) actually set the key the_count to the text value result.stdout, since result.stdout is treated as plain text, not a variable. If you want to treat it as a variable, you'd better use {{ result.stdout }}. You can verify this via running the playbook with -v option.
Tasks:
set_fact: the_content1=content.stdout
set_fact: the_content2={{ content.stdout }}
Output:
TASK [set_fact] ****************************************************************
ok: [192.168.1.58] => {"ansible_facts": {"the_content1": "content.stdout"}, "changed": false}
TASK [set_fact] ****************************************************************
ok: [192.168.1.58] => {"ansible_facts": {"the_content2": "hello world"}, "changed": false}
The debug module has two possible parameter: var and msg. The var parameter expect a variable name.
debug: var={{hostvars}}
In this line, first of all, Ansible extracts the value of hostvars, since it is enclosed with two brackets. Secondly, it tries to find a variable whose name is the value of hostvars, since var parameter expects a variable name directly. That is why you see the following strange output. This means Ansible couldn't find a variable whose name is <ansible.vars.hostvars.HostVars object at 0x7f3b6602b290>.
"<ansible.vars.hostvars.HostVars object at 0x7f3b6602b290>": "VARIABLE IS NOT DEFINED!"
You can use the following:
debug: var=hostvars
debug: msg={{hostvars}}
References:
Register variables don't survive across plays with different hosts
set_fact - Set host facts from a task
debug - Print statements during execution