ansible Iterate over list in list - ansible

I'm trying to adopt the task in the last answer from this post Ansible - managing multiple SSH keys for multiple users & roles
My variable looks like:
provisioning_user:
- name: ansible
state: present
ssh_public_keyfiles:
- ansible.pub
- user.pub
- name: foo
state: present
ssh_public_keyfiles:
- bar.pub
- key.pub
and my code looks like
- name: lookup ssh pubkeys from keyfiles and create ssh_pubkeys_list
set_fact:
ssh_pubkeys_list: "{{ lookup('file', item.ssh_public_keyfiles) }}"
loop: "{{ provisioning_user }}"
register: ssh_pubkeys_results_list
I want to store keys under the files directory and assign them with the variable to different users so that when a key changes, i only have to change the file and run the playbook instead of changing it in any hostvars file where the old key is used. But I get the following error and dont know how to solve it. I want to do this for every user defined in provisioning_user
fatal: [cloud.xxx.xxx]: FAILED! =>
msg: 'An unhandled exception occurred while running the lookup plugin ''file''. Error was a <class ''AttributeError''>, original message: ''list'' object has no attribute ''startswith''. ''list'' object has no attribute ''startswith'''
Can anyone please help me?

The file lookup reads the content of a single file, but you're passing it a list. That's not going to work, and is the direct cause of the error message you've reported.
You're also using both set_fact and register in the same task, which doesn't make much sense: the whole point of a set_fact task is to create a new variable; you shouldn't need to register the result.
Your life is going to be complicated by the fact that each user can have multiple key files. We need to build a data structure that maps each user name to a list of keys; we can do that like this:
- name: lookup ssh pubkeys from keyfiles
set_fact:
pubkeys: >-
{{
pubkeys |
combine({
item.0.name: pubkeys.get(item.0.name, []) + [lookup('file', item.1)]})
}}
vars:
pubkeys: {}
loop: "{{ provisioning_user|subelements('ssh_public_keyfiles') }}"
This creates the variable pubkeys, which is a dictionary that maps usernames to keys. Assuming that our provisioning_user variable looks like this:
provisioning_user:
- name: ansible
state: present
ssh_public_keyfiles:
- ansible.pub
- user.pub
- name: foo
state: present
ssh_public_keyfiles:
- bar.pub
- key.pub
- name: bar
state: present
ssh_public_keyfiles: []
After running the above task, pubkeys looks like:
"pubkeys": {
"ansible": [
"ssh-rsa ...",
"ssh-rsa ..."
],
"foo": [
"ssh-rsa ...",
"ssh-rsa ..."
]
}
We can use pubkeys in our authorized_keys task like this:
- name: test key
authorized_key:
user: "{{ item.name }}"
key: "{{ '\n'.join(pubkeys[item.name]) }}"
comment: "{{ item.key_comment | default('managed by ansible') }}"
state: "{{ item.state | default('true') }}"
exclusive: "{{ item.key_exclusive | default('true') }}"
key_options: "{{ item.key_options | default(omit) }}"
manage_dir: "{{ item.manage_dir | default('true') }}"
loop: "{{ provisioning_user }}"
when: item.name in pubkeys
I think your life would be easier if you were to rethink how you're managing keys. Instead of allowing each user to have a list of multiple key files, have a single public key file for each user -- named after the username -- that may contain multiple public keys.
That reduces your provisioning_user data to:
provisioning_user:
- name: ansible
state: present
- name: foo
state: present
- name: bar
state: present
And in our files/ directory, we have:
files
├── ansible.keys
└── foo.keys
You no longer need the set_fact task at all, and the authorized_keys task looks like:
- name: test key
authorized_key:
user: "{{ item.name }}"
key: "{{ keys }}"
comment: "{{ item.key_comment | default('managed by ansible') }}"
state: "{{ item.state | default('true') }}"
exclusive: "{{ item.key_exclusive | default('true') }}"
key_options: "{{ item.key_options | default(omit) }}"
manage_dir: "{{ item.manage_dir | default('true') }}"
when: >-
('files/%s.keys' % item.name) is exists
vars:
keys: "{{ lookup('file', '%s.keys' % item.name) }}"
loop: "{{ provisioning_user }}"
Note that in the above the when condition requires an explicit path, while the file lookup will implicitly look in the files directory.
These changes dramatically simplify your playbook.

Related

Create only those users from a list that do not exist

I have a list of users and I only want to create those, which do not exist on the system.
This is what I have tried:
- name: Connection to Unix server
hosts: localhost
vars:
USER_ID_details:
- user_id: my_user1
groups: wheel
real_full_name: my_user_name1
affected_host: localhost
email_id: my_user1#ibm.com
- user_id: my_user2
groups: wheel
real_full_name: my_user_name2
affected_host: localhost
email_id: my_user2#ibm.com
tasks:
- name: check for the ID is present
#shell: "id {{ item.user_id }}"
shell: grep "{{ item.user_id }}" /etc/passwd | awk -F":" '{print $1}'
loop: "{{ USER_ID_details }}"
ignore_errors: true
register: id_check
- name: setting var
set_fact:
user_id_names1: "{{ user_id_names1|default([]) + [item.stdout] }}"
with_items: "{{ id_check.results }}"
when: item.stdout != ""
- debug: var=user_id_names1
- block:
- name: create Linux user as per specification
user:
name: "{{ item.user_id }}"
password: "{{ pass_reg.stdout_lines[0] | password_hash('sha512') }}"
group: "{{ group_name }}"
groups: "{{ item.groups }}"
comment: "{{ comment }}"
shell: "{{ user_shell }}"
#uid: "{{ uid_num.item }}"
home: "/home/{{ item.user_id}}"
loop: "{{ USER_ID_details }}"
when:
- os_type == "RedHat"
- "{{ item.user_id }} not {{id_check.results}}"
What could be the best way to check if user exists, and only add those, that don't exist on server. I'm trying to check the user_id_names1 list of ids generated against list dictionary USER_ID_details and filter the existing ones.
As Vladimir Botka stated on the comment, ansible does that already. Most modules (including the user module) will ensure that the state you specify will be present on the machine, after ansible ran.
For example, if you specify that a certain user exists on the system, it will after you ran the playbook. It will be created if it didn't exist before, but it will not be added, if it already existed.
The catch is, that ansible will try to create the state you specified, possibly changing your existing users.
For example, let's assume your user already exists, but has changed the default shell to /bin/zsh while in your playbook you specify, that it should have /bin/bash. In that case, ansible will change the default shell to /bin/bash whenever you run your playbook.
If you don't care about existing users being modified (or you are sure they never will be) you can just run the user module for all users every time, as users will not be added twice.
Otherwise you can do this to check if a user exists and only add it if it does not:
tasks:
- name: get list of existing users
getent:
database: passwd
- name: get list of existing usernames
set_fact:
existing_users: "{{ ansible_facts.getent_passwd.keys() | list }}"
- name: create Linux user as per specification
user:
name: "{{ item.user_id }}"
password: "{{ pass_reg.stdout_lines[0] | password_hash('sha512') }}"
group: "{{ group_name }}"
groups: "{{ item.groups }}"
comment: "{{ comment }}"
shell: "{{ user_shell }}"
home: "/home/{{ item.user_id}}"
loop: "{{ USER_ID_details }}"
when: item.user_id not in existing_users
Make sure to read the documentation of the user module and that you understand what all the options do.
For example, the password option will set the password of that user to the specified value. If the user changed his password, you will change it back every time you run the playbook. Set update_password: on_create to prevent that.
You are also setting the primary group of all users to the same value (in group_name). Make sure that is what you actually want to do.
#toydarian Used below method too when i didnot know about the getent option.
- name: check for the ID is present
#shell: "id {{ item.user_id }}"
shell: grep "{{ item.user_id }}" /etc/passwd | awk -F":" '{print $1}'
loop: "{{ USER_ID_details }}"
ignore_errors: true
register: id_check
- name: setting var
set_fact:
user_id_names1: "{{ user_id_names1|default([]) + [item.stdout] }}"
with_items: "{{ id_check.results }}"
when: item.stdout != ""
- debug: var=user_id_names1
- name: create Linux user as per specification
user:
name: "{{ item.user_id }}"
password: "{{ pass_reg.stdout_lines[0] | password_hash('sha512') }}"
group: "{{ group_name }}"
groups: "{{ item.groups }}"
comment: "{{ item.real_full_name }}"
shell: "{{ user_shell }}"
#uid: "{{ uid_num.item }}"
home: "/home/{{ item.user_id}}"
loop: "{{ USER_ID_details }}"
when:
- os_type == "RedHat"
- item.user_id not in user_id_names1

