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>
144 lines
5.9 KiB
YAML
144 lines
5.9 KiB
YAML
---
|
|
|
|
- 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"
|