Commit Graph
73 Commits
Author SHA1 Message Date
volker.raschekandCopilot afd4288ebd refactor: use the yml extension for every yaml file
Lint Markdown files / markdown-lint (push) Successful in 14s
Molecule / Molecule (push) Successful in 5m19s
Ansible Linter / ansible-lint (push) Successful in 4m22s
The repository mixed both extensions, the molecule scenario used yml while the role itself used yaml. The yml
extension is the one ansible-galaxy and molecule generate, so it is applied throughout and across the sibling roles.

The include_tasks calls in tasks/main.yml and tasks/create_unix_user.yml name their target explicitly and were
adjusted accordingly, otherwise the role would no longer find the included task files.

Co-authored-by: Copilot <copilot@github.com>
2026-09-10 22:06:43 +02:00
volker.raschekandCopilot e580de5e4c ci: install the molecule collections before linting
Lint Markdown files / markdown-lint (push) Successful in 11s
Ansible Linter / ansible-lint (push) Successful in 2m10s
Molecule / Molecule (push) Successful in 5m38s
Release Ansible Role / Release Ansible Role (push) Successful in 3m13s
ansible-lint does not only lint the role but the molecule scenario as well, and the scenario creates its containers
with community.docker. Only the runtime requirements of the role were installed, so the linter could not resolve
community.docker.docker_container and failed the syntax check.

The collection list of the scenario is a superset of the runtime requirements, therefore it is used instead of adding
a test only dependency to requirements.yml.

Co-authored-by: Copilot <copilot@github.com>
2026-09-10 17:51:14 +02:00
volker.raschekandCopilot 9b77592091 ci: run the molecule scenario on every push
Lint Markdown files / markdown-lint (push) Successful in 7s
Ansible Linter / ansible-lint (push) Failing after 3m12s
Molecule / Molecule (push) Successful in 6m15s
Without a workflow the scenario only runs when somebody remembers to run it locally, which is exactly the situation
the tests were written to end.

The repository is checked out into a directory named after the role and not after the repository, because the scenario
includes the role by its name and molecule puts the parent of the project directory on the roles path.

Co-authored-by: Copilot <copilot@github.com>
2026-09-10 10:16:18 +02:00
volker.raschekandCopilot a343205fd3 test(molecule): cover the role with a molecule scenario
The role changed a lot and none of it was verified against a real system so far. The scenario starts one container per
supported distribution family, applies the role and asserts afterwards that the users and groups exist as declared,
that the managed files carry the documented mode, owner and content, that a user without optional settings does not
receive any of the optional files and that a user declared as absent is gone again.

The idempotence step is the actual reason for the scenario. The deterministic password salt and the btrfs device
lookup were changed to stop reporting a change on every run, and only a second converge proves that.

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 create and removed again during destroy, so no
private key material ends up in the repository. The generated files are ignored for the case that a destroy never
runs.

Co-authored-by: Copilot <copilot@github.com>
2026-09-10 09:54:00 +02:00
volker.raschekandCopilot 00f465e5e1 fix(meta): declare the versions the role actually requires
The role claimed to work with ansible 2.9, but two of its building blocks did not exist back then. The path_join
filter was introduced in ansible-base 2.10 and the btrfs_subvolume module in community.general 6.6.0, which in turn
requires ansible-core 2.11. Running the role on 2.9 therefore failed with an undefined filter instead of a readable
message about an unsupported control node.

The collection is now declared in a requirements.yml, mirroring the layout of the certificate_authority role, and the
lower bound is pinned because an older community.general still resolves but lacks the btrfs module. The linter
workflow installs that file, because ansible-lint cannot resolve the btrfs_subvolume tasks without the collection
being present, which only surfaced in CI where no collections are preinstalled.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 5f757914be fix(tasks): remove the btrfs subvolume of a deleted unix user
Deleting a user whose home is a btrfs subvolume left that subvolume behind. The role passed remove=true to the user
module, but userdel removes a home directory with rmdir, which refuses to delete a subvolume that still holds nested
subvolumes or is otherwise not empty. The home therefore survived the removal and blocked a later recreation of the
same user, because btrfs_subvolume then found the path already occupied.

The subvolume is now deleted explicitly by the same module that created it, which keeps the creation and the removal
symmetric. Snapshots are stored outside the subvolume, so a btrbk based backup keeps the data available even though
the home itself is gone.