Ansible - skip undefined variable in dict

I`m using ipa_user module to setup users. There is variable passsword which force new password.
For some users (when var is not in dict) I would like to skip it in iteration, but it always fail.
This is snippet from my playbook. Ansible version is 2.7
task:
- name: adding ipa users
ipa_user:
name: "{{ item.value.login }}"
state: "{{ item.value.state }}"
givenname: "{{ item.value.givenname }}"
sn: "{{ item.value.surname }}"
mail: "{{ item.value.mail }}"
telephonenumber: "{{ item.value.telephonenumber }}"
title: "{{ item.value.title }}"
password: "{{ item.value.password }}" <<- to be skipped if not found
ipa_host: ipa.gdi.telekom.de
ipa_user: admin
ipa_pass: "{{ ipa_pass }}"
with_dict: "{{ipausers}}"
when: item.key in ipausers.keys()
register: output_ipa_users
Log:
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'dict object' has no attribute 'password'\n\nThe error appears to have been in '/builds/gitlab/infra/user-management/roles/free-ipa/tasks/main.yml': line 13, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- name: adding ipa users\n ^ here\n"}
Note: I tried it with:
with_dict: "{{ipausers|default({})}}"
ignore_errors: yes
without success
Not sure if it'll be much help to you now but for others than stumble on this post, I ended up with something like below for a similar problem. I'm using Ansible 2.7.8.
- name: Creating user accounts...
user:
name: "{{ item.name }}"
state: "{{ item.state }}"
comment: "{{ item.comment | default(omit) }}"
group: "{{ item.groups is defined | ternary((item.groups|default([]))[0], omit) }}"
groups: "{{ item.groups | default(omit) }}"
password: "{{ item.password_hash | default(omit) }}"
uid: "{{ item.uid | default(omit) }}"
with_items: "{{ managed_users }}"
The solution is
group: "{{ item.groups is defined | ternary((item.groups|default([]))[0], omit) }}"
If groups isn't in item then Ansible will omit the group part of this tasks but jinja2 will evaluate item.groups[0] anyway. So to allow for this we have to use item.groups|default([]) so jinja2 uses an empty list when groups isn't defined instead of throwing a 'dict object' has no attribute error. The omit part is similar to the default(omit) filter where Ansible simply omits the option from the task.
Lubo's problem is a little simpler so using just default(omit) filter should work. That said as password is required so the entire task should be skipped with a conditional.
- name: adding ipa users
ipa_user:
name: "{{ item.value.login }}"
state: "{{ item.value.state }}"
givenname: "{{ item.value.givenname }}"
sn: "{{ item.value.surname }}"
mail: "{{ item.value.mail }}"
telephonenumber: "{{ item.value.telephonenumber }}"
title: "{{ item.value.title }}"
password: "{{ item.value.password | default(omit) }}" #<-- would be omitted
ipa_host: ipa.gdi.telekom.de
ipa_user: admin
ipa_pass: "{{ ipa_pass }}"
with_dict: "{{ipausers}}"
when: item.key in ipausers.keys() and item.key.password is defined #<-- second check for when password is not defined.
register: output_ipa_users
If you want to completely skip the ipa_user module execution when password is not defined, check for its presence in your when clause:
when: item.value.password | default('') | length > 0
If you want to execute the ipa_user module without specifying a password for user if it does not exists, use the omit placeholder in your module params:
password: "{{ item.value.password | default(omit) }}"
Note: your current when clause can be removed. It will always return true as you are looping over a dict and later checking if the current key in the loop is part of that dict.
There is a special omit variable to omit module parameters.
password: "{{ item.value.password|default(omit) }}"
To make a playbook or a role reusable it is a good idea to declare all parameters of a module in the task and default(omit) parameters that are not required.

Combine multiple public keys with Ansible

How can I combine multiple ssh public keys to use with Ansible's authorized_key module?
I have variables file containing users and keys:
ssh_users:
- name: peter
keys:
- 'ssh-rsa AAAAB3NzaC1yc2EAAA peter#key1'
- 'ssh-rsa AAAABsgsdfgyc2EAAA peter#key2'
root: yes
- name: paul
keys:
- 'ssh-rsa AAAAB3Nzaafac2EAAA paul#key1'
root: no
I'd like to go over this list, pick out users (and their keys) which have 'root: yes' and combine them to update root user's authorized_keys file.
This doesn't work:
- name: lookup keys
set_fact:
keylist: "{{ item.keys }}"
with_items: "{{ ssh_users }}"
when: item.root == true
register: result
- name: make a list
set_fact:
splitlist: "{{ result.results |
selectattr('ansible_facts','defined') | map(attribute='ansible_facts.keylist') | list | join('\n') }}"
- name: update SSH authorized_keys
authorized_key:
user: root
key: "{{ splitlist }}"
state: present
exclusive: yes
You can get what you want using the Jinja selectattr and map filters, like this:
---
- hosts: localhost
gather_facts: false
vars:
# Here's our data: two users with 'root' access,
# one without. We expect to see three public keys in
# the resulting authorized_keys file.
#
# Note that I've renamed the "keys" key to "pubkeys", because
# otherwise it conflicts with the "keys" method of dictionary
# objects (leading to errors when you try to access something
# like item.keys).
ssh_users:
- name: alice
pubkeys:
- 'ssh-rsa alice-key-1 alice#key1'
root: true
- name: peter
pubkeys:
- 'ssh-rsa peter-key-1 peter#key1'
- 'ssh-rsa peter-key-2 peter#key2'
root: true
- name: paul
pubkeys:
- 'ssh-rsa paul-key-1 paul#key1'
root: false
tasks:
- become: true
authorized_key:
user: root
key: "{{ '\n'.join(ssh_users|selectattr('root')|map(attribute='pubkeys')|flatten) }}"
state: present
exclusive: true
In the authorized_key task, we first use the selectattr filter to extract those users with root access. We pass that to the map filter to extract just the pubkeys attribute, which would give us two lists (one with one key, the other with two keys). Finally, we pass that to the flatten filter to create a single list, and then join the resulting keys with newlines to match the input format expected by the authorized_key module. The resulting .ssh/authorized_keys file looks like:
ssh-rsa alice-key-1 alice#key1
ssh-rsa peter-key-1 peter#key1
ssh-rsa peter-key-2 peter#key2
Is this the code you're looking for?
- name: update SSH authorized_keys
authorized_key:
user: root
key: "{{ item.1 }}"
loop: "{{ ssh_users | subelements('keys', skip_missing=True) }}"
when: item.0.root
You do not need parameters exclusive and state. Defaults exclusive: no and state: present are OK, I think.
Keys, where root: False, can be removed
- name: remove SSH authorized_keys
authorized_key:
state: absent
user: root
key: "{{ item.1 }}"
loop: "{{ ssh_users | subelements('keys', skip_missing=True) }}"
when: not item.0.root
To add and remove keys in one task ternary filter might be used
- name: Preen SSH authorized_keys
authorized_key:
state: "{{ item.0.root | ternary('present','absent') }}"
user: root
key: "{{ item.1 }}"
loop: "{{ ssh_users | subelements('keys', skip_missing=True) }}"

How to create a 'null' default in Ansible

I want 'lucy' to follow the user module creators' default behaviour which is to create and use a group matching the user name 'lucy'. However for 'frank' I want the primary group to be an existing one; gid 1003. So my hash looks like this:
lucy:
comment: dog
frank:
comment: cat
group: 1003
And my task looks like this:
- name: Set up local unix user accounts
user:
name: "{{ item.key }}"
comment: "{{ item.value.comment }}"
group: "{{ item.value.group | default(undef) }}"
loop: "{{ users|dict2items }}"
This doesn't work, as undef is not recognised. Nor is anything else I can think of. 'null', 'None' etc. all fail. '' creates an empty string which is not right either. I can't find out how to do it.
Any ideas?
default(omit) is what you are looking for. For example,
- name: Set up local Unix user accounts
user:
name: "{{ item.key }}"
comment: "{{ item.value.comment }}"
group: "{{ item.value.group | default(omit) }}"
loop: "{{ users|dict2items }}"
Comments
Comment by Lucas Basquerotto: "... omit only works correctly when used directly in a module, it won't work in a set_fact ..."
A: You're wrong. For example, default(omit) works both in set_fact and in the module. The first item in the list defaults to false with the result "VARIABLE IS NOT DEFINED!". The second item defaults to omit. Omitted parameter get_checksum defaults to true with the checksum in the results
shell> cat pb.yml
- hosts: localhost
tasks:
- set_fact:
test:
- "{{ gchk|default(false) }}"
- "{{ gchk|default(omit) }}"
- stat:
path: /etc/passwd
get_checksum: "{{ item }}"
loop: "{{ test }}"
register: result
- debug:
var: item.stat.checksum
loop: "{{ result.results }}"
gives
shell> ansible-playbook pb.yml | grep item.stat.checksum
item.stat.checksum: VARIABLE IS NOT DEFINED!
item.stat.checksum: 7c73e9f589ca1f0a1372aa4cd6944feec459c4a8
In addition to this, default(omit) works as expected also in some expressions. For example
- debug:
msg: "{{ {'a': item}|combine({'b': true}) }}"
loop: "{{ test }}"
gives
msg:
a: false
b: true
msg:
b: true
See the results without default values
shell> ansible-playbook pb.yml -e "gchk={{ true|bool }}"

Ansible - failed to lookup user

How can I solve problem with run ansible role below? If a user doesn't exist on the remote server, ansible gets me the error "Failed to lookup user test1: 'getpwnam(): name not found: test1". I need manage multiple users on multiple servers. Thanks
vars:
user_list:
- user: test1
state: present
path: /usr/local/test1/.ssh/authoried_keys
keys:
- "ssh-rsa test1"
- user: test2
state: absent
path: /home/test2/.ssh/authoried_keys
keys:
- "ssh-rsa test2"
tasks:
- name: Manage SSH-keys
authorized_key:
user: "{{ item.0.user }}"
key: "{{ item.1 }}"
path: "{{ item.0.path }}"
state: "{{ item.0.state }}"
with_subelements:
- '{{ user_list }}'
- keys
CentOS Linux 7, Ansible 2.4.2.0
Perhaps you could check the existing users through ansible's wrapper for getent?
It feels a bit simpler and you don't need to use the shell module:
tasks:
- name: Get existing users
getent:
database: passwd
- name: Disable expired users
user:
name: "{{ item.name }}"
shell: /sbin/nologin
with_items:
- "{{ users_removed }}"
when: item.name in getent_passwd.keys()
Note though that as #techraf points out, at production environments you should always aim at declaring and knowing beforehand which users should and shouldn't be present :)
I think, that I solved my problem.
tasks:
- name: Check for users
shell: cat /etc/passwd | cut -f1 -d":"
register: sshkeys_users
changed_when: False
- name: Manage SSH-keys
authorized_key:
user: "{{ item.0.user }}"
key: "{{ item.1 }}"
path: "{{ item.0.path }}"
state: "{{ item.0.state }}"
with_subelements:
- '{{ user_list }}'
- keys
when: sshkeys_users is defined and item.0.user in sshkeys_users.stdout_lines

Resources