1 Commits
Author SHA1 Message Date
CSRBot 2759e18fe6 chore(deps): pin dependencies
Lint Markdown files / markdown-lint (push) Failing after 14m37s
Ansible Linter / ansible-lint (pull_request) Failing after 14m41s
Ansible Linter / ansible-lint (push) Failing after 14m44s
Lint Markdown files / markdown-lint (pull_request) Successful in 4s
2026-06-05 12:02:58 +00:00
31 changed files with 157 additions and 579 deletions
+20
View File
@@ -0,0 +1,20 @@
name: Ansible Linter
on:
pull_request:
types: [ "opened", "reopened", "synchronize" ]
push:
branches: [ '**' ]
tags-ignore: [ '**' ]
jobs:
ansible-lint:
runs-on:
- ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Run ansible-lint
uses: ansible/ansible-lint@5fac056c45595896c973fbde871f01f6cb14d74c # v26.4.0
with:
args: "--config-file .ansible-lint"
setup_python: "true"
-22
View File
@@ -1,22 +0,0 @@
name: Ansible Linter
on:
pull_request:
types: [ "opened", "reopened", "synchronize" ]
push:
branches: [ '**' ]
tags-ignore: [ '**' ]
jobs:
ansible-lint:
runs-on:
- ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run ansible-lint
uses: ansible/ansible-lint@e7f397ad6dfa20d274afa17cd7bbedd84ed136f5 # v26.9.0
with:
args: "--config-file .ansible-lint"
# The molecule scenario is linted as well, so its collections are required beside the ones of the role.
requirements_file: "molecule/default/collections.yml"
setup_python: "true"
@@ -12,7 +12,7 @@ jobs:
runs-on:
- ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: DavidAnson/markdownlint-cli2-action@30a0e04f1870d58f8d717450cc6134995f993c63 # v21.0.0
with:
globs: '**/*.md'
-29
View File
@@ -1,29 +0,0 @@
name: Molecule
on:
pull_request:
types: [ "opened", "reopened", "synchronize" ]
push:
branches: [ '**' ]
tags-ignore: [ '**' ]
permissions:
contents: read
jobs:
molecule:
name: Molecule
runs-on: ubuntu-latest-amd64
steps:
# The scenario includes the role by its directory name, whose parent is used as roles path.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: unix-users
- name: Install molecule
run: |
apt update --yes
apt install --yes python3-pip
pip3 install --break-system-packages molecule docker
- name: Run molecule
run: molecule test
working-directory: unix-users
+1 -2
View File
@@ -1,2 +1 @@
.ansible
molecule/default/files/ssh
.ansible
-55
View File
@@ -6,57 +6,6 @@ The ansible role `volker-raschek.unix-users` create and manage users on Linux ba
Linux, Fedora and Ubuntu. Furthermore, the role can also be used to create groups, `~/.forward`, `~/.netrc` and to
manage the `~/.ssh` directory.
## Requirements
The role requires `ansible-core` 2.11 or newer. A home directory can optionally be created as btrfs subvolume, which
relies on the `btrfs_subvolume` module of the collection `community.general`.
```bash
ansible-galaxy collection install -r requirements.yml
```
The role manages users, groups and their home directories, so it has to be executed with `become: true`.
## Tests
The role is tested with [Molecule](https://ansible.readthedocs.io/projects/molecule/). The scenario starts one docker
container per supported distribution family, applies the role, asserts that a second run reports no change and finally
verifies the created users and groups, the permissions and the content of the managed files and that a user declared
as `absent` is gone again. A btrfs home is not covered, because a container has no btrfs filesystem to create a
subvolume on.
The ssh key pair the scenario feeds into the role is generated during `molecule create` and removed again during
`molecule destroy`, so no private key is kept in the repository.
Molecule ships only its `default` driver, therefore `docker` is required besides molecule itself. The collections are
declared in `molecule/default/collections.yml` and installed by molecule.
```bash
pip install molecule docker
```
The complete sequence creates the containers, tests them and removes them afterwards.
```bash
molecule test
```
While working on the role the containers are better kept alive.
```bash
# create the containers and apply the role
molecule converge
# run the assertions of molecule/default/verify.yml against the running containers
molecule verify
# open a shell in one of the containers
molecule login --host unix-users-debian
# remove the containers
molecule destroy
```
## Examples
### User and group
@@ -85,10 +34,6 @@ unix_users:
Optionally, the home directory of a user can also be created as dedicated btrfs subvolume. This make it possible to
create snapshots of the home directory, for example via `btrbk`.
> [!WARNING]
> Removing a user with `state: absent` also deletes the btrfs subvolume of the home directory. Snapshots taken from that
> subvolume are not removed and keep the data available.
```yaml
unix_users:
toor:
+1 -1
View File
@@ -9,7 +9,7 @@ galaxy_info:
- unix
- linux
license: "MIT"
min_ansible_version: "2.11"
min_ansible_version: "2.9"
namespace: volker-raschek
platforms:
- name: ArchLinux
-5
View File
@@ -1,5 +0,0 @@
---
collections:
- name: community.docker
- name: community.general
-64
View File
@@ -1,64 +0,0 @@
---
# A btrfs home is not covered, a container has no btrfs filesystem to create a subvolume on.
- name: Converge
hosts: all
vars:
unix_groups:
molecule-alice:
# An unquoted gid is an integer, the role has to cope with that.
gid: 4242
state: present
molecule-bob:
state: present
molecule-obsolete:
state: absent
unix_users:
molecule-alice:
state: present
name: Alice
uid: 4242
group: molecule-alice
home: /home/molecule-alice
shell: /bin/bash
password: alice
email: alice@example.local
ssh:
config:
- Host: "*"
StrictHostKeyChecking: "no"
authorized_keys:
- filename: molecule.pub
command: "/usr/bin/true"
envs:
- key: EDITOR
value: vi
private_keys:
- molecule.ed25519.key
netrc:
- machine: hostname.local
login: alice
password: secret
shell_rc_files:
- file: molecule.bashrc
aliases:
- key: dcd
value: docker compose down
envs:
- export: true
key: PATH
value: "${HOME}/bin:${PATH}"
functions:
- name: foo
value: "echo \"bar\""
# Bob declares nothing optional, so none of the optional files may show up in his home.
molecule-bob:
state: present
group: molecule-bob
molecule-dave:
state: absent
tasks:
# The role is included by the name of its directory, which molecule put on the roles path.
- name: Include the role unix-users
ansible.builtin.include_role:
name: unix-users
-49
View File
@@ -1,49 +0,0 @@
---
- name: Create
hosts: localhost
gather_facts: false
vars:
_private_key: "{{ molecule_scenario_directory }}/files/ssh/private_keys/molecule.ed25519.key"
_authorized_key: "{{ molecule_scenario_directory }}/files/ssh/authorized_keys/molecule.pub"
tasks:
- name: Start a container per platform
community.docker.docker_container:
name: "{{ item.name }}"
image: "{{ item.image }}"
command: "sleep infinity"
state: started
loop: "{{ molecule_yml.platforms }}"
loop_control:
label: "{{ item.name }}"
- name: Write the instance config
ansible.builtin.copy:
content: |
{% for platform in molecule_yml.platforms %}
- instance: {{ platform.name }}
connection: community.docker.docker
{% endfor %}
dest: "{{ molecule_instance_config }}"
mode: "0600"
- name: Create the fixture directories
ansible.builtin.file:
path: "{{ item | dirname }}"
state: directory
mode: "0700"
loop:
- "{{ _private_key }}"
- "{{ _authorized_key }}"
# The key pair is generated instead of committed, private key material does not belong into a repository.
- name: Generate the ssh key pair the role is fed with
ansible.builtin.command:
cmd: "ssh-keygen -t ed25519 -N '' -C molecule -f {{ _private_key }}"
creates: "{{ _private_key }}"
- name: Offer the public key as authorized key fixture
ansible.builtin.copy:
src: "{{ _private_key }}.pub"
dest: "{{ _authorized_key }}"
mode: "0644"
-25
View File
@@ -1,25 +0,0 @@
---
- name: Destroy
hosts: localhost
gather_facts: false
tasks:
- name: Remove the container of every platform
community.docker.docker_container:
name: "{{ item.name }}"
state: absent
loop: "{{ molecule_yml.platforms }}"
loop_control:
label: "{{ item.name }}"
# The generated key pair is not kept around, a stale private key in the working tree is asking for trouble.
- name: Remove the ssh fixtures
ansible.builtin.file:
path: "{{ molecule_scenario_directory }}/files/ssh"
state: absent
- name: Empty the instance config
ansible.builtin.copy:
content: "[]"
dest: "{{ molecule_instance_config }}"
mode: "0600"
-24
View File
@@ -1,24 +0,0 @@
---
driver:
name: default
options:
managed: true
login_cmd_template: "docker exec --interactive --tty {instance} bash"
platforms:
- name: unix-users-archlinux
image: docker.io/library/archlinux:base
- name: unix-users-debian
image: docker.io/library/debian:13
- name: unix-users-fedora
image: registry.fedoraproject.org/fedora:43
provisioner:
name: ansible
# The role under test is the project directory itself, so its parent has to be on the roles path.
env:
ANSIBLE_ROLES_PATH: "${MOLECULE_PROJECT_DIRECTORY}/.."
config_options:
defaults:
interpreter_python: auto_silent
-37
View File
@@ -1,37 +0,0 @@
---
- name: Prepare
hosts: all
gather_facts: false
vars:
# The base images ship neither a python interpreter for ansible nor ssh-keygen, which the role shells out to.
_bootstrap: |
set -eu
if command -v pacman > /dev/null; then
pacman --sync --refresh --noconfirm openssh python shadow
elif command -v apt-get > /dev/null; then
apt-get update
apt-get install --yes openssh-client passwd python3
else
dnf install --assumeyes openssh-clients python3 shadow-utils
fi
tasks:
# The raw command is wrapped explicitly, because the bootstrap relies on shell builtins.
- name: Bootstrap the python interpreter and the tools required by the role
ansible.builtin.raw: "/bin/sh -c {{ _bootstrap | quote }}"
changed_when: true
# The removal paths of the role can only be observed on objects that exist before the role runs.
- name: Seed the objects the converge removes again
hosts: all
tasks:
- name: Create the group that the converge removes
ansible.builtin.group:
name: molecule-obsolete
state: present
- name: Create the user that the converge removes
ansible.builtin.user:
name: molecule-dave
group: users
state: present
-4
View File
@@ -1,4 +0,0 @@
---
# The role has no role dependencies, but molecule warns about the missing file.
roles: []
-143
View File
@@ -1,143 +0,0 @@
---
- name: Verify
hosts: all
vars:
_alice_home: /home/molecule-alice
_bob_home: /home/molecule-bob
_expected_modes:
/home/molecule-alice/.ssh: "0700"
/home/molecule-alice/.ssh/authorized_keys: "0600"
/home/molecule-alice/.ssh/config: "0644"
/home/molecule-alice/.ssh/molecule.ed25519.key: "0600"
/home/molecule-alice/.ssh/molecule.ed25519.key.pub: "0644"
/home/molecule-alice/.forward: "0644"
/home/molecule-alice/.netrc: "0600"
/home/molecule-alice/.bashrc.d: "0755"
/home/molecule-alice/.bashrc.d/molecule.bashrc: "0644"
/home/molecule-alice/.cache: "0755"
/home/molecule-alice/.config: "0755"
/home/molecule-alice/.local/share: "0755"
/home/molecule-alice/.local/state: "0755"
# Only files the role writes are listed, .bashrc originates from the lineinfile task.
_expected_contents:
/home/molecule-alice/.forward: "alice@example.local"
/home/molecule-alice/.netrc: "machine hostname.local login alice password secret"
/home/molecule-alice/.ssh/config: " StrictHostKeyChecking no"
/home/molecule-alice/.bashrc.d/molecule.bashrc: "alias dcd='docker compose down'"
/home/molecule-alice/.bashrc: "source \"/home/molecule-alice/.bashrc.d/molecule.bashrc\""
tasks:
- name: Stat the files of molecule-alice
ansible.builtin.stat:
path: "{{ item.key }}"
register: _alice_files
loop: "{{ _expected_modes | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: Assert that the files of molecule-alice exist with the expected mode and owner
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.mode == item.item.value
- item.stat.pw_name == 'molecule-alice'
- item.stat.gr_name == 'molecule-alice'
fail_msg: >-
{{ item.item.key }} has mode {{ item.stat.mode | default('none') }} and owner
{{ item.stat.pw_name | default('none') }}:{{ item.stat.gr_name | default('none') }}
instead of {{ item.item.value }} and molecule-alice:molecule-alice
loop: "{{ _alice_files.results }}"
loop_control:
label: "{{ item.item.key }}"
- name: Read the files of molecule-alice
ansible.builtin.slurp:
src: "{{ item.key }}"
register: _alice_contents
loop: "{{ _expected_contents | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: Assert that the files of molecule-alice hold the configured values
ansible.builtin.assert:
that: item.item.value in (item.content | b64decode).splitlines()
fail_msg: "{{ item.item.key }} does not contain the line {{ item.item.value }}"
loop: "{{ _alice_contents.results }}"
loop_control:
label: "{{ item.item.key }}"
- name: Read the authorized_keys and the extracted public key of molecule-alice
ansible.builtin.slurp:
src: "{{ item }}"
register: _alice_keys
loop:
- "{{ _alice_home }}/.ssh/authorized_keys"
- "{{ _alice_home }}/.ssh/molecule.ed25519.key.pub"
- name: Assert that the authorized key carries its options and matches the extracted public key
vars:
_authorized_key: "{{ (_alice_keys.results[0].content | b64decode).splitlines() | select('search', 'ssh-ed25519') | first }}"
_public_key: "{{ _alice_keys.results[1].content | b64decode | trim }}"
ansible.builtin.assert:
that:
- _authorized_key.startswith('command="/usr/bin/true",environment="EDITOR=vi" ')
- _public_key.startswith('ssh-ed25519 ')
- _public_key.split()[1] == _authorized_key.split()[2]
fail_msg: "the authorized key of molecule-alice does not match the key extracted from its private key"
- name: Read the user and group database
ansible.builtin.getent:
database: "{{ item }}"
loop:
- passwd
- group
- shadow
- name: Assert that molecule-alice was created as declared
vars:
_entry: "{{ ansible_facts['getent_passwd']['molecule-alice'] }}"
ansible.builtin.assert:
that:
- _entry[1] == '4242'
- _entry[2] == '4242'
- _entry[3] == 'Alice'
- _entry[4] == _alice_home
- _entry[5] == '/bin/bash'
fail_msg: "molecule-alice was created as {{ _entry }}"
- name: Assert that the integer gid of the group molecule-alice was applied
ansible.builtin.assert:
that: ansible_facts['getent_group']['molecule-alice'][1] == '4242'
fail_msg: "the group molecule-alice has gid {{ ansible_facts['getent_group']['molecule-alice'][1] }}"
# A random salt would produce a new hash on every run, which the idempotence step would report as a change.
- name: Assert that the password hash is derived from a deterministic salt
vars:
_expected_hash: "{{ 'alice' | password_hash('sha512', 'molecule-alice' | hash('sha512') | truncate(16, true, '')) }}"
ansible.builtin.assert:
that: ansible_facts['getent_shadow']['molecule-alice'][0] == _expected_hash
fail_msg: "the password hash of molecule-alice is not reproducible and therefore changes on every run"
- name: Assert that molecule-dave and the group molecule-obsolete were removed
ansible.builtin.assert:
that:
- "'molecule-dave' not in ansible_facts['getent_passwd']"
- "'molecule-obsolete' not in ansible_facts['getent_group']"
fail_msg: "the removal of molecule-dave or of the group molecule-obsolete did not happen"
- name: Stat the home of molecule-bob and the files he did not ask for
ansible.builtin.stat:
path: "{{ item }}"
register: _bob_files
loop:
- "{{ _bob_home }}"
- "{{ _bob_home }}/.ssh"
- "{{ _bob_home }}/.forward"
- "{{ _bob_home }}/.netrc"
- name: Assert that molecule-bob got a home but none of the optional files
ansible.builtin.assert:
that:
- _bob_files.results[0].stat.exists
- not _bob_files.results[1:] | map(attribute='stat.exists') | select | list
fail_msg: "molecule-bob has files that were never declared for him"
-6
View File
@@ -1,6 +0,0 @@
---
collections:
# The btrfs_subvolume module has been added in community.general 6.6.0.
- name: community.general
version: ">=6.6.0"
+17
View File
@@ -0,0 +1,17 @@
---
- name: "Exist file {{ skel_file }}"
ansible.builtin.stat:
path: "{{ user_user_home }}/{{ skel_file }}"
register: _skel_file
- name: Copy skel file
when: _skel_file.stat is defined and
_skel_file.stat.exist is defined and
not _skel_file.stat.exist
ansible.builtin.copy:
src: "/etc/skel/{{ _skel_file }}"
dest: "{{ user_user_home }}/{{ skel_file }}"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0600"
-18
View File
@@ -1,18 +0,0 @@
---
- name: "Check if the skel file exists: /etc/skel/{{ skel_file }}"
ansible.builtin.stat:
path: "/etc/skel/{{ skel_file }}"
register: _unix_users_skel_file
# force: false keeps an already customized dotfile in the home directory untouched.
- name: "Copy skel file: {{ skel_file }}"
ansible.builtin.copy:
src: "/etc/skel/{{ skel_file }}"
dest: "{{ _unix_users_home }}/{{ skel_file }}"
remote_src: true
force: false
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0644"
when: _unix_users_skel_file.stat.exists
@@ -3,7 +3,7 @@
- name: "Determine shell rc directory"
ansible.builtin.set_fact:
# path_join examples: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/path_join_filter.html#examples
_shell_rc_file: "{{ (_unix_users_home, '.bashrc.d', shell_rc_file.file) | path_join }}"
_shell_rc_file: "{{ (user_user_home, '.bashrc.d', shell_rc_file.file) | path_join }}"
- name: "Create shell rc directory: {{ _shell_rc_file | dirname }}"
ansible.builtin.file:
@@ -23,10 +23,8 @@
- name: "Source shell rc file: {{ _shell_rc_file }}"
ansible.builtin.lineinfile:
path: "{{ _unix_users_home }}/.bashrc"
path: "{{ user_user_home }}/.bashrc"
line: "source \"{{ _shell_rc_file }}\""
create: true
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0644"
state: "present"
+14
View File
@@ -0,0 +1,14 @@
---
- name: "Create unix group with random gid: {{ unix_group.key }}"
ansible.builtin.group:
name: "{{ unix_group.key }}"
state: "{{ unix_group.value.state | default('present') }}"
when: unix_group.value.gid is not defined or unix_group.value.gid is defined and unix_group.value.gid | length <= 0
- name: "Create unix group with pre-defined gid: {{ unix_group.key }}"
ansible.builtin.group:
name: "{{ unix_group.key }}"
gid: "{{ unix_group.value.gid }}"
state: "{{ unix_group.value.state | default('present') }}"
when: unix_group.value.gid is defined and unix_group.value.gid | length > 0
-8
View File
@@ -1,8 +0,0 @@
---
- name: "Create unix group: {{ unix_group.key }}"
ansible.builtin.group:
name: "{{ unix_group.key }}"
# The boolean form of default also omits an empty gid, which means let the system assign one.
gid: "{{ unix_group.value.gid | default(omit, true) }}"
state: "{{ unix_group.value.state | default('present') }}"
@@ -2,7 +2,7 @@
- name: "Define home directory for unix user: {{ unix_user.key }}"
ansible.builtin.set_fact:
_unix_users_home: "{{ unix_user.value.home | default('/home/' + unix_user.key) }}"
user_user_home: "{{ unix_user.value.home | default('/home/' + unix_user.key) }}"
- name: "Create btrfs volume for unix user: {{ unix_user.key }}"
when: unix_user.value.btrfs is defined and
@@ -10,9 +10,16 @@
block:
- name: "Find btrfs device"
ansible.builtin.command:
cmd: /bin/bash -c "findmnt -no SOURCE -T {{ _unix_users_home }} | sed 's/\[.*\]//'"
cmd: /bin/bash -c "findmnt -no SOURCE -T {{ user_user_home }} | sed 's/\[.*\]//'"
register: _unix_users_btrfs_device
changed_when: false
failed_when: _unix_users_btrfs_device.rc != 0
changed_when: _unix_users_btrfs_device.rc == 0
- name: "Found btrfs device"
ansible.builtin.debug:
msg: _unix_users_btrfs_device.stdout
when: _unix_users_debug is defined and
_unix_users_debug is true
- name: "Determine filesystem of device"
ansible.builtin.set_fact:
@@ -23,33 +30,77 @@
msg: "Determined device {{ _unix_users_btrfs_device.stdout }} does not have a btrfs filesystem"
when: _unix_users_device_filesystem != 'btrfs'
# The subvolume stays root owned until the user exists. It is chowned further below, after the user was created.
- name: "Create btrfs volume for unix user: {{ unix_user.key }}"
community.general.btrfs_subvolume:
filesystem_device: "{{ _unix_users_btrfs_device.stdout }}"
name: "{{ _unix_users_home }}"
name: "{{ user_user_home }}"
state: present
- name: "Adapt home dir permissions"
ansible.builtin.file:
path: "{{ user_user_home }}"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
state: directory
mode: "0755"
- name: "Create unix user: {{ unix_user.key }}"
- name: "Create unix user without additional groups and uid: {{ unix_user.key }}"
ansible.builtin.user:
name: "{{ unix_user.key }}"
uid: "{{ unix_user.value.uid | default(omit) }}"
group: "{{ unix_user.value.group | default('users') }}"
groups: "{{ unix_user.value.groups | default(omit) }}"
comment: "{{ unix_user.value.name | default(omit) }}"
comment: "{{ unix_user.value.name }}"
create_home: "{{ unix_user.value.create_home | default(true) }}"
home: "{{ _unix_users_home }}"
home: "{{ user_user_home }}"
shell: "{{ unix_user.value.shell | default('/bin/bash') }}"
# The salt is derived from the user name, a random one would produce a new hash and a change on every run.
password: "{{ unix_user.value.password | password_hash('sha512', unix_user.key | hash('sha512') | truncate(16, true, '')) if unix_user.value.password is defined and unix_user.value.password | length > 0 else '!' }}"
password: "{{ unix_user.value.password | password_hash('sha512') if unix_user.value.password is defined and unix_user.value.password | length > 0 else '!' }}"
state: present
when: unix_user.value.groups is not defined and unix_user.value.uid is not defined
- name: "Create unix user without additional groups and with uid: {{ unix_user.key }}"
ansible.builtin.user:
name: "{{ unix_user.key }}"
uid: "{{ unix_user.value.uid }}"
group: "{{ unix_user.value.group | default('users') }}"
comment: "{{ unix_user.value.name }}"
create_home: "{{ unix_user.value.create_home | default(true) }}"
home: "{{ user_user_home }}"
shell: "{{ unix_user.value.shell | default('/bin/bash') }}"
password: "{{ unix_user.value.password | password_hash('sha512') if unix_user.value.password is defined and unix_user.value.password | length > 0 else '!' }}"
state: present
when: unix_user.value.groups is not defined and unix_user.value.uid is defined
- name: "Create unix user with additional groups and uid: {{ unix_user.key }}"
ansible.builtin.user:
name: "{{ unix_user.key }}"
uid: "{{ unix_user.value.uid }}"
group: "{{ unix_user.value.group | default('users') }}"
groups: "{{ unix_user.value.groups | join(',') }}"
comment: "{{ unix_user.value.name }}"
create_home: "{{ unix_user.value.create_home | default(true) }}"
home: "{{ user_user_home }}"
shell: "{{ unix_user.value.shell | default('/bin/bash') }}"
password: "{{ unix_user.value.password | password_hash('sha512') if unix_user.value.password is defined and unix_user.value.password | length > 0 else '!' }}"
state: present
when: unix_user.value.groups is defined and unix_user.value.uid is defined
- name: "Create unix user with additional groups and without uid: {{ unix_user.key }}"
ansible.builtin.user:
name: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
groups: "{{ unix_user.value.groups | join(',') }}"
comment: "{{ unix_user.value.name }}"
create_home: "{{ unix_user.value.create_home | default(true) }}"
home: "{{ user_user_home }}"
shell: "{{ unix_user.value.shell | default('/bin/bash') }}"
password: "{{ unix_user.value.password | password_hash('sha512') if unix_user.value.password is defined and unix_user.value.password | length > 0 else '!' }}"
state: present
when: unix_user.value.groups is defined and unix_user.value.uid is not defined
- name: "Adapt permissions and copy skel for unix user: {{ unix_user.key }}"
when: unix_user.value.btrfs is defined and
unix_user.value.btrfs
block:
- name: "Copy skel files"
ansible.builtin.include_tasks: copy_skel_file.yml
ansible.builtin.include_tasks: copy_skel_file.yaml
loop_control:
loop_var: skel_file
with_items:
@@ -58,7 +109,7 @@
- ".bashrc"
- name: "Change permission unix users home dir: {{ unix_user.key }}"
ansible.builtin.file:
path: "{{ _unix_users_home }}"
path: "{{ user_user_home }}"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
state: directory
@@ -66,17 +117,16 @@
- name: "Create .ssh directory for unix user: {{ unix_user.key }}"
ansible.builtin.file:
path: "{{ _unix_users_home }}/.ssh"
path: "{{ user_user_home }}/.ssh"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0700"
state: directory
when: unix_user.value.ssh is defined
- name: "Create authorized_keys file for unix user: {{ unix_user.key }}"
ansible.builtin.template:
src: authorized_keys.j2
dest: "{{ _unix_users_home }}/.ssh/authorized_keys"
dest: "{{ user_user_home }}/.ssh/authorized_keys"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0600"
@@ -84,60 +134,60 @@
- name: "Remove authorized_keys file for unix user: {{ unix_user.key }}"
ansible.builtin.file:
path: "{{ _unix_users_home }}/.ssh/authorized_keys"
path: "{{ user_user_home }}/.ssh/authorized_keys"
state: absent
when: unix_user.value.ssh.authorized_keys is not defined or unix_user.value.ssh.authorized_keys | length <= 0
# The relative source is resolved against the files directory of the playbook, like the lookup in authorized_keys.j2.
- name: "Create private SSH keys for unix user: {{ unix_user.key }}"
ansible.builtin.copy:
src: "ssh/private_keys/{{ item }}"
dest: "{{ _unix_users_home }}/.ssh/{{ item }}"
src: "{{ playbook_dir }}/ssh/private_keys/{{ item }}"
dest: "{{ user_user_home }}/.ssh/{{ item }}"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0600"
no_log: true
with_items:
- "{{ unix_user.value.ssh.private_keys }}"
when: unix_user.value.ssh.private_keys is defined and unix_user.value.ssh.private_keys | length > 0
- name: "Extract public SSH keys from private keys for unix user: {{ unix_user.key }}"
ansible.builtin.shell:
cmd: "ssh-keygen -y -f {{ _unix_users_home }}/.ssh/{{ item }} > {{ _unix_users_home }}/.ssh/{{ item }}.pub"
creates: "{{ _unix_users_home }}/.ssh/{{ item }}.pub"
args:
executable: /bin/bash
cmd: "ssh-keygen -y -f {{ user_user_home }}/.ssh/{{ item }} > {{ user_user_home }}/.ssh/{{ item }}.pub"
creates: "{{ user_user_home }}/.ssh/{{ item }}.pub"
with_items:
- "{{ unix_user.value.ssh.private_keys }}"
when: unix_user.value.ssh.private_keys is defined and unix_user.value.ssh.private_keys | length > 0
- name: "Correct permissions of public SSH keys for unix user: {{ unix_user.key }}"
ansible.builtin.file:
path: "{{ _unix_users_home }}/.ssh/{{ item }}.pub"
path: "{{ user_user_home }}/.ssh/{{ item }}.pub"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0644"
with_items:
- "{{ unix_user.value.ssh.private_keys }}"
when: unix_user.value.ssh.private_keys is defined and unix_user.value.ssh.private_keys | length > 0
when: unix_user.value.ssh.private_keys is defined and unix_user.value.ssh.private_keys | length >= 0
- name: "Create custom SSH client config for unix user: {{ unix_user.key }}"
ansible.builtin.template:
src: config.j2
dest: "{{ _unix_users_home }}/.ssh/config"
dest: "{{ user_user_home }}/.ssh/config"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0644"
when: unix_user.value.ssh.config is defined and unix_user.value.ssh.config | length > 0
when: unix_user.value.ssh.config is defined and unix_user.value.ssh.config | length >= 0
- name: "Remove custom SSH client config for unix user: {{ unix_user.key }}"
ansible.builtin.file:
path: "{{ _unix_users_home }}/.ssh/config"
path: "{{ user_user_home }}/.ssh/config"
state: absent
when: unix_user.value.ssh.config is not defined or unix_user.value.ssh.config | length <= 0
when: unix_user.value.ssh.config is not defined
- name: "Create .forward file to forward emails for unix user: {{ unix_user.key }}"
ansible.builtin.template:
src: forward.j2
dest: "{{ _unix_users_home }}/.forward"
dest: "{{ user_user_home }}/.forward"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0644"
@@ -145,7 +195,7 @@
- name: "Remove .forward file to forward emails for unix user: {{ unix_user.key }}"
ansible.builtin.file:
path: "{{ _unix_users_home }}/.forward"
path: "{{ user_user_home }}/.forward"
state: absent
when: unix_user.value.email is not defined
@@ -157,14 +207,14 @@
mode: "0755"
state: "directory"
with_items:
- "{{ unix_user.value.xdg.dirs.cache | default(_unix_users_home + '/.cache') }}"
- "{{ unix_user.value.xdg.dirs.config | default(_unix_users_home + '/.config') }}"
- "{{ unix_user.value.xdg.dirs.data | default(_unix_users_home + '/.local/share') }}"
- "{{ unix_user.value.xdg.dirs.state | default(_unix_users_home + '/.local/state') }}"
- "{{ unix_user.value.xdg.dirs.cache | default(user_user_home + '/.cache') }}"
- "{{ unix_user.value.xdg.dirs.config | default(user_user_home + '/.config') }}"
- "{{ unix_user.value.xdg.dirs.data | default(user_user_home + '/.local/share') }}"
- "{{ unix_user.value.xdg.dirs.state | default(user_user_home + '/.local/state') }}"
- name: "Create shell rc files"
when: unix_user.value.shell_rc_files is defined
ansible.builtin.include_tasks: create_shell_rc_file.yml
ansible.builtin.include_tasks: create_shell_rc_file.yaml
with_items:
- "{{ unix_user.value.shell_rc_files }}"
loop_control:
@@ -174,8 +224,7 @@
when: unix_user.value.netrc is defined and unix_user.value.netrc | length > 0
ansible.builtin.template:
src: netrc.j2
dest: "{{ _unix_users_home }}/.netrc"
dest: "{{ user_user_home }}/.netrc"
owner: "{{ unix_user.key }}"
group: "{{ unix_user.value.group | default('users') }}"
mode: "0600"
no_log: true
+4 -4
View File
@@ -1,7 +1,7 @@
---
- name: Remove unix user
ansible.builtin.include_tasks: remove_unix_user.yml
ansible.builtin.include_tasks: remove_unix_user.yaml
with_dict: "{{ unix_users }}"
loop_control:
loop_var: unix_user
@@ -11,7 +11,7 @@
unix_user.value.state == 'absent'
- name: Remove unix groups
ansible.builtin.include_tasks: remove_unix_group.yml
ansible.builtin.include_tasks: remove_unix_group.yaml
with_dict: "{{ unix_groups }}"
loop_control:
loop_var: unix_group
@@ -21,7 +21,7 @@
unix_group.value.state == 'absent'
- name: Create unix groups
ansible.builtin.include_tasks: create_unix_group.yml
ansible.builtin.include_tasks: create_unix_group.yaml
with_dict: "{{ unix_groups }}"
loop_control:
loop_var: unix_group
@@ -33,7 +33,7 @@
)
- name: Create unix users
ansible.builtin.include_tasks: create_unix_user.yml
ansible.builtin.include_tasks: create_unix_user.yaml
no_log: true
with_dict: "{{ unix_users }}"
loop_control:
+7
View File
@@ -0,0 +1,7 @@
---
- name: Remove unix user {{ unix_user.key }}
ansible.builtin.user:
name: "{{ unix_user.key }}"
state: absent
remove: true
-37
View File
@@ -1,37 +0,0 @@
---
- name: "Define home directory for unix user: {{ unix_user.key }}"
ansible.builtin.set_fact:
_unix_users_home: "{{ unix_user.value.home | default('/home/' + unix_user.key) }}"
# userdel cannot remove a btrfs subvolume. Such a home is deleted afterwards via the btrfs_subvolume module.
- name: "Remove unix user: {{ unix_user.key }}"
ansible.builtin.user:
name: "{{ unix_user.key }}"
state: absent
remove: "{{ not (unix_user.value.btrfs | default(false)) }}"
- name: "Remove btrfs home of unix user: {{ unix_user.key }}"
when: unix_user.value.btrfs is defined and
unix_user.value.btrfs
block:
- name: "Stat home directory"
ansible.builtin.stat:
path: "{{ _unix_users_home }}"
register: _unix_users_home_stat
# findmnt fails on a missing path, so the device is only determined as long as the home directory exists.
- name: "Delete btrfs subvolume of an existing home directory"
when: _unix_users_home_stat.stat.exists
block:
- name: "Find btrfs device"
ansible.builtin.command:
cmd: /bin/bash -c "findmnt -no SOURCE -T {{ _unix_users_home }} | sed 's/\[.*\]//'"
register: _unix_users_btrfs_device
changed_when: false
- name: "Delete btrfs subvolume: {{ _unix_users_home }}"
community.general.btrfs_subvolume:
filesystem_device: "{{ _unix_users_btrfs_device.stdout }}"
name: "{{ _unix_users_home }}"
state: absent
+1 -1
View File
@@ -5,7 +5,7 @@
{% if shell_rc_file.functions is defined %}
{% for function in shell_rc_file.functions %}
function {{ function.name }} {
{{ function.value | indent(2, True) }}
{{ function.body | indent(2, True) }}
}
{% endfor %}