Files
Varaverk/Partnership/gitea_ssh_setup.sh
T
Gmer4Lfe 369a9e6c19 Platform adapter: rename System_Essentials, add Plugin/unraid/adapter.sh, wire call sites
- Rename unRAID_Essentials/ → System_Essentials/ (git detects as rename)
- Add Plugin/unraid/adapter.sh: 13 platform_*() functions providing OS-agnostic API
  for storage health, service management, mover, user scripts, notifications,
  disk temps, and platform command validation
- Update load_config.sh: detect PLATFORM (unraid/truenas/unknown), export SCRIPTS_DIR,
  auto-source Plugin/$PLATFORM/adapter.sh after common.sh
- Wire all call sites: replace direct rc.d, pgrep/pkill, var.ini, dynamix.cfg,
  disks.ini, and validate_unraid_cmd calls with platform_*() functions across
  watchdogs, orchestrators, and System_Essentials scripts
- Update all documentation: rename refs, update webgui escalation logic,
  add platform adapter section to Plugin README, update main README with
  portability vision and corrected self-healing stack description
2026-06-04 18:14:34 -04:00

470 lines
19 KiB
Bash
Executable File

#!/bin/bash
# ==============================================================================================
# ============================= Gitea SSH Setup ================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Generates an SSH keypair for Gitea authentication and registers the public
# key with Gitea via the API. Run after the Gitea container is up — typically
# called during onboard setup or when re-keying a server.
#
# Idempotent: skips key generation if key already exists and skips registration
# if the same public key is already registered in Gitea. Use --force to
# regenerate and re-register.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Resolve Gitea API endpoint
# Container running locally → use container IP + GITEA_HTTP_PORT
# Container not local → fall back to GITEA_DOMAIN
#
# 2. Validate GITEA_API_TOKEN is set
# Token needed for API calls — create once in Gitea UI (see CONFIGURATION)
#
# 3. Generate SSH keypair (ed25519) at GITEA_SSH_KEY
# Skipped if key already exists — use --force to regenerate
#
# 4. Check if public key already registered in Gitea
# Compares fingerprints — skips POST if already registered
#
# 5. Register public key via POST /api/v1/user/keys
# Title: "unraid-{host_id}" (e.g. unraid-host1)
#
# 6. Test SSH connection to Gitea
# ssh -i GITEA_SSH_KEY -p SSH_PORT git@<gitea_host>
# Success confirms key is accepted and auth works end-to-end
#
# 7. Write ~/.ssh/config Host entry for gitea
# Skipped if entry already exists — use --force to overwrite
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Required
# ssh-keygen and ~/.ssh/ operations require consistent permissions.
#
# Single Instance Lock
# acquire_lock prevents concurrent runs.
#
# Token Validated Before Any API Call
# If HOST*_GITEA_API_TOKEN is empty, exits with a clear message pointing to
# Gitea Settings → Applications → Generate Token (scope: write:user).
#
# Key Never Clobbered Without --force
# Existing key at GITEA_SSH_KEY is left untouched unless --force is passed.
# Prevents accidentally invalidating working auth.
#
# Duplicate Registration Safe
# Checks existing keys before POST — will not create duplicate entries in Gitea.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
# HOST*_GITEA_API_TOKEN
# Personal access token for the Gitea user who owns the repository.
# Create in Gitea: Settings → Applications → Generate Token → scope: write:user
# Aliased by detect_hosts() → GITEA_API_TOKEN.
#
# master.conf
#
# GITEA_CONTAINER Container name for local IP detection (default: "Gitea")
# GITEA_DOMAIN Domain fallback if container is remote or not running
# GITEA_SSH_KEY Path for the SSH keypair (default: /root/.ssh/unraid_gitea)
# SSH_PORT Gitea SSH port (default: 221)
# GITEA_HTTP_PORT Gitea API port (default: 3000)
#
# ==============================================================================================
# STATE FILES
# ==============================================================================================
#
# ~/.ssh/config Updated with Host entry for gitea on successful setup
# GITEA_SSH_KEY Private key (permissions: 600)
# GITEA_SSH_KEY.pub Public key — registered with Gitea
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# gitea_ssh_setup.sh
# Generate key if missing, register with Gitea, test connection.
#
# gitea_ssh_setup.sh --force
# Regenerate key and re-register even if already set up.
#
# gitea_ssh_setup.sh --status
# Show key state, fingerprint, registration status, SSH test. No changes.
#
# gitea_ssh_setup.sh --dry-run
# Show what would be done without generating, registering, or writing files.
#
# gitea_ssh_setup.sh --log
# Verbose output for each step.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPTS_ROOT="$SCRIPT_DIR/.."
source "$SCRIPTS_ROOT/load_config.sh"
# ── Parse --force before parse_args ───────────────────────────────────────────────────────────
FORCE=false
FILTERED_ARGS=()
for arg in "$@"; do
case "$arg" in
--force) FORCE=true ;;
*) FILTERED_ARGS+=("$arg") ;;
esac
done
parse_args "${FILTERED_ARGS[@]}"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
platform_require_cmd \
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
"" "" \
"unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
acquire_lock
detect_hosts
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
[[ "$FORCE" == true ]] && warn "FORCE mode — existing key and registration will be replaced"
log "Identity: $MY_ID ($LOCAL_SERVER_NAME)"
log "SSH key: $GITEA_SSH_KEY"
log "SSH port: $SSH_PORT"
# ==============================================================================================
# ━━━ Resolve Gitea API Endpoint ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR Gitea API Endpoint ━━━"
GITEA_HOST=""
GITEA_API=""
# Try local container first
CONTAINER_IP=$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
"$GITEA_CONTAINER" 2>/dev/null | awk 'NF{print $1; exit}')
if [[ -n "$CONTAINER_IP" ]]; then
GITEA_HOST="$CONTAINER_IP"
GITEA_API="http://${CONTAINER_IP}:${GITEA_HTTP_PORT}/api/v1"
log "Gitea container running locally: $CONTAINER_IP:$GITEA_HTTP_PORT"
elif [[ -n "$GITEA_DOMAIN" ]]; then
GITEA_HOST="$GITEA_DOMAIN"
GITEA_API="https://${GITEA_DOMAIN}/api/v1"
warn "Gitea container not running locally — using domain: $GITEA_DOMAIN"
else
error "Cannot resolve Gitea API endpoint"
error "Gitea container ($GITEA_CONTAINER) is not running and GITEA_DOMAIN is not set in master.conf"
error "Start the Gitea container first, or set GITEA_DOMAIN"
exit 1
fi
log "API endpoint: $GITEA_API"
# ==============================================================================================
# ━━━ Validate API Token ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SHIELD API Token ━━━"
# Alias: detect_hosts() maps HOST*_GITEA_API_TOKEN → GITEA_API_TOKEN
if [[ -z "${GITEA_API_TOKEN:-}" ]]; then
error "HOST${MY_ID#HOST}_GITEA_API_TOKEN is not set in host${MY_ID#HOST}.conf"
error ""
error "Create a token in Gitea:"
error " 1. Log into Gitea"
error " 2. Top-right profile → Settings → Applications"
error " 3. Generate New Token → name: unraid-setup → scope: write:user"
error " 4. Copy the token (shown once) → paste into HOST${MY_ID#HOST}_GITEA_API_TOKEN in host${MY_ID#HOST}.conf"
exit 1
fi
# Test token against the API
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $GITEA_API_TOKEN" \
"$GITEA_API/user" 2>/dev/null)
if [[ "$HTTP_CODE" == "200" ]]; then
log "API token valid ✅"
elif [[ "$HTTP_CODE" == "401" ]]; then
error "API token rejected (HTTP 401) — token may be expired or have wrong scope"
error "Regenerate the token in Gitea: Settings → Applications → Generate Token → scope: write:user"
exit 1
else
error "Gitea API not reachable (HTTP $HTTP_CODE) at $GITEA_API"
error "Check that the Gitea container is healthy and the port is correct (GITEA_HTTP_PORT=$GITEA_HTTP_PORT)"
exit 1
fi
# ── Status mode exits here after full state display ───────────────────────────────────────────
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR API endpoint: $GITEA_API"
echo ""
if [[ -f "${GITEA_SSH_KEY}.pub" ]]; then
FINGERPRINT=$(ssh-keygen -lf "${GITEA_SSH_KEY}.pub" 2>/dev/null | awk '{print $2}')
echo " $ICON_GEAR Key file: ${GITEA_SSH_KEY}.pub"
echo " $ICON_GEAR Fingerprint: $FINGERPRINT"
# Check registration
REGISTERED_KEYS=$(curl -s \
-H "Authorization: token $GITEA_API_TOKEN" \
"$GITEA_API/user/keys" 2>/dev/null)
LOCAL_PUB=$(awk '{print $1" "$2}' "${GITEA_SSH_KEY}.pub")
if echo "$REGISTERED_KEYS" | grep -qF "$LOCAL_PUB"; then
echo " $ICON_GEAR Registered: YES ✅"
else
echo " $ICON_GEAR Registered: NO — run without --status to register"
fi
else
echo " $ICON_GEAR Key file: NOT FOUND at $GITEA_SSH_KEY"
fi
# SSH connection test
echo ""
SSH_OUTPUT=$(ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \
-o StrictHostKeyChecking=no -o ConnectTimeout=5 \
git@"$GITEA_HOST" 2>&1 || true)
if echo "$SSH_OUTPUT" | grep -q "successfully authenticated"; then
echo " $ICON_DONE SSH test: connected ✅"
echo " $ICON_INFO $(echo "$SSH_OUTPUT" | grep "successfully authenticated")"
else
echo " $ICON_ERROR SSH test: failed"
echo " $ICON_INFO $SSH_OUTPUT"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
START=$(date +%s)
SETUP_SUCCESS=true
# ==============================================================================================
# ━━━ Generate SSH Keypair ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR SSH Keypair ━━━"
KEY_TITLE="unraid-$(echo "$MY_ID" | tr '[:upper:]' '[:lower:]')"
KEY_COMMENT="$KEY_TITLE@$(hostname)"
if [[ -f "$GITEA_SSH_KEY" ]] && [[ "$FORCE" == false ]]; then
FINGERPRINT=$(ssh-keygen -lf "${GITEA_SSH_KEY}.pub" 2>/dev/null | awk '{print $2}')
log "Key already exists — skipping generation (use --force to regenerate)"
log " Path: $GITEA_SSH_KEY"
log " Fingerprint: $FINGERPRINT"
else
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would generate ed25519 key at $GITEA_SSH_KEY"
warn "DRY RUN — key comment: $KEY_COMMENT"
else
mkdir -p "$(dirname "$GITEA_SSH_KEY")"
chmod 700 "$(dirname "$GITEA_SSH_KEY")"
# Remove existing if --force
[[ "$FORCE" == true ]] && rm -f "$GITEA_SSH_KEY" "${GITEA_SSH_KEY}.pub"
if ssh-keygen -t ed25519 -f "$GITEA_SSH_KEY" -C "$KEY_COMMENT" -N "" -q; then
chmod 600 "$GITEA_SSH_KEY"
chmod 644 "${GITEA_SSH_KEY}.pub"
FINGERPRINT=$(ssh-keygen -lf "${GITEA_SSH_KEY}.pub" 2>/dev/null | awk '{print $2}')
warn "SSH key generated ✅"
log " Path: $GITEA_SSH_KEY"
log " Fingerprint: $FINGERPRINT"
else
error "Failed to generate SSH key"
SETUP_SUCCESS=false
fi
fi
fi
# ==============================================================================================
# ━━━ Register Public Key with Gitea ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_LINK Register Key with Gitea ━━━"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would POST public key to $GITEA_API/user/keys"
warn "DRY RUN — key title: $KEY_TITLE"
elif [[ "$SETUP_SUCCESS" == true ]]; then
LOCAL_PUB=$(cat "${GITEA_SSH_KEY}.pub")
LOCAL_PUB_SHORT=$(awk '{print $1" "$2}' "${GITEA_SSH_KEY}.pub")
# Check if already registered
REGISTERED_KEYS=$(curl -s \
-H "Authorization: token $GITEA_API_TOKEN" \
"$GITEA_API/user/keys" 2>/dev/null)
if [[ "$FORCE" == false ]] && echo "$REGISTERED_KEYS" | grep -qF "$LOCAL_PUB_SHORT"; then
log "Public key already registered in Gitea — skipping (use --force to re-register)"
else
# If --force, delete the existing key with this title first
if [[ "$FORCE" == true ]]; then
EXISTING_ID=$(echo "$REGISTERED_KEYS" | \
python3 -c "import json,sys; keys=json.load(sys.stdin); [print(k['id']) for k in keys if k.get('title','')=='$KEY_TITLE']" 2>/dev/null)
if [[ -n "$EXISTING_ID" ]]; then
curl -s -X DELETE \
-H "Authorization: token $GITEA_API_TOKEN" \
"$GITEA_API/user/keys/$EXISTING_ID" >/dev/null 2>&1
log "Removed existing key '$KEY_TITLE' (ID: $EXISTING_ID)"
fi
fi
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token $GITEA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"key\":$(echo "$LOCAL_PUB" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))'),\"read_only\":false,\"title\":\"$KEY_TITLE\"}" \
"$GITEA_API/user/keys" 2>/dev/null)
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -n -1)
if [[ "$HTTP_CODE" == "201" ]]; then
KEY_ID=$(echo "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" 2>/dev/null)
warn "Public key registered in Gitea ✅ (ID: $KEY_ID, title: $KEY_TITLE)"
elif [[ "$HTTP_CODE" == "422" ]]; then
warn "Key already registered under a different title — skipping"
log "Response: $BODY"
else
error "Failed to register key (HTTP $HTTP_CODE)"
log "Response: $BODY"
SETUP_SUCCESS=false
fi
fi
fi
# ==============================================================================================
# ━━━ SSH Connection Test ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR SSH Connection Test ━━━"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would test: ssh -i $GITEA_SSH_KEY -p $SSH_PORT git@$GITEA_HOST"
else
SSH_OUTPUT=$(ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \
-o StrictHostKeyChecking=no \
-o ConnectTimeout=10 \
git@"$GITEA_HOST" 2>&1 || true)
if echo "$SSH_OUTPUT" | grep -q "successfully authenticated"; then
AUTHED_AS=$(echo "$SSH_OUTPUT" | grep "successfully authenticated" | sed 's/.*authenticated as //')
warn "SSH connection successful ✅ — authenticated as $AUTHED_AS"
else
error "SSH connection test failed"
log "Output: $SSH_OUTPUT"
warn "Key may not yet be active — Gitea sometimes takes a moment to pick up new keys"
warn "Run --status in 30 seconds to recheck"
SETUP_SUCCESS=false
fi
fi
# ==============================================================================================
# ━━━ SSH Config Entry ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR SSH Config Entry ━━━"
SSH_CONFIG="$HOME/.ssh/config"
ENTRY_MARKER="Host gitea-${MY_ID,,}"
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would add Host entry to $SSH_CONFIG:"
warn " Host gitea-${MY_ID,,}"
warn " HostName $GITEA_HOST"
warn " Port $SSH_PORT"
warn " User git"
warn " IdentityFile $GITEA_SSH_KEY"
elif grep -qF "$ENTRY_MARKER" "$SSH_CONFIG" 2>/dev/null && [[ "$FORCE" == false ]]; then
log "SSH config entry already exists — skipping (use --force to update)"
else
if [[ "$FORCE" == true ]]; then
# Remove old entry block if present
if grep -qF "$ENTRY_MARKER" "$SSH_CONFIG" 2>/dev/null; then
python3 - "$SSH_CONFIG" "$ENTRY_MARKER" <<'PYEOF'
import sys
path, marker = sys.argv[1], sys.argv[2]
with open(path) as f:
lines = f.readlines()
out, skip = [], False
for line in lines:
if line.strip() == marker:
skip = True
elif skip and line.startswith('Host '):
skip = False
if not skip:
out.append(line)
with open(path, 'w') as f:
f.writelines(out)
PYEOF
log "Removed old SSH config entry"
fi
fi
mkdir -p "$(dirname "$SSH_CONFIG")"
{
echo ""
echo "Host gitea-${MY_ID,,}"
echo " HostName $GITEA_HOST"
echo " Port $SSH_PORT"
echo " User git"
echo " IdentityFile $GITEA_SSH_KEY"
} >> "$SSH_CONFIG"
chmod 600 "$SSH_CONFIG"
log "SSH config entry added: Host gitea-${MY_ID,,} ✅"
fi
END=$(date +%s)
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY GITEA SSH SETUP SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Key: $GITEA_SSH_KEY"
echo "$ICON_LINK Gitea: $GITEA_HOST:$SSH_PORT"
echo "$ICON_GEAR Key title: $KEY_TITLE"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo ""
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — no changes made"
elif [[ "$SETUP_SUCCESS" == true ]]; then
echo "$ICON_DONE Status: done ✅"
echo ""
echo "Git remote push:"
echo " GIT_SSH_COMMAND='ssh -i $GITEA_SSH_KEY -p $SSH_PORT' git push"
else
echo "$ICON_ERROR Status: SETUP HAD ERRORS — check output above"
notify "Gitea SSH setup errors on $(hostname) ($MY_ID)" \
"Gitea SSH Setup" "warning"
exit 1
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"