`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>
`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>
`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>
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>
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>
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>
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>