I have created the following ansible role for samba, which works without problems.
The user will be created and the password will be set:
role:
---
- name: "Include OS-specific variables"
include_vars: "{{ ansible_os_family }}.yml"
- name: "Ensure Samba-related packages are installed"
apt:
name:
- samba
- samba-common
state: present
when: ansible_os_family == 'Debian'
- name: "Configure smb.conf"
template:
src: etc/samba/smb.conf
dest: /etc/samba/smb.conf
notify: restart_smbd
- name: "Create system user and home directory"
ansible.builtin.user:
name: "{{ item.name }}"
home: "{{ item.home }}"
system: yes
skeleton: no
shell: /sbin/nologin
group: nogroup
state: present
with_items: "{{ smb.user_details }}"
- name: "Create samba users"
shell: >
set -e -o pipefail
&& (pdbedit --user={{ item.name }} 2>&1 > /dev/null)
|| (echo '{{ item.smbpassword }}'; echo '{{ item.smbpassword }}')
| smbpasswd -s -a {{ item.name }}
args:
executable: /bin/bash
register: samba_create_users
changed_when: "'Added user' in samba_create_users.stdout"
loop: "{{ smb.user_details }}"
- name: "Set samba passwords correctly"
shell: >
set -e -o pipefail
&& (smbclient -U {{ item.name }}%{{ item.smbpassword }} -L 127.0.0.1 2>&1 > /dev/null)
|| (echo '{{ item.smbpassword }}'; echo '{{ item.smbpassword }}')
| smbpasswd {{ item.name }}
args:
executable: /bin/bash
register: samba_verify_users
changed_when: "'New SMB password' in samba_verify_users.stdout"
loop: "{{ smb.user_details }}"
- name: "Ensure Samba is running and enabled"
service:
name: "{{ samba_daemon }}"
state: started
enabled: true
host vars:
.
.
.
smb:
user_details:
- name: test
home: /backup/test
smbpassword: testtest
When i know want to improve the role, that an absent user is getting deleted, it will not work:
- name: "Configure smb.conf"
template:
src: etc/samba/smb.conf
dest: /etc/samba/smb.conf
notify: restart_smbd
- name: "Create system user and home directory"
become: yes
user:
name: "{{ item.key }}"
state: "{{ item.value.state | default('present') }}"
append: yes
system: yes
skeleton: no
group: nogroup
home: "{{ item.value.home }}"
shell: "{{ item.value.shell | default('/sbin/nologin') }}"
loop: "{{ samba_users | dict2items }}"
when: "'state' not in item.value or item.value.state == 'present'"
- name: "Create samba users"
shell: >
set -e -o pipefail
&& (pdbedit --user={{ item.key }} 2>&1 > /dev/null)
|| (echo '{{ item.value.name }}'; echo '{{ item.smbpassword }}')
| smbpasswd -s -a {{ item.key }}
args:
executable: /bin/bash
register: samba_create_users
changed_when: "'Added user' in samba_create_users.stdout"
loop: "{{ samba_users }}"
- name: "Set samba passwords correctly"
shell: >
set -e -o pipefail
&& (smbclient -U {{ item.key }}%{{ item.smbpassword }} -L 127.0.0.1 2>&1 > /dev/null)
|| (echo '{{ item.smbpassword }}'; echo '{{ item.smbpassword }}')
| smbpasswd {{ item.key }}
args:
executable: /bin/bash
register: samba_verify_users
changed_when: "'New SMB password' in samba_verify_users.stdout"
loop: "{{ samba_users }}"
- name: "Remove unwanted users."
become: yes
user:
name: "{{ item.key }}"
state: "{{ item.value.state | default('absent') }}"
remove: true
loop: "{{ samba_users | dict2items }}"
when: "'state' in item.value and item.value.state == 'absent'"
- name: "Ensure Samba is running and enabled"
service:
name: "{{ samba_daemon }}"
state: started
enabled: true
...
host vars:
.
.
.
samba_users:
test:
state: absent
The following error happens:
msg": "Invalid data passed to 'loop', it requires a list, got this instead: {'test': {'state': 'absent'}}. Hint: If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup."
Is it even possible to do this easily?
The problem is that if the user is absent, the "Create samba users" task should not actually be run through.
I would have to write a second task that deletes the Samba user.
But how can I control that when the user is absent he only uses the new task, e.g. "delete samba users"?
Related
What I'm trying to solve: I have several servers (in this example 3, 1 nfs server, 2 clients - they all resolve back to localhost for this minimal example), and the clients need to access shares on the server, which are created using the playbook.
The IP addresses of the clients need to be added to the respective entries in /etc/exports as they go - the list is not predefined at any given time. (In my actual playbook I use ansible facts, for this example I've added them as a variable)
In Ansible lineinfile regexp to manage /etc/exports Vladimir was so kind as to help with an initial thing, which works, but doesn't seem to be idempotent. The entries / ip addresses are added correctly the first time round, but the second run the IP addresses get re-added to the entries in /etc/exports, causing nfs to bomb out at that time (double entries)
Correct /etc/exports:
bar 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
foo 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
What I'm getting:
bar 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check) 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
foo 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check) 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
I've been butting my head around it but I can't come up with anything that works. I've distilled it down to the following playbook:
$ ansible-playbook main.yml
Content of the main.yml:
- hosts: localhost
become: yes
vars:
client_ip: 192.168.34.46
nfs_server: localhost
shares:
- bar
- foo
tasks:
- name: Mountpoint management
ansible.builtin.include_tasks: task.yml
loop: "{{ shares | default([]) }}"
loop_control:
loop_var: volume
args:
apply:
delegate_to: "{{ nfs_server }}"
- hosts: localhost
become: yes
vars:
client_ip: 192.168.34.47
nfs_server: localhost
shares:
- bar
- foo
tasks:
- name: Mountpoint management
ansible.builtin.include_tasks: task.yml
loop: "{{ shares | default([]) }}"
loop_control:
loop_var: volume
args:
apply:
delegate_to: "{{ nfs_server }}"
Second file task.yml which is needed to do a loop:
---
---
- name: Ensure /etc/exports exists
ansible.builtin.file:
path: /etc/exports
owner: root
group: root
mode: '0644'
state: touch
changed_when: False
- name: Add host {{ client_ip }} to {{ volume }}
ansible.builtin.lineinfile:
path: "/etc/exports"
regex: '^{{ volume }}(\s+)({{ ip_regex }})*({{ mount_opts_regex }})*(\s*)(.*)$'
line: '{{ volume }}\g<1>{{ ip }}{{ mount_opts }} \g<5>'
backrefs: true
vars:
ip: "{{ client_ip }}"
ip_regex: '{{ client_ip | regex_escape() }}'
mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
mount_opts_regex: '\(.*?\)'
- name: Read /etc/exports
command: "cat {{ item }}"
register: result
check_mode: no
loop:
- /etc/exports
changed_when: False
- ansible.builtin.set_fact:
content: "{{ dict(_files|zip(_lines)) }}"
vars:
_lines: "{{ result.results|map(attribute='stdout_lines')|list }}"
_files: "{{ result.results|map(attribute='item')|list }}"
- name: Add new line to /etc/exports
ansible.builtin.lineinfile:
path: "/etc/exports"
line: '{{ volume }} {{ client_ip }}{{ mount_opts }}'
vars:
mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
loop: "{{ content }}"
when: content[item] | select('search', volume)|length == 0
Well, thinking it through I finally went with constructing two lists, mapping those into a dict and checking if the values are already present. If they are, don't do anything, if not, add it.
main.yaml:
- hosts: localhost
become: yes
vars:
client_ip: 192.168.34.46
nfs_server: localhost
shares:
- bar
- foo
tasks:
- name: Mountpoint management
ansible.builtin.include_tasks: task.yml
loop: "{{ shares | default([]) }}"
loop_control:
loop_var: volume
args:
apply:
delegate_to: "{{ nfs_server }}"
- hosts: localhost
become: yes
vars:
client_ip: 192.168.34.47
nfs_server: localhost
shares:
- bar
- foo
tasks:
- name: Mountpoint management
ansible.builtin.include_tasks: task.yml
loop: "{{ shares | default([]) }}"
loop_control:
loop_var: volume
args:
apply:
delegate_to: "{{ nfs_server }}"
task.yaml:
---
- name: Ensure /etc/exports exists
ansible.builtin.file:
path: /etc/exports
owner: root
group: root
mode: '0644'
state: touch
changed_when: false
- name: Read /etc/exports
command: "cat /etc/exports"
register: result
check_mode: no
changed_when: false
- ansible.builtin.set_fact:
share_names: "{{ share_names | default([]) + [item.split(' ')[0]] }}"
share_values: "{{ share_values | default([]) + [item.split(' ')[1:]] }}"
loop:
"{{ result.stdout_lines }}"
- ansible.builtin.set_fact:
share_content: "{{ dict(share_names | zip(share_values)) }}"
- name: Add host {{ client_ip }} to {{ volume }}
ansible.builtin.lineinfile:
path: "/etc/exports"
regex: '^{{ volume }}(\s+)({{ ip_regex }})*({{ mount_opts_regex }})*(\s*)(.*)$'
line: '{{ volume }}\g<1>{{ client_ip }}{{ mount_opts }} \g<5>'
backrefs: true
vars:
ip_regex: '{{ client_ip | regex_escape() }}'
mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
mount_opts_regex: '\(.*?\)'
str: '{{ client_ip }}{{ mount_opts }}'
when: volume in share_content and str not in share_content[volume]
- name: Add new line to /etc/exports
ansible.builtin.lineinfile:
path: "/etc/exports"
line: '{{ volume }} {{ client_ip }}{{ mount_opts }}'
vars:
mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
loop: "{{ share_content }}"
when: volume not in share_content
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
I am trying to use a variable from with_item to create a fact whose name changes:
- name: get ABC Root CA pem from chamber
shell: AWS_PROFILE={{aws_profile}} chamber read -q secrets abc_ca_{{ item }} | awk 'NR==2 { FS="[ \t+]"; print($2); }' | base64 -d | gunzip
changed_when: false
delegate_to: localhost
vars:
ansible_become: no
with_items: "{{ abc_environments }}" # see defaults/main.yml
when: ('chamber' in chamber_installed.stdout)
register: abc_returned_environment
tags:
- service-discovery
- name: set ABC root ca pem secrets
set_fact:
aws_secrets: "{{ aws_secrets|default({}) | combine( {item.item: item.stdout} ) }}"
with_items: "{{abc_returned_environment.results}}"
when: ('chamber' in chamber_installed.stdout)
tags:
- service-discovery
- name: Write aws_secrets ABC root CA file
vars:
env: "{{ item }}"
copy:
content: "{{ aws_secrets.(lookup('vars', env )) }}\n"
dest: /etc/prometheus/abc_{{ item }}_file_sd/ca_{{ item }}.pem
owner: prometheus
group: prometheus
mode: 0600
with_items: "{{ abc_environments }}"
when: ('chamber' in chamber_installed.stdout)
tags:
- service-discovery
How do I get the content line to behave something like this?
content: "{{ aws_secrets.abc_ca_dev }}\n"
Where abc_environments is set as:
abc_environments:
- dev
- qa
- int
- staging-green
- staging-blue
- prod-green
- prod-blue
So it turn out to be actually quite simple:
- name: Write aws_secrets ABC root CA file
copy:
content: "{{ aws_secrets[ item ] }}\n"
dest: /etc/prometheus/abc_{{ item }}_file_sd/ca_{{ item }}.pem
owner: prometheus
group: prometheus
mode: 0600
with_items: "{{ abc_environments }}"
when: ('chamber' in chamber_installed.stdout)
tags:
- service-discovery
Also handles dashes in the variable names.
I am writing a playbook to create a user if it does not exist. If it exist then it ask again for a new username. It must ask again for new user input. But var_promt runs only one time. How can I do that?
- name: An example prompting playbook
hosts: all
vars_prompt:
- name: username
prompt: "Enter a username"
tasks:
- name: Print out your input
debug:
msg: "You provided the {{ username }} for the prompt"
- name: User Exist
command: grep {{ username }} /etc/passwd
ignore_errors: yes
register: user_exist
- name: User Existance output
debug: var=user_exist.stdout
- name: User creation
user: name={{ username }}
when: user_exist.stdout is not match(".*:.*:.*:.*::.*:")
- name: Display Message
debug:
msg: "User {{ username }} already exists"
when: user_exist.stdout is match(".*:.*:.*:.*::.*:")
- name: User check
command: id {{ username }}
ignore_errors: yes
register: ID
- name: UserID of Username
debug: var=ID.stdout
name: An example prompting playbook
hosts: localhost
vars_files:
/root/users.yml
tasks:
name: Create deploy user
user:
name: "{{ item.name }}"
shell: "{{ item.shell }}"
password: "{{ lookup('password', '/tmp/{{ item.name }}.txt chars=ascii_letters') | password_hash('sha512') }}"
createhome: yes
groups: "{{ item.groups }}"
update_password: on_create
with_items: "{{ users }}"
Although the following command works when typing in in shell
echo -ne "myser\nmypass\n" | smbpasswd -a -s myuser
The following task fails in ansible
- name: add dms samba user
command: echo -ne "myuser\nmypass\n" | smbpasswd -a -s myuser
notify: restart samba
It does not produce any errors, but the user is not created.
Working with ansible 2.3.0.0 on Ubuntu 16.0.4.
As stated, pipes won't work with the command module. I've used something like this in the past to create Samba users:
- name: Configure Samba users.
shell: >
(pdbedit --user={{ item.username }} 2>&1 > /dev/null)
|| (echo '{{ item.password }}'; echo '{{ item.password }}')
| smbpasswd -s -a {{ item.username }}
register: smbpasswd
changed_when: "'Added user' in smbpasswd.stdout"
with_items: "{{ samba_users }}"
loop_control:
label: "{{ item.username }}"
The task will only run if the user does not exist yet. So changing passwords won't work with this example.
Please try this approach with your Ansible Playbook:
- name: set Samba passwords for each user
shell: "printf '{{ item.passwd }}\n{{ item.passwd }}\n' | smbpasswd -a {{ item.name }}"
with_items:
- "{{ users }}"
tags: smbpasswd
Please note that you will need to map your variables file that includes users: with the format of:
users:
- name: userName
passwd: myClearTextPassword
Please note that to support smbpasswd you will be passing this password as clear text. Additionally, noting this is only a single task that would need to be included in your playbook.
I improved the code from siwyd and Tormod Macleod slightly.
Thanks to both of you!
- name: shell - create samba users
shell: >
set -e -o pipefail
&& (pdbedit --user={{ item.username }} 2>&1 > /dev/null)
|| (echo '{{ item.password }}'; echo '{{ item.password }}')
| smbpasswd -s -a {{ item.username }}
args:
executable: /bin/bash
register: samba_create_users
changed_when: "'Added user' in samba_create_users.stdout"
loop: "{{ samba_users }}"
no_log: true
- name: shell - set samba passwords correctly
shell: >
set -e -o pipefail
&& (smbclient -U {{ item.username }}%{{ item.password }} -L 127.0.0.1 2>&1 > /dev/null)
|| (echo '{{ item.password }}'; echo '{{ item.password }}')
| smbpasswd {{ item.username }}
args:
executable: /bin/bash
register: samba_verify_users
changed_when: "'New SMB password' in samba_verify_users.stdout"
loop: "{{ samba_users }}"
no_log: true
Changes:
Added pipefail to satisfy Ansible Lint (https://ansible-lint.readthedocs.io/en/latest/default_rules.html#risky-shell-pipe)
Changed executable to /bin/bash, beacause /bin/sh doesn't know pipefail
Added no_log to prevent password logging if the task fails
Removed loop_control label, since logging is disabled
Used loop instead of with_items
The answer by siwyd above is excellent. I was struggling to figure out how to solve this problem in an idempotent way until I saw this. For my use-case, I'd like to keep the passwords in sync so I've added another play to do this. Might be useful for someone
- name: shell - create samba users
shell: >
(pdbedit --user={{ item.username }} 2>&1 > /dev/null)
|| (echo '{{ item.password }}'; echo '{{ item.password }}')
| smbpasswd -s -a {{ item.username }}
register: create_samba_users
changed_when: "'Added user' in create_samba_users.stdout"
become: true
with_items: "{{ samba_users }}"
loop_control:
label: "{{ item.username }}"
- name: shell - set samba passwords correctly
shell: >
(smbclient -U {{ item.username }}%{{ item.password }} -L 127.0.0.1 2>&1 > /dev/null)
|| (echo '{{ item.password }}'; echo '{{ item.password }}')
| smbpasswd {{ item.username }}
register: verify_samba_users
changed_when: "'New SMB password' in verify_samba_users.stdout"
become: true
with_items: "{{ samba_users }}"
loop_control:
label: "{{ item.username }}"
the command module does not support pipelining. use the shell module for stuff like this.
see:
command module
shell module
Another variant using dictionary lists:
ad_users: [
{ username: john.doe, password: P4ssw0rd*, givenname: John, surname: Doe, mail: john.doe#domain, ou: "OU=Department,OU=Division" },
{ username: jane.doe, password: P455w0rd*, givenname: Jane, surname: Doe, mail: jane.doe#domain, ou: "OU=Department,OU=Division" },
]
- name: Add user to AD
command: samba-tool user create {{ item.username }} {{ item.password }} --given-name='{{ item.givenname }}' --surname='{{ item.surname }}' --mail-address={{ item.mail }} --userou='{{ item.ou }}'
loop: "{{ ad_users }}"
Just remember to vault sensitive data.