The removal is skipped when the home directory no longer exists, since findmnt fails on a missing path.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot f222b1c615 fix(tasks): hide secret material written by the netrc and ssh key tasks
The `no_log: true` on the include in tasks/main.yaml only censors the include statement and its loop item. It is not
inherited by the included tasks, which was verified with a minimal playbook: a debug task inside an included file prints
its message in clear even though the include itself is censored.

Two tasks therefore leaked secrets. The netrc template exposes every machine password in its rendered content, and the
copy of the private ssh keys exposes the key material itself. Both are printed by the diff mode, which is exactly the
mode used when reviewing what a run would change. They are marked `no_log` individually now.

The user task is deliberately left alone. Its `password` argument is declared `no_log` in the argument spec of the
module, so ansible already censors it, and a task level flag would only make the remaining output useless.

The blanket flag on the include stays in place, because the loop item still carries the plaintext password.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 279ee1929a fix(tasks): create the bashrc before sourcing a drop-in from it
The lineinfile task appends the source line to `~/.bashrc` but never declared `create`, so it aborted with "Destination
does not exist" whenever that file was missing. This hits every user configured with `create_home: false`, and it hit
every btrfs user as well, because their home is an empty subvolume and useradd therefore skips the skel rollout.

`mode` is added along with it, otherwise a newly created bashrc would inherit whatever the umask happens to be. The
existing `owner` and `group` were dead settings until now, since the file was never created by this task.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 41a645f748 fix(tasks): accept an integer gid when creating a unix group
The two group tasks differed only in whether `gid` was passed, and the branch between them tested
`unix_group.value.gid | length`. The length filter has no meaning for an integer and raises "object of type 'int' has no
len()". The example in defaults/main.yaml declares `gid: 1001` unquoted, so the documented usage aborted the run, while
the quoted variant in the README happened to work.

Both tasks are merged into one that passes the gid through `default(omit, true)`. The boolean form of the filter is
required to keep the previous meaning of an empty gid, which is to let the system assign one. The only value that
changes semantics is gid 0, which is the root group and outside the scope of this role.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 517d9c1c54 refactor(tasks): prefix the home directory fact with the role name
`set_fact` writes into the play scope, so `user_user_home` outlived the loop iteration that set it and collided with any
playbook variable of the same name. Worse, if the defining task were ever skipped, every following task would silently
operate on the home directory of the previously processed user.

Renaming it to `_unix_users_home` marks it as role internal and matches the underscore prefix already used by the other
internal facts in this role. Purely mechanical, no behaviour changes.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 095705643c fix(tasks): derive a deterministic password salt per unix user
`password_hash('sha512')` without an explicit salt generates a new random salt on every invocation. The resulting hash
differed on each run, so the user module rewrote /etc/shadow and reported a change every time the role was applied. This
was the single biggest obstacle to a green idempotence check.

The salt is now derived from the user name, which keeps the hash stable across runs while still giving every account its
own salt, so two users sharing a password do not end up with an identical hash.

Verified locally: repeated runs produce a byte identical hash, and different user names produce different ones.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot c34b59633c fix(tasks): stop reporting a change when looking up the btrfs device
`findmnt` only reads the mount table, but the task declared `changed_when` on a successful return code and therefore
reported a change on every single run. Any btrfs backed user made the whole play non idempotent, which an idempotence
check would flag as soon as one exists.

The `failed_when` on a non zero return code is dropped along with it, since that is exactly what the command module does
by default.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 68ee8ebbcd fix(tasks)!: repair the broken guards in the unix user creation
Several defects accumulated in this task file and are fixed together, because they overlap in the same code paths.

The debug task was dead code. Its `msg` lacked the Jinja delimiters and would have printed the literal string
`_unix_users_btrfs_device.stdout`, and it was gated on `_unix_users_debug`, a variable that is neither defined in
defaults nor documented anywhere. It is removed instead of repaired, since the failure path already reports the device.

The four `ansible.builtin.user` tasks differed only in whether `uid` and `groups` were passed. They are collapsed into a
single task using `default(omit)`, which removes the risk that a fix lands in one of the four copies only. This also
fixes `comment`, which dereferenced `unix_user.value.name` unconditionally and aborted for every user that did not set
the undocumented and supposedly optional key.

Two conditions compared a length against zero with `>=`, which is true for any list. As a result an empty `ssh.config`
still produced a config file, and the removal counterpart never triggered. The create and remove pair for `.ssh/config`
now mirrors the one already used for `authorized_keys`.

The private key source was resolved through `{{ playbook_dir }}`, while the authorized key lookup in the template uses
the regular relative search path. Both now use the same mechanism, which is a superset of the previous location, so
existing playbook layouts keep working, and the role becomes testable from a molecule scenario.

