diff --git a/setup/install-modules.sh b/setup/install-modules.sh index 53f2461..e8e3b48 100755 --- a/setup/install-modules.sh +++ b/setup/install-modules.sh @@ -119,6 +119,7 @@ count_steps() { [[ "$sel" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$sel" == *"freeipa-client"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"python"* ]] && TOTAL=$(( TOTAL + 1 )) @@ -185,6 +186,7 @@ SELECTED=$(dialog --backtitle "$BACKTITLE" \ "podman" "Podman rootless containers · buildah" off \ "cockpit" "Cockpit web UI · machines · podman" off \ "ssh-server" "SSH server openssh · key-auth · enabled" off \ + "freeipa-client" "FreeIPA Client sssd + ipa-client-install + enrollment" off \ "freeipa-server" "FreeIPA Server interactive server setup + client gen" off \ "freeipa-image" "FreeIPA Image OCI/LXC/Proxmox LXC builder + Keycloak" off \ "python" "Python tools pyright · pipx · pynvim" off \ @@ -230,6 +232,7 @@ SUMMARY="" [[ "$SELECTED" == *"podman"* ]] && SUMMARY+=" ✦ Podman\n" [[ "$SELECTED" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit\n" [[ "$SELECTED" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server\n" +[[ "$SELECTED" == *"freeipa-client"* ]] && SUMMARY+=" ✦ FreeIPA Client\n" [[ "$SELECTED" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n" [[ "$SELECTED" == *"freeipa-image"* ]] && SUMMARY+=" ✦ FreeIPA Image Builder\n" [[ "$SELECTED" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n" @@ -278,6 +281,7 @@ count_steps "$SELECTED" [[ "$SELECTED" == *"podman"* ]] && run_module "Podman" "$APPS/podman.sh" [[ "$SELECTED" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh" [[ "$SELECTED" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.sh" +[[ "$SELECTED" == *"freeipa-client"* ]] && run_module "FreeIPA Client" "$APPS/freeipa-client.sh" [[ "$SELECTED" == *"freeipa-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh" [[ "$SELECTED" == *"freeipa-image"* ]] && run_module "FreeIPA Image" "$APPS/freeipa-image-builder.sh" [[ "$SELECTED" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh" diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.service b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.service new file mode 100644 index 0000000..75ee154 --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.service @@ -0,0 +1,10 @@ +[Unit] +Description=Apply setup modules based on FreeIPA ansipa-module-* host groups +After=network-online.target sssd.service +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/ansipa-install-modules.sh +StandardOutput=journal +StandardError=journal diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.sh new file mode 100755 index 0000000..47753ba --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# ansipa-install-modules.sh — apply setup modules to this host based on +# FreeIPA host group membership. +# +# Host groups follow the naming convention: +# ansipa-module- e.g. ansipa-module-docker, ansipa-module-ollama +# +# When this host is a member of such a group, the corresponding module +# script in /usr/local/lib/ansipa-modules/.sh is executed (once, +# stamped in /var/lib/ansipa-modules/). +# +# Configuration: /etc/ansipa-modules.conf +# ANSIPA_USER= non-root user for AUR helper (yay) +# MODULES_DIR=/usr/local/lib/ansipa-modules +# STATE_DIR=/var/lib/ansipa-modules + +set -euo pipefail + +CONFIG=/etc/ansipa-modules.conf +[[ -f "$CONFIG" ]] && source "$CONFIG" + +ANSIPA_USER="${ANSIPA_USER:-}" +MODULES_DIR="${MODULES_DIR:-/usr/local/lib/ansipa-modules}" +STATE_DIR="${STATE_DIR:-/var/lib/ansipa-modules}" +PREFIX="ansipa-module-" +LOG_TAG="ansipa-modules" + +log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; } +warn() { echo "[$LOG_TAG][WARN] $*" >&2; logger -t "$LOG_TAG" "WARN: $*" 2>/dev/null || true; } + +# ── Resolve ANSIPA_USER ─────────────────────────────────────────────────────── +if [[ -z "$ANSIPA_USER" ]]; then + # Use the first non-root, non-system user with a login shell + ANSIPA_USER=$(awk -F: '($3>=1000 && $7!~/nologin|false/) {print $1; exit}' /etc/passwd) +fi +if [[ -z "$ANSIPA_USER" ]]; then + warn "Cannot determine ANSIPA_USER. Set it in $CONFIG." + exit 1 +fi + +log "Running as root, AUR helper delegated to user: $ANSIPA_USER" +mkdir -p "$STATE_DIR" + +# ── Create a yay wrapper so module scripts can call 'yay' as non-root ──────── +YAY_BIN=$(command -v yay 2>/dev/null || true) +WRAP_DIR=$(mktemp -d /tmp/ansipa-wrap.XXXXXX) +trap 'rm -rf "$WRAP_DIR"' EXIT + +if [[ -n "$YAY_BIN" ]]; then + cat > "$WRAP_DIR/yay" </dev/null || hostname) + +if ! command -v ipa &>/dev/null; then + warn "ipa command not found — host not enrolled in FreeIPA. Exiting." + exit 0 +fi + +# kinit with host keytab so IPA commands work from the service context +kinit -k "host/$HOST_FQDN" &>/dev/null || true + +RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \ + | grep -i "Member of host-groups:" | sed 's/.*: //' || true) + +if [[ -z "$RAW_GROUPS" ]]; then + log "Host '$HOST_FQDN' is not a member of any host groups — nothing to do." + exit 0 +fi + +# Parse comma-separated list, keep only ansipa-module-* entries +WANTED_MODULES=() +while IFS=',' read -ra GRP_ARRAY; do + for g in "${GRP_ARRAY[@]}"; do + g="${g// /}" # strip spaces + if [[ "$g" == ${PREFIX}* ]]; then + WANTED_MODULES+=("${g#$PREFIX}") + fi + done +done <<< "$RAW_GROUPS" + +if [[ ${#WANTED_MODULES[@]} -eq 0 ]]; then + log "No ansipa-module-* host groups found for '$HOST_FQDN'." + exit 0 +fi + +log "Modules requested for this host: ${WANTED_MODULES[*]}" + +# ── Apply each module ───────────────────────────────────────────────────────── +for MODULE in "${WANTED_MODULES[@]}"; do + STAMP="$STATE_DIR/${MODULE}.done" + SCRIPT="$MODULES_DIR/${MODULE}.sh" + + if [[ -f "$STAMP" ]]; then + log "Module '$MODULE' already applied (stamp: $STAMP) — skipping." + continue + fi + + if [[ ! -f "$SCRIPT" ]]; then + warn "Module script not found: $SCRIPT — skipping '$MODULE'." + continue + fi + + log "Applying module: $MODULE" + if env PATH="$WRAP_DIR:$PATH" bash "$SCRIPT" >>"$STATE_DIR/${MODULE}.log" 2>&1; then + touch "$STAMP" + log "Module '$MODULE' applied successfully." + else + warn "Module '$MODULE' failed — see $STATE_DIR/${MODULE}.log" + fi +done diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.timer b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.timer new file mode 100644 index 0000000..78686b4 --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Periodic FreeIPA module sync + +[Timer] +OnBootSec=3min +OnUnitActiveSec=30min + +[Install] +WantedBy=timers.target diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-modules.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-modules.yml new file mode 100644 index 0000000..aef0fe5 --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-modules.yml @@ -0,0 +1,101 @@ +--- +# deploy-ansipa-modules.yml — deploy the module auto-installer to enrolled hosts. +# +# Prerequisites on target hosts: +# - FreeIPA client enrolled (sssd running, ipa command available) +# - A non-root user with yay access (set ANSIPA_USER in /etc/ansipa-modules.conf) +# +# Usage: +# ansible-playbook -i inventory deploy-ansipa-modules.yml +# ansible-playbook -i inventory deploy-ansipa-modules.yml -e ansipa_user=amir +# +# FreeIPA host group convention: +# Create host groups named ansipa-module- (e.g. ansipa-module-docker) +# and add hosts to them. The timer will apply the matching module automatically. + +- name: Deploy FreeIPA module auto-installer + hosts: all + become: yes + + vars: + ansipa_user: "{{ lookup('env', 'ANSIPA_USER') | default('', true) }}" + modules_dir: /usr/local/lib/ansipa-modules + state_dir: /var/lib/ansipa-modules + + tasks: + + - name: Create module directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ modules_dir }}" + - "{{ state_dir }}" + + - name: Write /etc/ansipa-modules.conf + copy: + dest: /etc/ansipa-modules.conf + mode: '0644' + content: | + # ansipa-modules configuration + # ANSIPA_USER: non-root user used to run the AUR helper (yay). + # Leave blank to auto-detect the first non-system user. + ANSIPA_USER={{ ansipa_user }} + MODULES_DIR={{ modules_dir }} + STATE_DIR={{ state_dir }} + when: ansipa_user != "" + + - name: Deploy main module installer script + copy: + src: ansipa-install-modules.sh + dest: /usr/local/bin/ansipa-install-modules.sh + mode: '0755' + + - name: Deploy module scripts + copy: + src: "{{ item }}" + dest: "{{ modules_dir }}/{{ item | basename }}" + mode: '0755' + with_fileglob: + - "../optional-Modules/apps/*.sh" + + - name: Install systemd service + copy: + dest: /etc/systemd/system/ansipa-install-modules.service + mode: '0644' + content: | + [Unit] + Description=Apply setup modules based on FreeIPA ansipa-module-* host groups + After=network-online.target sssd.service + Wants=network-online.target + + [Service] + Type=oneshot + ExecStart=/usr/local/bin/ansipa-install-modules.sh + StandardOutput=journal + StandardError=journal + + - name: Install systemd timer + copy: + dest: /etc/systemd/system/ansipa-install-modules.timer + mode: '0644' + content: | + [Unit] + Description=Periodic FreeIPA module sync + + [Timer] + OnBootSec=3min + OnUnitActiveSec=30min + + [Install] + WantedBy=timers.target + + - name: Reload systemd + command: systemctl daemon-reload + + - name: Enable and start module timer + systemd: + name: ansipa-install-modules.timer + enabled: yes + state: started diff --git a/setup/modules/optional-Modules/apps/freeipa-client.sh b/setup/modules/optional-Modules/apps/freeipa-client.sh new file mode 100755 index 0000000..555819c --- /dev/null +++ b/setup/modules/optional-Modules/apps/freeipa-client.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -euo pipefail + +# FreeIPA client — installs client packages and optionally enrolls this host. +# Packages: sssd + cyrus-sasl-gssapi from pacman; freeipa-client (AUR) for +# ipa-client-install, ipa-getkeytab, etc. + +PACMAN_PKGS=(sssd cyrus-sasl-gssapi openldap krb5 oddjob) +AUR_PKGS=(freeipa-client) + +echo "[+] Installing FreeIPA client packages..." +pacman -S --noconfirm --needed "${PACMAN_PKGS[@]}" + +if command -v yay &>/dev/null; then + echo "[+] Installing freeipa-client (AUR)..." + yay -S --noconfirm --needed "${AUR_PKGS[@]}" +else + echo "[!] yay not found — skipping AUR packages (freeipa-client)." + echo " Install yay, then run: yay -S --needed freeipa-client" +fi + +# Enable sssd (without starting — host is not enrolled yet) +systemctl enable sssd.service 2>/dev/null || true + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLIENT_ENROLL="$SCRIPT_DIR/../../FreeipaAnsible/freeipa-client.sh" + +echo "" +echo "[✓] FreeIPA client packages installed." +echo "" +echo " To enroll this host, run one of:" +echo " ipa-client-install --domain= --server= --principal=admin" +if [[ -f "$CLIENT_ENROLL" ]]; then + echo " $CLIENT_ENROLL --interactive" + echo " $CLIENT_ENROLL --answerfile /path/to/answerfile.json" +fi +echo "" +echo " After enrollment, enable auto-home-dir creation:" +echo " authselect select sssd with-mkhomedir --force" diff --git a/setup/tui-install.sh b/setup/tui-install.sh index 69dc5be..7f578e4 100755 --- a/setup/tui-install.sh +++ b/setup/tui-install.sh @@ -129,6 +129,7 @@ count_steps() { [[ "$a" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"freeipa-client"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"python"* ]] && TOTAL=$(( TOTAL + 1 )) @@ -220,6 +221,7 @@ SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \ "podman" "Podman rootless containers · buildah" off \ "cockpit" "Cockpit web UI · machines · podman" off \ "ssh-server" "SSH server openssh · key-auth · enabled" off \ + "freeipa-client" "FreeIPA Client sssd + ipa-client-install + enrollment" off \ "freeipa-server" "FreeIPA Server interactive server setup + client gen" off \ "freeipa-image" "FreeIPA Image OCI/LXC/Proxmox LXC builder + Keycloak" off \ "python" "Python tools pyright · pipx · pynvim" off \ @@ -272,6 +274,7 @@ if [[ -n "$SELECTED_APPS" ]]; then [[ "$SELECTED_APPS" == *"podman"* ]] && SUMMARY+=" ✦ Podman (rootless) + Buildah\n" [[ "$SELECTED_APPS" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit web UI\n" [[ "$SELECTED_APPS" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server (openssh, key auth)\n" + [[ "$SELECTED_APPS" == *"freeipa-client"* ]] && SUMMARY+=" ✦ FreeIPA Client\n" [[ "$SELECTED_APPS" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n" [[ "$SELECTED_APPS" == *"freeipa-image"* ]] && SUMMARY+=" ✦ FreeIPA Image Builder\n" [[ "$SELECTED_APPS" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n" @@ -335,6 +338,7 @@ fi [[ "$SELECTED_APPS" == *"podman"* ]] && run_module "Podman" "$APPS/podman.sh" [[ "$SELECTED_APPS" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh" [[ "$SELECTED_APPS" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.sh" +[[ "$SELECTED_APPS" == *"freeipa-client"* ]] && run_module "FreeIPA Client" "$APPS/freeipa-client.sh" [[ "$SELECTED_APPS" == *"freeipa-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh" [[ "$SELECTED_APPS" == *"freeipa-image"* ]] && run_module "FreeIPA Image" "$APPS/freeipa-image-builder.sh" [[ "$SELECTED_APPS" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh"