From 6f346b88128fe137c4b454707a0360da3adf95af Mon Sep 17 00:00:00 2001 From: Samuel James Date: Thu, 3 Sep 2026 12:06:30 +0000 Subject: [PATCH] life-church: add lc-hosts inventory + lc-precheck playbook (facts + read-only pre-checks) --- life-church/playbooks/lc-precheck.yml | 144 ++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 life-church/playbooks/lc-precheck.yml diff --git a/life-church/playbooks/lc-precheck.yml b/life-church/playbooks/lc-precheck.yml new file mode 100644 index 0000000..68797cc --- /dev/null +++ b/life-church/playbooks/lc-precheck.yml @@ -0,0 +1,144 @@ +--- +# ============================================================================ +# life-church/playbooks/lc-precheck.yml +# Pre-checks + fact gathering for Life Church desktops (thinkcenter, tc1). +# READ-ONLY: gathers facts and asserts baseline conditions. Changes nothing. +# +# become (sudo) password is fetched from Infisical /lc-hosts at runtime on +# localhost, then applied per-host via ansible_become_pass. Never hardcoded. +# ============================================================================ + +# --------------------------------------------------------------------------- +# Play 1 (localhost): resolve Infisical creds + read per-host sudo passwords +# --------------------------------------------------------------------------- +- name: "Life Church pre-check — resolve become passwords from Infisical" + hosts: localhost + gather_facts: false + tasks: + - name: "[resolve] Infisical creds from env (Semaphore environment)" + ansible.builtin.set_fact: + infisical_client_id: "{{ lookup('env','INFISICAL_CLIENT_ID') }}" + infisical_client_secret: "{{ lookup('env','INFISICAL_CLIENT_SECRET') }}" + infisical_project_id: "{{ lookup('env','INFISICAL_PROJECT_ID') }}" + infisical_url: "{{ lookup('env','INFISICAL_API_URL') | default('http://192.168.122.41:8080', true) }}" + infisical_env_slug: "{{ lookup('env','INFISICAL_ENV_SLUG') | default('prod', true) }}" + + - name: "[resolve] Assert Infisical creds present" + ansible.builtin.assert: + that: + - infisical_client_id | length > 0 + - infisical_client_secret | length > 0 + - infisical_project_id | length > 0 + fail_msg: "Infisical creds missing from environment — check Semaphore environment 'infisical-dev'." + + - name: "[infisical] Log in" + infisical.vault.login: + url: "{{ infisical_url }}" + auth_method: universal_auth + universal_auth_client_id: "{{ infisical_client_id }}" + universal_auth_client_secret: "{{ infisical_client_secret }}" + register: infisical_login + no_log: true + + - name: "[infisical] Read /lc-hosts secrets" + infisical.vault.read_secrets: + login_data: "{{ infisical_login.login_data }}" + project_id: "{{ infisical_project_id }}" + env_slug: "{{ infisical_env_slug }}" + path: "/lc-hosts" + as_dict: true + register: lc_secrets + no_log: true + + - name: "[set] Stash become passwords keyed by host (localhost hostvars)" + ansible.builtin.set_fact: + become_pass_map: + thinkcenter: "{{ lc_secrets.secrets['THINKCENTER_SUDO_PASS'] }}" + tc1: "{{ lc_secrets.secrets['TC1_SUDO_PASS'] }}" + no_log: true + +# --------------------------------------------------------------------------- +# Play 2 (the desktops): connectivity, facts, and read-only pre-checks +# --------------------------------------------------------------------------- +- name: "Life Church pre-check — connectivity + facts + baseline checks" + hosts: lifechurch_desktops + gather_facts: true + become: true + vars: + # pull this host's sudo pass from the map computed on localhost in Play 1 + ansible_become_pass: "{{ hostvars['localhost'].become_pass_map[inventory_hostname] }}" + tasks: + - name: "[net] Host reachable (SSH connection established)" + ansible.builtin.ping: + + - name: "[sudo] Privilege escalation works" + ansible.builtin.command: id -u + register: id_check + changed_when: false + + - name: "[sudo] Confirm running as root after become" + ansible.builtin.assert: + that: id_check.stdout == "0" + fail_msg: "become did not escalate to root on {{ inventory_hostname }}" + success_msg: "sudo OK on {{ inventory_hostname }}" + + - name: "[disk] Root filesystem usage" + ansible.builtin.set_fact: + root_mount: "{{ ansible_mounts | selectattr('mount','equalto','/') | first }}" + + - name: "[disk] Assert root filesystem has >10% free" + ansible.builtin.assert: + that: (root_mount.size_available / root_mount.size_total) > 0.10 + fail_msg: "LOW DISK on {{ inventory_hostname }}: only {{ (100 * root_mount.size_available / root_mount.size_total) | round(1) }}% free" + success_msg: "Disk OK: {{ (100 * root_mount.size_available / root_mount.size_total) | round(1) }}% free" + + - name: "[mem] Assert at least 200MB RAM free" + ansible.builtin.assert: + that: ansible_memfree_mb > 200 + fail_msg: "LOW MEMORY on {{ inventory_hostname }}: {{ ansible_memfree_mb }}MB free" + success_msg: "Memory OK: {{ ansible_memfree_mb }}MB free of {{ ansible_memtotal_mb }}MB" + + - name: "[time] Check NTP/clock sync status" + ansible.builtin.command: timedatectl show -p NTPSynchronized --value + register: ntp_sync + changed_when: false + failed_when: false + + - name: "[pkg] Count pending apt security updates" + ansible.builtin.shell: >- + set -o pipefail; + apt-get -s -o Debug::NoLocking=true upgrade 2>/dev/null | grep -c '^Inst' || true + args: + executable: /bin/bash + register: pending_updates + changed_when: false + failed_when: false + + - name: "[reboot] Reboot-required flag present?" + ansible.builtin.stat: + path: /var/run/reboot-required + register: reboot_required + + - name: "[report] Host summary" + ansible.builtin.debug: + msg: + - "===== {{ inventory_hostname }} ({{ ansible_host }}) =====" + - "OS : {{ ansible_distribution }} {{ ansible_distribution_version }} | kernel {{ ansible_kernel }}" + - "CPU / RAM : {{ ansible_processor_vcpus }} vcpus | {{ ansible_memtotal_mb }}MB total, {{ ansible_memfree_mb }}MB free" + - "Root disk : {{ (root_mount.size_total / 1073741824) | round(1) }}GB total, {{ (root_mount.size_available / 1073741824) | round(1) }}GB free ({{ (100 * root_mount.size_available / root_mount.size_total) | round(1) }}% free)" + - "Uptime : {{ (ansible_uptime_seconds / 3600) | round(1) }} hours" + - "Clock synced : {{ 'yes' if ntp_sync.stdout == 'yes' else 'NO — check time sync' }}" + - "Pending pkgs : {{ pending_updates.stdout | trim }} upgradable" + - "Reboot needed : {{ 'YES' if reboot_required.stat.exists else 'no' }}" + - "Default IPv4 : {{ ansible_default_ipv4.address | default('n/a') }} ({{ ansible_default_ipv4.interface | default('n/a') }})" + +# --------------------------------------------------------------------------- +# Play 3 (localhost): overall result +# --------------------------------------------------------------------------- +- name: "Life Church pre-check — result" + hosts: localhost + gather_facts: false + tasks: + - name: "[result] ALL PRE-CHECKS PASSED" + ansible.builtin.debug: + msg: "Pre-checks passed for all Life Church desktops — hosts reachable, sudo OK, resources healthy."