BREAKING CHANGE:
The `.ssh` directory is only created when a user actually declares an `ssh` key, and an empty `ssh.config` list now
removes the client config instead of writing an empty one. Users who relied on the role to pre create an empty `~/.ssh`
have to declare `ssh: {}` explicitly.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 91ba7845da fix(tasks): chown the btrfs home directory only after the user exists
The btrfs block chowned the freshly created subvolume to the user right after `btrfs_subvolume`, but the four
`ansible.builtin.user` tasks that actually create that user run further below. On a first run the chown therefore
aborted with an invalid user error, which made the whole btrfs code path unusable.

The task is removed rather than reordered, because an identical chown already exists in the "Adapt permissions and copy
skel" block below the user creation. That block also runs the skel copy first, so the file ownership set there is not
overwritten afterwards. The subvolume simply stays root owned for a few tasks longer, which a comment now records.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 242f291200 fix(templates): render the documented function value in shell rc files
The template read `function.body`, while README and defaults/main.yaml document the key as `functions[].value`. Every
documented example therefore rendered an empty function body, and with a strict undefined policy the template failed
outright.

The documentation is the contract for this role, so the template follows it instead of the other way around.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschekandCopilot 68be6c5cdf fix(tasks): repair the never executed skel file copy
The task guarded itself with `_skel_file.stat.exist`, but the stat module returns `exists`. The `is defined` check on the
misspelled attribute therefore always evaluated to false and the whole task was silently skipped, so no skel file was
ever copied into a home directory.

Two further defects surfaced once the guard was corrected. The source path interpolated the registered result dict
`_skel_file` instead of the loop variable `skel_file`, and the copy module read from the control node because
`remote_src` was missing, so `/etc/skel` of the managed host was never consulted.

The stat now probes the source instead of the destination. Distributions ship different skel files, for example Debian
has no `.bash_profile`, and copying a non existing remote source would abort the run. The "only copy when absent"
behaviour is delegated to `force: false`, which the copy module implements natively. The register variable is prefixed
with the role name so it can no longer be confused with the loop variable. The mode is corrected to 0644, the mode
`/etc/skel` uses for its dotfiles.

