setup: add freeipa-client module and FreeIPA group-based module automation

- Add freeipa-client module (sssd, cyrus-sasl-gssapi, freeipa-client AUR)
  with post-install enrollment hints; wired into tui-install.sh and
  install-modules.sh
- Add ansipa-install-modules.sh: reads IPA host groups named
  ansipa-module-<name>, applies matching module scripts via a yay wrapper
  that drops to ANSIPA_USER so AUR builds work from the root service
- Add ansipa-install-modules.service + .timer (boot + 30 min)
- Add deploy-ansipa-modules.yml Ansible playbook that deploys scripts,
  writes /etc/ansipa-modules.conf, and enables the timer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
The_miro 2026-05-18 11:40:51 +02:00
parent 9e708556d5
commit c51af40fce
7 changed files with 283 additions and 0 deletions

View File

@ -119,6 +119,7 @@ count_steps() {
[[ "$sel" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"freeipa-client"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$sel" == *"python"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"python"* ]] && TOTAL=$(( TOTAL + 1 ))
@ -185,6 +186,7 @@ SELECTED=$(dialog --backtitle "$BACKTITLE" \
"podman" "Podman rootless containers · buildah" off \ "podman" "Podman rootless containers · buildah" off \
"cockpit" "Cockpit web UI · machines · podman" off \ "cockpit" "Cockpit web UI · machines · podman" off \
"ssh-server" "SSH server openssh · key-auth · enabled" 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-server" "FreeIPA Server interactive server setup + client gen" off \
"freeipa-image" "FreeIPA Image OCI/LXC/Proxmox LXC builder + Keycloak" off \ "freeipa-image" "FreeIPA Image OCI/LXC/Proxmox LXC builder + Keycloak" off \
"python" "Python tools pyright · pipx · pynvim" off \ "python" "Python tools pyright · pipx · pynvim" off \
@ -230,6 +232,7 @@ SUMMARY=""
[[ "$SELECTED" == *"podman"* ]] && SUMMARY+=" ✦ Podman\n" [[ "$SELECTED" == *"podman"* ]] && SUMMARY+=" ✦ Podman\n"
[[ "$SELECTED" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit\n" [[ "$SELECTED" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit\n"
[[ "$SELECTED" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server\n" [[ "$SELECTED" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server\n"
[[ "$SELECTED" == *"freeipa-client"* ]] && SUMMARY+=" ✦ FreeIPA Client\n"
[[ "$SELECTED" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n" [[ "$SELECTED" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n"
[[ "$SELECTED" == *"freeipa-image"* ]] && SUMMARY+=" ✦ FreeIPA Image Builder\n" [[ "$SELECTED" == *"freeipa-image"* ]] && SUMMARY+=" ✦ FreeIPA Image Builder\n"
[[ "$SELECTED" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n" [[ "$SELECTED" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n"
@ -278,6 +281,7 @@ count_steps "$SELECTED"
[[ "$SELECTED" == *"podman"* ]] && run_module "Podman" "$APPS/podman.sh" [[ "$SELECTED" == *"podman"* ]] && run_module "Podman" "$APPS/podman.sh"
[[ "$SELECTED" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh" [[ "$SELECTED" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh"
[[ "$SELECTED" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.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-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh"
[[ "$SELECTED" == *"freeipa-image"* ]] && run_module "FreeIPA Image" "$APPS/freeipa-image-builder.sh" [[ "$SELECTED" == *"freeipa-image"* ]] && run_module "FreeIPA Image" "$APPS/freeipa-image-builder.sh"
[[ "$SELECTED" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh" [[ "$SELECTED" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh"

View File

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

View File

@ -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-<name> 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/<name>.sh is executed (once,
# stamped in /var/lib/ansipa-modules/).
#
# Configuration: /etc/ansipa-modules.conf
# ANSIPA_USER=<username> 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" <<EOF
#!/bin/bash
exec sudo -u "$ANSIPA_USER" "$YAY_BIN" "\$@"
EOF
chmod +x "$WRAP_DIR/yay"
fi
# ── Discover which ansipa-module-* host groups this host belongs to ───────────
HOST_FQDN=$(hostname -f 2>/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

View File

@ -0,0 +1,9 @@
[Unit]
Description=Periodic FreeIPA module sync
[Timer]
OnBootSec=3min
OnUnitActiveSec=30min
[Install]
WantedBy=timers.target

View File

@ -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-<name> (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

View File

@ -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=<domain> --server=<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"

View File

@ -129,6 +129,7 @@ count_steps() {
[[ "$a" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"freeipa-client"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"freeipa-image"* ]] && TOTAL=$(( TOTAL + 1 ))
[[ "$a" == *"python"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"python"* ]] && TOTAL=$(( TOTAL + 1 ))
@ -220,6 +221,7 @@ SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \
"podman" "Podman rootless containers · buildah" off \ "podman" "Podman rootless containers · buildah" off \
"cockpit" "Cockpit web UI · machines · podman" off \ "cockpit" "Cockpit web UI · machines · podman" off \
"ssh-server" "SSH server openssh · key-auth · enabled" 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-server" "FreeIPA Server interactive server setup + client gen" off \
"freeipa-image" "FreeIPA Image OCI/LXC/Proxmox LXC builder + Keycloak" off \ "freeipa-image" "FreeIPA Image OCI/LXC/Proxmox LXC builder + Keycloak" off \
"python" "Python tools pyright · pipx · pynvim" 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" == *"podman"* ]] && SUMMARY+=" ✦ Podman (rootless) + Buildah\n"
[[ "$SELECTED_APPS" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit web UI\n" [[ "$SELECTED_APPS" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit web UI\n"
[[ "$SELECTED_APPS" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server (openssh, key auth)\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-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n"
[[ "$SELECTED_APPS" == *"freeipa-image"* ]] && SUMMARY+=" ✦ FreeIPA Image Builder\n" [[ "$SELECTED_APPS" == *"freeipa-image"* ]] && SUMMARY+=" ✦ FreeIPA Image Builder\n"
[[ "$SELECTED_APPS" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n" [[ "$SELECTED_APPS" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n"
@ -335,6 +338,7 @@ fi
[[ "$SELECTED_APPS" == *"podman"* ]] && run_module "Podman" "$APPS/podman.sh" [[ "$SELECTED_APPS" == *"podman"* ]] && run_module "Podman" "$APPS/podman.sh"
[[ "$SELECTED_APPS" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh" [[ "$SELECTED_APPS" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh"
[[ "$SELECTED_APPS" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.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-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh"
[[ "$SELECTED_APPS" == *"freeipa-image"* ]] && run_module "FreeIPA Image" "$APPS/freeipa-image-builder.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" [[ "$SELECTED_APPS" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh"