Dotfiles/setup/modules/optional-Modules/apps/freeipa-image-builder.sh

382 lines
15 KiB
Bash
Executable File

#!/bin/bash
# freeipa-image-builder.sh — build a FreeIPA server image for multiple targets
#
# Targets (TUI chooser):
# docker Build Docker/Podman image and optionally push to a registry
# lxc Export rootfs as a Proxmox/LXC .tar.zst CT template
# proxmox-vm Customize a Rocky/Fedora cloud image → QCOW2 (requires virt-customize)
# oci-archive Export OCI tarball via skopeo (for air-gapped import)
#
# Keycloak:
# Optionally generates a docker-compose.yml that includes the full
# FreeIPA + Keycloak + PostgreSQL constellation and a pre-configured
# keycloak-configure.sh for post-start LDAP federation setup.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMAGE_SRC="$SCRIPT_DIR/../../FreeipaAnsible/image"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; NC='\033[0m'
log() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
info() { echo -e "${CYAN}[i]${NC} $*"; }
section() { echo -e "\n${BLUE}━━━ $* ━━━${NC}"; }
ask() { printf "${MAGENTA}[?]${NC} %s " "$*"; }
[[ $EUID -eq 0 ]] && { error "Run as your normal user (not root)."; exit 1; }
# ─── Detect container engine ──────────────────────────────────────────────────
if command -v podman &>/dev/null; then
ENGINE="podman"
elif command -v docker &>/dev/null; then
ENGINE="docker"
else
error "Neither podman nor docker found. Install one first."
exit 1
fi
info "Container engine: $ENGINE"
# ─── dialog theme ─────────────────────────────────────────────────────────────
TMP_D="$(mktemp -d)"; trap 'rm -rf "$TMP_D"' EXIT
BACKTITLE="FreeIPA Image Builder"
export DIALOGRC="$TMP_D/dialogrc"
cat > "$DIALOGRC" <<'EOF'
use_shadow = ON
use_colors = ON
screen_color = (BLACK,BLACK,ON)
title_color = (MAGENTA,BLACK,ON)
border_color = (MAGENTA,BLACK,ON)
button_active_color = (BLACK,MAGENTA,ON)
button_inactive_color = (WHITE,BLACK,OFF)
menubox_color = (WHITE,BLACK,OFF)
menubox_border_color = (MAGENTA,BLACK,ON)
item_color = (WHITE,BLACK,OFF)
item_selected_color = (BLACK,MAGENTA,ON)
tag_color = (CYAN,BLACK,ON)
tag_selected_color = (BLACK,CYAN,ON)
check_color = (WHITE,BLACK,OFF)
check_selected_color = (BLACK,MAGENTA,ON)
uarrow_color = (MAGENTA,BLACK,ON)
darrow_color = (MAGENTA,BLACK,ON)
EOF
command -v dialog &>/dev/null || { sudo pacman -S --noconfirm dialog; }
# ─── Target chooser ───────────────────────────────────────────────────────────
TARGET=$(dialog --backtitle "$BACKTITLE" \
--title " Select Target Format " \
--menu "Choose the output format:" 16 70 4 \
"docker" "Docker / Podman image (local + optional registry push)" \
"lxc" "LXC / Proxmox CT template (.tar.zst rootfs archive)" \
"proxmox-vm" "Proxmox VM (cloud-init QCOW2, requires virt-customize)" \
"oci-archive" "OCI archive (skopeo tarball for air-gapped import)" \
3>&1 1>&2 2>&3) || { clear; echo "Aborted."; exit 0; }
# ─── Keycloak? ───────────────────────────────────────────────────────────────
WITH_KEYCLOAK=false
dialog --backtitle "$BACKTITLE" \
--title " Keycloak Integration " \
--yesno "\nInclude Keycloak in the output?\n\n\
Adds PostgreSQL + Keycloak to the compose stack and generates\n\
keycloak-configure.sh for LDAP federation post-start setup." 11 64 \
&& WITH_KEYCLOAK=true || true
# ─── FreeIPA config ───────────────────────────────────────────────────────────
section "FreeIPA configuration"
info "These values are embedded in the .env template and compose file."
info "Override them at runtime via environment variables."
echo
ask "IPA Domain (e.g. corp.example.com):"; read -r IPA_DOMAIN
[[ -z "$IPA_DOMAIN" ]] && { error "IPA_DOMAIN is required."; exit 1; }
GUESSED_HOSTNAME="ipa.$IPA_DOMAIN"
ask "Server FQDN [$GUESSED_HOSTNAME]:"; read -r I; IPA_HOSTNAME="${I:-$GUESSED_HOSTNAME}"
GUESSED_REALM="${IPA_DOMAIN^^}"
ask "Kerberos Realm [$GUESSED_REALM]:"; read -r I; IPA_REALM="${I:-$GUESSED_REALM}"
ask "Setup integrated DNS? [y/N]:"; read -r I
IPA_SETUP_DNS=false; [[ "${I,,}" == "y"* ]] && IPA_SETUP_DNS=true
IMAGE_TAG="freeipa-server:${IPA_DOMAIN//./-}"
ask "Image tag [$IMAGE_TAG]:"; read -r I; IMAGE_TAG="${I:-$IMAGE_TAG}"
# Output directory
DEFAULT_OUT="$HOME/freeipa-image-output"
ask "Output directory [$DEFAULT_OUT]:"; read -r I; OUTPUT_DIR="${I:-$DEFAULT_OUT}"
# Keycloak config (if selected)
KC_HOSTNAME="keycloak.$IPA_DOMAIN"
KC_REALM="${IPA_DOMAIN%%.*}"
if [[ "$WITH_KEYCLOAK" == true ]]; then
echo
ask "Keycloak hostname [$KC_HOSTNAME]:"; read -r I; KC_HOSTNAME="${I:-$KC_HOSTNAME}"
ask "Keycloak realm name [$KC_REALM]:"; read -r I; KC_REALM="${I:-$KC_REALM}"
fi
# Proxmox VM cloud image (only if proxmox-vm selected)
CLOUD_IMAGE_URL=""
if [[ "$TARGET" == "proxmox-vm" ]]; then
echo
ROCKY_URL="https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2"
ask "Cloud image URL [$ROCKY_URL]:"; read -r I; CLOUD_IMAGE_URL="${I:-$ROCKY_URL}"
fi
# ─── Confirm ──────────────────────────────────────────────────────────────────
echo
info "──────────────────────────────────────"
printf " Target: %s\n" "$TARGET"
printf " Domain: %s\n" "$IPA_DOMAIN"
printf " FQDN: %s\n" "$IPA_HOSTNAME"
printf " Realm: %s\n" "$IPA_REALM"
printf " Image tag: %s\n" "$IMAGE_TAG"
printf " Keycloak: %s\n" "$WITH_KEYCLOAK"
printf " Output: %s\n" "$OUTPUT_DIR"
info "──────────────────────────────────────"
echo
ask "Proceed? [y/N]:"; read -r CONFIRM
[[ "${CONFIRM,,}" != "y"* ]] && { echo "Aborted."; exit 0; }
mkdir -p "$OUTPUT_DIR"
# ─── Step 1: Always build the container image ─────────────────────────────────
section "Building container image ($ENGINE)"
$ENGINE build \
--tag "$IMAGE_TAG" \
--label "ipa.domain=$IPA_DOMAIN" \
--label "ipa.realm=$IPA_REALM" \
"$IMAGE_SRC"
log "Image built: $IMAGE_TAG"
# ─── Step 2: Target-specific export ──────────────────────────────────────────
case "$TARGET" in
docker)
section "Docker/Podman — local image ready"
log "Image '$IMAGE_TAG' is in the local $ENGINE store."
ask "Push to a registry? [y/N]:"; read -r I
if [[ "${I,,}" == "y"* ]]; then
ask "Registry image name (e.g. registry.example.com/freeipa-server:latest):"; read -r REG_TAG
if [[ -n "$REG_TAG" ]]; then
$ENGINE tag "$IMAGE_TAG" "$REG_TAG"
$ENGINE push "$REG_TAG"
log "Pushed: $REG_TAG"
fi
fi
;;
lxc)
section "LXC / Proxmox CT template"
LXC_ARCHIVE="$OUTPUT_DIR/freeipa-server-lxc.tar.zst"
info "Creating temporary container to export rootfs..."
TMP_CTR="freeipa-lxc-export-$$"
$ENGINE create --name "$TMP_CTR" "$IMAGE_TAG" /bin/true
info "Exporting rootfs (this may take a minute)..."
if command -v zstd &>/dev/null; then
$ENGINE export "$TMP_CTR" | zstd -T0 -o "$LXC_ARCHIVE"
else
warn "zstd not found — falling back to gzip (.tar.gz)"
LXC_ARCHIVE="${LXC_ARCHIVE%.tar.zst}.tar.gz"
$ENGINE export "$TMP_CTR" | gzip -9 > "$LXC_ARCHIVE"
fi
$ENGINE rm "$TMP_CTR" &>/dev/null
log "LXC template: $LXC_ARCHIVE"
# Proxmox import instructions
cat > "$OUTPUT_DIR/lxc-import-instructions.txt" <<LXCTXT
Proxmox LXC import instructions
================================
Upload template to Proxmox:
scp $LXC_ARCHIVE root@proxmox:/var/lib/vz/template/cache/
Create container (via Proxmox web UI or CLI):
pct create <VMID> local:vztmpl/$(basename "$LXC_ARCHIVE") \\
--hostname $IPA_HOSTNAME \\
--memory 2048 --cores 2 \\
--net0 name=eth0,bridge=vmbr0,ip=dhcp \\
--ostype fedora --unprivileged 0
NOTE: FreeIPA requires a privileged LXC container (unprivileged=0).
Add to /etc/pve/lxc/<VMID>.conf:
lxc.apparmor.profile: unconfined
lxc.cap.drop:
Set FreeIPA env vars before first start:
pct exec <VMID> -- bash -c 'cat >> /etc/environment <<EOF
IPA_DOMAIN=$IPA_DOMAIN
IPA_REALM=$IPA_REALM
IPA_ADMIN_PASSWORD=YourAdminPassword
IPA_DM_PASSWORD=YourDMPassword
EOF'
Start and watch first-boot:
pct start <VMID>
pct exec <VMID> -- journalctl -f -u ipa-first-boot
LXCTXT
log "Instructions: $OUTPUT_DIR/lxc-import-instructions.txt"
;;
proxmox-vm)
section "Proxmox VM — cloud-init QCOW2"
if ! command -v virt-customize &>/dev/null; then
error "virt-customize not found. Install libguestfs-tools:"
error " sudo pacman -S libguestfs # Arch"
error " sudo dnf install libguestfs-tools # Fedora/RHEL"
exit 1
fi
if ! command -v qemu-img &>/dev/null; then
error "qemu-img not found. Install qemu-img / qemu-tools."
exit 1
fi
CLOUD_IMG="$TMP_D/cloud-base.qcow2"
QCOW2_OUT="$OUTPUT_DIR/freeipa-server.qcow2"
info "Downloading cloud image..."
curl -L --progress-bar -o "$CLOUD_IMG" "$CLOUD_IMAGE_URL"
info "Customizing image (installing FreeIPA packages)..."
virt-customize -a "$CLOUD_IMG" \
--install freeipa-server,freeipa-server-dns,ansible-core,python3-netaddr \
--copy-in "$IMAGE_SRC/ipa-first-boot.sh":/usr/local/sbin/ \
--copy-in "$IMAGE_SRC/ipa-first-boot.service":/etc/systemd/system/ \
--run-command "chmod +x /usr/local/sbin/ipa-first-boot.sh" \
--run-command "systemctl enable ipa-first-boot.service" \
--selinux-relabel
qemu-img convert -O qcow2 "$CLOUD_IMG" "$QCOW2_OUT"
log "QCOW2: $QCOW2_OUT"
# Cloud-init snippet
cat > "$OUTPUT_DIR/cloud-init-user-data.yml" <<CIEOF
#cloud-config
# Attach this as cloud-init user-data when creating the Proxmox VM.
hostname: ${IPA_HOSTNAME%%.*}
fqdn: $IPA_HOSTNAME
manage_etc_hosts: true
write_files:
- path: /etc/environment
append: true
content: |
IPA_DOMAIN=$IPA_DOMAIN
IPA_REALM=$IPA_REALM
IPA_ADMIN_PASSWORD=ChangeMe123!
IPA_DM_PASSWORD=ChangeMe456!
runcmd:
- systemctl enable --now ipa-first-boot.service
CIEOF
log "Cloud-init snippet: $OUTPUT_DIR/cloud-init-user-data.yml"
info "Proxmox VM import:"
info " qm create <VMID> --name freeipa --memory 4096 --cores 4 --net0 virtio,bridge=vmbr0"
info " qm importdisk <VMID> $QCOW2_OUT local-lvm"
info " qm set <VMID> --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-<VMID>-disk-0"
info " qm set <VMID> --ide2 local-lvm:cloudinit --boot c --bootdisk scsi0"
;;
oci-archive)
section "OCI archive (skopeo)"
if ! command -v skopeo &>/dev/null; then
error "skopeo not found. Install: sudo pacman -S skopeo"
exit 1
fi
OCI_ARCHIVE="$OUTPUT_DIR/freeipa-server-oci.tar"
info "Exporting OCI archive..."
skopeo copy \
"${ENGINE}-daemon:${IMAGE_TAG}" \
"oci-archive:${OCI_ARCHIVE}:latest"
log "OCI archive: $OCI_ARCHIVE"
info "Import on air-gapped host:"
info " skopeo copy oci-archive:freeipa-server-oci.tar docker-daemon:freeipa-server:latest"
info " # or for podman:"
info " skopeo copy oci-archive:freeipa-server-oci.tar containers-storage:freeipa-server:latest"
;;
esac
# ─── Generate compose + .env for all targets ─────────────────────────────────
section "Generating deployment files → $OUTPUT_DIR"
cp "$IMAGE_SRC/docker-compose.yml" "$OUTPUT_DIR/docker-compose.yml"
cp "$IMAGE_SRC/keycloak-configure.sh" "$OUTPUT_DIR/keycloak-configure.sh"
chmod +x "$OUTPUT_DIR/keycloak-configure.sh"
# Patch compose to reference the built image tag instead of building locally
sed -i "s|image: freeipa-server:local|image: $IMAGE_TAG|" "$OUTPUT_DIR/docker-compose.yml"
if [[ "$WITH_KEYCLOAK" == false ]]; then
# Comment out Keycloak and postgres services
python3 - <<PYEOF "$OUTPUT_DIR/docker-compose.yml"
import sys, re
path = sys.argv[1]
with open(path) as f: content = f.read()
# Add a note but keep the file intact so it's easy to re-enable
header = "# Keycloak disabled at build time. Remove the '# ' prefixes below to enable.\n"
content = re.sub(r'(\n (postgres|keycloak):\n)', lambda m: '\n # ' + m.group(0).lstrip('\n'), content)
with open(path, 'w') as f: f.write(content)
PYEOF
fi
# Generate .env with current values as defaults
cat > "$OUTPUT_DIR/.env" <<ENVEOF
# Generated by freeipa-image-builder.sh — edit before use
IPA_HOSTNAME=$IPA_HOSTNAME
IPA_DOMAIN=$IPA_DOMAIN
IPA_REALM=$IPA_REALM
IPA_ADMIN_PASSWORD=ChangeMe123!
IPA_DM_PASSWORD=ChangeMe456!
IPA_SETUP_DNS=$IPA_SETUP_DNS
IPA_DNS_FORWARDER=
IPA_SETUP_KRA=false
KC_HOSTNAME=$KC_HOSTNAME
KC_REALM=$KC_REALM
KC_ADMIN=admin
KC_ADMIN_PASSWORD=ChangeMe789!
KC_DB_PASSWORD=ChangeMe000!
IPA_BIND_DN=cn=Directory Manager
IPA_BIND_PASSWORD=
IPA_USE_LDAPS=false
ENVEOF
log ".env (edit passwords before use)"
log "docker-compose.yml"
log "keycloak-configure.sh"
# ─── Final summary ────────────────────────────────────────────────────────────
section "Done"
cat <<EOF
Image: $IMAGE_TAG
Target: $TARGET
Output: $OUTPUT_DIR/
Quick start:
cd $OUTPUT_DIR
\$EDITOR .env # set real passwords
$ENGINE compose up -d$([ "$TARGET" == "docker" ] && echo "" || echo " freeipa")
$ENGINE compose logs -f freeipa # watch first-boot (~10 min)
$([ "$WITH_KEYCLOAK" == "true" ] && cat <<KC
# After FreeIPA is healthy:
KC_ADMIN_PASSWORD=<pw> IPA_SERVER=$IPA_HOSTNAME \\
IPA_DOMAIN=$IPA_DOMAIN IPA_DM_PASSWORD=<pw> \\
./keycloak-configure.sh
KC
)
EOF