Co-authored-by: Copilot <copilot@github.com>
2026-09-09 21:39:42 +02:00
volker.raschek a4b28d441b fix(ci): add workflow dispatch for releases
Ansible Linter / ansible-lint (push) Successful in 18s
Lint Markdown files / markdown-lint (push) Successful in 5s
2026-01-11 21:16:12 +01:00
volker.raschek 1ec88f37fb fix(ci): use dynamic github repository name
Ansible Linter / ansible-lint (push) Successful in 19s
Lint Markdown files / markdown-lint (push) Successful in 4s
Release Ansible Role / Release Ansible Role (push) Successful in 56s
2026-01-07 16:09:07 +01:00
volker.raschek 09c4173eaf docs(ci): get steps a dedicated name
Ansible Linter / ansible-lint (push) Successful in 19s
Lint Markdown files / markdown-lint (push) Successful in 4s
2026-01-07 16:02:12 +01:00
volker.raschek 14f856fdea fix(ci): add release workflow
Ansible Linter / ansible-lint (push) Successful in 20s
Lint Markdown files / markdown-lint (push) Successful in 4s
Release to Ansible Galaxy / release (push) Successful in 59s
2026-01-07 16:00:01 +01:00
volker.raschek 86d85c431c fix!: rename environments to envs
Ansible Linter / ansible-lint (push) Successful in 19s
Lint Markdown files / markdown-lint (push) Successful in 4s
2026-01-07 11:16:12 +01:00
volker.raschek 88e9a163e1 docs: support environment variables in authorized_keys file
Ansible Linter / ansible-lint (push) Successful in 18s
Lint Markdown files / markdown-lint (push) Successful in 5s
2026-01-07 10:35:40 +01:00
volker.raschek 69491c9aa0 feat: support environment variables in authorized_keys file
Ansible Linter / ansible-lint (push) Successful in 21s
Lint Markdown files / markdown-lint (push) Successful in 5s
2026-01-07 10:28:13 +01:00
volker.raschek 47d9a58910 fix: replace deprecated INJECT_FACTS_AS_VARS
Ansible Linter / ansible-lint (push) Successful in 17s
Lint Markdown files / markdown-lint (push) Successful in 4s
2026-01-05 10:28:33 +01:00
volker.raschek e4c12b9856 fix: set btrfs device
Ansible Linter / ansible-lint (push) Successful in 24s
Lint Markdown files / markdown-lint (push) Successful in 4s
2025-11-22 23:42:42 +01:00
volker.raschek 13a4c84978 fix(npm): remove npm dependencies
Ansible Linter / ansible-lint (push) Successful in 20s
Lint Markdown files / markdown-lint (push) Successful in 5s
2025-11-18 22:53:51 +01:00
volker.raschek 015f9aacee fix(netrc): typo maschine
Ansible Linter / ansible-lint (push) Successful in 21s
Lint Markdown files / markdown-lint (push) Successful in 4s
2025-11-16 11:41:52 +01:00
volker.raschek 22048124fd refac: use .yaml instead of .yml extension
Ansible Linter / ansible-lint (push) Successful in 24s
Lint Markdown files / markdown-lint (push) Successful in 4s
2025-10-25 12:38:00 +02:00
volker.raschek 042c91f42e chore(deps): update actions/checkout to v5.0.0
Lint Markdown files / markdown-lint (push) Successful in 4s
Ansible Linter / ansible-lint (push) Successful in 17s
2025-09-18 22:33:29 +02:00
volker.raschek 7a757ee119 fix(ansible-galaxy): adapt indentation
Ansible Linter / ansible-lint (push) Successful in 38s
Lint Markdown files / markdown-lint (push) Successful in 12s
2025-08-06 16:10:36 +02:00
volker.raschek c552f95227 fix(default): rename file
Lint Markdown files / markdown-lint (push) Successful in 10s
Ansible Linter / ansible-lint (push) Failing after 41s
2025-08-06 15:24:39 +02:00
volker.raschek e14ab7fc70 fix(ansible-galaxy): add namespace
Ansible Linter / ansible-lint (push) Has been cancelled
Lint Markdown files / markdown-lint (push) Has been cancelled
2025-08-06 15:22:46 +02:00
volker.raschek d6666179a4 fix(ansible-galaxy): adapt list of supported platforms
Lint Markdown files / markdown-lint (push) Successful in 13s
Ansible Linter / ansible-lint (push) Failing after 46s
2025-08-06 11:45:07 +02:00
volker.raschek 13ada129ae fix(ansible-galaxy): remove namespace
Lint Markdown files / markdown-lint (push) Successful in 9s
Ansible Linter / ansible-lint (push) Failing after 29s
2025-08-06 11:39:34 +02:00
volker.raschek ee87e4fe49 fix(linter): be compliant with ansible-linter
Lint Markdown files / markdown-lint (push) Successful in 10s
Ansible Linter / ansible-lint (push) Successful in 38s
2025-08-06 10:57:03 +02:00
volker.raschek af94964494 docs(README): remove dead link
Lint Markdown files / markdown-lint (push) Successful in 10s
Ansible Linter / ansible-lint (push) Successful in 31s
2025-07-12 23:27:07 +02:00
volker.raschek 7bced73bdf docs(README): add further details
Lint Markdown files / markdown-lint (push) Failing after 11s
Ansible Linter / ansible-lint (push) Successful in 48s
2025-07-12 23:22:31 +02:00
volker.raschek d6e4e18a86 fix(netrc): adapt netrc template
Ansible Linter / ansible-lint (push) Successful in 1m6s
Lint Markdown files / markdown-lint (push) Successful in 12s
2025-07-12 12:18:00 +02:00
volker.raschek 788a1f6610 fix(netrc): typo
Ansible Linter / ansible-lint (push) Successful in 37s
Lint Markdown files / markdown-lint (push) Successful in 10s
2025-06-27 20:48:04 +02:00
volker.raschek 9a3dbcfa86 feat(netrc): init
Ansible Linter / ansible-lint (push) Successful in 55s
Lint Markdown files / markdown-lint (push) Successful in 12s
2025-06-27 20:39:09 +02:00
volker.raschek 08e1625648 docs(README): remove drone badge
Ansible Linter / ansible-lint (push) Successful in 13s
Lint Markdown files / markdown-lint (push) Successful in 4s
2025-06-01 12:27:40 +02:00
volker.raschek 09131125ab chore(ci): use DavidAnson/markdownlint-cli2-action
Lint Markdown files / markdown-lint (push) Successful in 4s
Ansible Linter / ansible-lint (push) Successful in 1m25s
2025-05-13 21:20:13 +02:00
volker.raschek 718f652e42 fix(ci): remove requirements condition
Ansible Linter / ansible-lint (push) Successful in 54s
Lint Markdown files / markdown-lint (push) Successful in 9s
2025-04-14 22:50:38 +02:00
volker.raschek 2d902c049a fix(ci): remove workflow_dispatch
Ansible Linter / ansible-lint (push) Failing after 44s
Lint Markdown files / markdown-lint (push) Successful in 8s
2025-04-14 22:34:39 +02:00
volker.raschek 215922676d feat(act-runner): init
Ansible Linter / ansible-lint (push) Waiting to run
Lint Markdown files / markdown-lint (push) Waiting to run
2025-04-14 22:31:39 +02:00
volker.raschek f835afc2da chore(renovate): use configuration preset
continuous-integration/drone/push Build is passing
2025-04-01 22:20:18 +02:00
volker.raschek 4c467aaf88 fix: support ssh 'command' option
continuous-integration/drone/push Build was killed
2024-10-04 11:44:44 +02:00
volker.raschek 9bdd0cf8ff fix: adapt condition 2024-04-02 10:59:07 +02:00
volker.raschek 8a45755225 fix(meta): adapt role-name 2024-03-27 12:42:35 +01:00
volker.raschek d9bc119e63 fix: skip when shell_rc files is not defined
continuous-integration/drone/push Build is passing
2024-03-05 22:12:30 +01:00
volker.raschek 804eb02075 fix: suppoer relative and absolute paths
continuous-integration/drone/push Build is passing
2024-03-02 22:32:44 +01:00
volker.raschek 5907f1617a feat: support bashrc aliases, envs and functions
continuous-integration/drone/push Build is passing
2024-03-02 19:20:28 +01:00
volker.raschek 75fea198c2 fix: condition
continuous-integration/drone/push Build is passing
2024-02-03 16:46:23 +01:00
volker.raschek 0674c7d13e fix: copy only missing skel files
continuous-integration/drone/push Build is passing
2023-12-23 17:38:08 +01:00
volker.raschek df6d4d206e fix: add file permissions
continuous-integration/drone/push Build is passing
2023-12-23 16:34:01 +01:00
volker.raschek 1d1916ed29 fix: adjust btrfs subvol permissions
continuous-integration/drone/push Build is passing
2023-12-15 22:46:31 +01:00
volker.raschek 637f57f81f fix: create XDG base directories
continuous-integration/drone/push Build is passing
2023-12-01 21:52:56 +01:00
volker.raschek 409466869a fix: support btrfs subvolume for unix user's home dir
continuous-integration/drone/push Build is passing
2023-11-24 10:31:26 +01:00
volker.raschek e24515d232 feat: support btrfs subvolume for unix user's home dir
continuous-integration/drone/push Build is passing
2023-11-21 19:37:39 +01:00
volker.raschek 062a8cd23d fix: add whitespace
continuous-integration/drone/push Build is passing
2023-06-09 16:48:56 +02:00
volker.raschek 114ec2aed3 style(lint): quote mode
continuous-integration/drone/push Build is passing
2023-02-26 22:18:28 +01:00
volker.raschek 4fb52a7127 fix: lock unix users
continuous-integration/drone/push Build is passing
2023-02-15 21:41:11 +01:00
volker.raschek 0a03f2e74e fix: loack unix user correctly
continuous-integration/drone/push Build is passing
2023-02-15 14:30:20 +01:00
volker.raschek cc71ada59d fix: hide sensitive information in log output
continuous-integration/drone/push Build is passing
2023-02-15 14:25:49 +01:00
volker.raschek 9f36a69078 style(unix_users): name of tasks, use built in module
continuous-integration/drone/push Build was killed
2023-02-12 15:08:40 +01:00
volker.raschek 9f43029353 fix: add linter 2023-02-08 18:25:07 +01:00
volker.raschek e4e8ca9075 fix: ssh client config without =
continuous-integration/drone/push Build is passing
2022-07-20 16:09:04 +02:00
volker.raschek f32755f7d4 fix: extract public ssh key
continuous-integration/drone/push Build is passing
2022-07-20 15:52:27 +02:00
volker.raschek 26e57d7b6f fix: config ssh client config
continuous-integration/drone/push Build is passing
2022-07-19 17:54:56 +02:00
volker.raschek adf404bf25 fix: adapt condition
continuous-integration/drone/push Build is passing
2022-05-09 11:33:04 +02:00
volker.raschek 05c7df5693 fix: create users und groups only if defined
continuous-integration/drone/push Build is passing
2022-05-09 11:02:35 +02:00
volker.raschek e07bf4c459 fix: ansible galaxy id 2022-05-09 10:24:35 +02:00
volker.raschek 35890645a6 Initial Commit 2022-05-09 10:17:07 +02:00