feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready

This commit is contained in:
2025-10-31 01:39:24 +01:00
parent 55c04e4fd0
commit e26eb2aa12
601 changed files with 44184 additions and 32477 deletions

View File

@@ -0,0 +1,241 @@
#!/bin/bash
#
# Main Deployment Script
# Uses script framework for professional deployment automation
#
set -euo pipefail
# Determine script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source libraries
# shellcheck source=./lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"
# shellcheck source=./lib/ansible.sh
source "${SCRIPT_DIR}/lib/ansible.sh"
# Configuration
readonly DEPLOYMENT_NAME="Framework Production Deployment"
readonly START_TIME=$(date +%s)
# Usage information
usage() {
cat << EOF
Usage: $0 [OPTIONS] [GIT_REPO_URL]
Professional deployment automation using Ansible.
OPTIONS:
-h, --help Show this help message
-c, --check Run in check mode (dry-run)
-v, --verbose Enable verbose output
-d, --debug Enable debug logging
-f, --force Skip confirmation prompts
--no-health-check Skip health checks
EXAMPLES:
# Deploy from existing code on server
$0
# Deploy from specific Git repository
$0 https://github.com/user/repo.git
# Dry-run to see what would happen
$0 --check
# Debug mode
$0 --debug
EOF
exit 0
}
# Parse command line arguments
parse_args() {
local git_repo_url=""
local check_mode=false
local force=false
local health_check=true
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
;;
-c|--check)
check_mode=true
shift
;;
-v|--verbose)
set -x
shift
;;
-d|--debug)
export DEBUG=1
shift
;;
-f|--force)
force=true
shift
;;
--no-health-check)
health_check=false
shift
;;
*)
if [[ -z "$git_repo_url" ]]; then
git_repo_url="$1"
else
log_error "Unknown argument: $1"
usage
fi
shift
;;
esac
done
echo "$check_mode|$force|$health_check|$git_repo_url"
}
# Pre-deployment checks
pre_deployment_checks() {
log_step "Running pre-deployment checks..."
# Check Ansible
check_ansible || die "Ansible check failed"
# Test connectivity
test_ansible_connectivity || die "Connectivity check failed"
# Check playbook syntax
local playbook="${ANSIBLE_PLAYBOOK_DIR}/deploy.yml"
if [[ -f "$playbook" ]]; then
check_playbook_syntax "$playbook" || log_warning "Playbook syntax check failed"
fi
log_success "Pre-deployment checks passed"
}
# Deployment summary
show_deployment_summary() {
local git_repo_url="$1"
local check_mode="$2"
echo ""
echo "========================================="
echo " ${DEPLOYMENT_NAME}"
echo "========================================="
echo ""
echo "Mode: $([ "$check_mode" = "true" ] && echo "CHECK (Dry-Run)" || echo "PRODUCTION")"
echo "Target: 94.16.110.151 (production)"
echo "Services: framework_web, framework_queue-worker"
if [[ -n "$git_repo_url" ]]; then
echo "Git Repo: $git_repo_url"
else
echo "Source: Existing code on server"
fi
echo "Ansible: $(ansible --version | head -1)"
echo "Timestamp: $(timestamp)"
echo ""
}
# Post-deployment health check
post_deployment_health_check() {
log_step "Running post-deployment health checks..."
log_info "Checking service status..."
if ansible_adhoc production_server shell "docker stack services framework" &> /dev/null; then
log_success "Services are running"
else
log_warning "Could not verify service status"
fi
log_info "Testing website availability..."
if ansible_adhoc production_server shell "curl -k -s -o /dev/null -w '%{http_code}' https://michaelschiemer.de/" | grep -q "200\|302"; then
log_success "Website is responding"
else
log_warning "Website health check failed"
fi
log_success "Health checks completed"
}
# Main deployment function
main() {
# Parse arguments
IFS='|' read -r check_mode force health_check git_repo_url <<< "$(parse_args "$@")"
# Show summary
show_deployment_summary "$git_repo_url" "$check_mode"
# Confirm deployment
if [[ "$force" != "true" ]] && [[ "$check_mode" != "true" ]]; then
if ! confirm "Proceed with deployment?" "n"; then
log_warning "Deployment cancelled by user"
exit 0
fi
echo ""
fi
# Pre-deployment checks
pre_deployment_checks
# Run deployment
log_step "Starting deployment..."
echo ""
if [[ "$check_mode" = "true" ]]; then
local playbook="${ANSIBLE_PLAYBOOK_DIR}/deploy.yml"
ansible_dry_run "$playbook" ${git_repo_url:+-e "git_repo_url=$git_repo_url"}
else
run_deployment "$git_repo_url"
fi
local deployment_exit_code=$?
if [[ $deployment_exit_code -eq 0 ]]; then
echo ""
log_success "Deployment completed successfully!"
# Post-deployment health check
if [[ "$health_check" = "true" ]] && [[ "$check_mode" != "true" ]]; then
echo ""
post_deployment_health_check
fi
# Show deployment stats
local end_time=$(date +%s)
local elapsed=$(duration "$START_TIME" "$end_time")
echo ""
echo "========================================="
echo " Deployment Summary"
echo "========================================="
echo "Status: SUCCESS ✅"
echo "Duration: $elapsed"
echo "Website: https://michaelschiemer.de"
echo "Timestamp: $(timestamp)"
echo "========================================="
echo ""
return 0
else
echo ""
log_error "Deployment failed!"
echo ""
log_info "Troubleshooting:"
log_info " 1. Check Ansible logs above"
log_info " 2. SSH to server: ssh -i ~/.ssh/production deploy@94.16.110.151"
log_info " 3. Check services: docker stack services framework"
log_info " 4. View logs: docker service logs framework_web --tail 50"
echo ""
return 1
fi
}
# Execute main function
main "$@"

View File

@@ -0,0 +1,361 @@
#!/bin/bash
#
# Deployment Diagnostics Script
# Purpose: Comprehensive diagnostics for troubleshooting deployment issues
#
# Usage:
# ./scripts/deployment-diagnostics.sh # Run all diagnostics
# ./scripts/deployment-diagnostics.sh --quick # Quick checks only
# ./scripts/deployment-diagnostics.sh --verbose # Verbose output
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
PRODUCTION_SERVER="94.16.110.151"
REGISTRY="git.michaelschiemer.de:5000"
STACK_NAME="framework"
IMAGE="framework"
QUICK_MODE=false
VERBOSE=false
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_error() {
echo -e "${RED}${NC} $1"
}
log_success() {
echo -e "${GREEN}${NC} $1"
}
log_warn() {
echo -e "${YELLOW}${NC} $1"
}
log_info() {
echo -e "${BLUE}${NC} $1"
}
log_section() {
echo ""
echo -e "${CYAN}═══ $1 ═══${NC}"
}
# SSH helper
ssh_exec() {
ssh -i ~/.ssh/production deploy@"${PRODUCTION_SERVER}" "$@" 2>/dev/null || echo "SSH_FAILED"
}
# Check local prerequisites
check_local() {
log_section "Local Environment"
# Git status
if git status &> /dev/null; then
log_success "Git repository detected"
BRANCH=$(git rev-parse --abbrev-ref HEAD)
log_info "Current branch: ${BRANCH}"
if [[ -n $(git status --porcelain) ]]; then
log_warn "Working directory has uncommitted changes"
else
log_success "Working directory is clean"
fi
else
log_error "Not in a git repository"
fi
# Docker
if command -v docker &> /dev/null; then
log_success "Docker installed"
DOCKER_VERSION=$(docker --version | cut -d' ' -f3 | tr -d ',')
log_info "Version: ${DOCKER_VERSION}"
else
log_error "Docker not found"
fi
# Ansible
if command -v ansible-playbook &> /dev/null; then
log_success "Ansible installed"
ANSIBLE_VERSION=$(ansible-playbook --version | head -1 | cut -d' ' -f2)
log_info "Version: ${ANSIBLE_VERSION}"
else
log_error "Ansible not found"
fi
# SSH key
if [[ -f ~/.ssh/production ]]; then
log_success "Production SSH key found"
else
log_error "Production SSH key not found at ~/.ssh/production"
fi
}
# Check SSH connectivity
check_ssh() {
log_section "SSH Connectivity"
RESULT=$(ssh_exec "echo 'OK'")
if [[ "$RESULT" == "OK" ]]; then
log_success "SSH connection to production server"
else
log_error "Cannot connect to production server via SSH"
log_info "Check: ssh -i ~/.ssh/production deploy@${PRODUCTION_SERVER}"
return 1
fi
}
# Check Docker Swarm
check_docker_swarm() {
log_section "Docker Swarm Status"
SWARM_STATUS=$(ssh_exec "docker info | grep 'Swarm:' | awk '{print \$2}'")
if [[ "$SWARM_STATUS" == "active" ]]; then
log_success "Docker Swarm is active"
# Manager nodes
MANAGERS=$(ssh_exec "docker node ls --filter role=manager --format '{{.Hostname}}'")
log_info "Manager nodes: ${MANAGERS}"
# Worker nodes
WORKERS=$(ssh_exec "docker node ls --filter role=worker --format '{{.Hostname}}' | wc -l")
log_info "Worker nodes: ${WORKERS}"
else
log_error "Docker Swarm is not active"
return 1
fi
}
# Check services
check_services() {
log_section "Framework Services"
# List services
SERVICES=$(ssh_exec "docker service ls --filter 'name=${STACK_NAME}' --format '{{.Name}}: {{.Replicas}}'")
if [[ -n "$SERVICES" ]]; then
log_success "Framework services found"
echo "$SERVICES" | while read -r line; do
log_info "$line"
done
else
log_error "No framework services found"
return 1
fi
# Check web service
WEB_STATUS=$(ssh_exec "docker service ps ${STACK_NAME}_web --filter 'desired-state=running' --format '{{.CurrentState}}' | head -1")
if [[ "$WEB_STATUS" =~ Running ]]; then
log_success "Web service is running"
else
log_error "Web service is not running: ${WEB_STATUS}"
fi
# Check worker service
WORKER_STATUS=$(ssh_exec "docker service ps ${STACK_NAME}_queue-worker --filter 'desired-state=running' --format '{{.CurrentState}}' | head -1")
if [[ "$WORKER_STATUS" =~ Running ]]; then
log_success "Queue worker is running"
else
log_error "Queue worker is not running: ${WORKER_STATUS}"
fi
}
# Check Docker images
check_images() {
log_section "Docker Images"
# Current running image
CURRENT_IMAGE=$(ssh_exec "docker service inspect ${STACK_NAME}_web --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'")
if [[ -n "$CURRENT_IMAGE" ]]; then
log_success "Current image: ${CURRENT_IMAGE}"
else
log_error "Cannot determine current image"
fi
# Available images (last 5)
log_info "Available images (last 5):"
ssh_exec "docker images ${REGISTRY}/${IMAGE} --format ' {{.Tag}} ({{.CreatedAt}})' | grep -v buildcache | head -5"
}
# Check networks
check_networks() {
log_section "Docker Networks"
NETWORKS=$(ssh_exec "docker network ls --filter 'name=${STACK_NAME}' --format '{{.Name}}: {{.Driver}}'")
if [[ -n "$NETWORKS" ]]; then
log_success "Framework networks found"
echo "$NETWORKS" | while read -r line; do
log_info "$line"
done
else
log_warn "No framework-specific networks found"
fi
}
# Check volumes
check_volumes() {
log_section "Docker Volumes"
VOLUMES=$(ssh_exec "docker volume ls --filter 'name=${STACK_NAME}' --format '{{.Name}}'")
if [[ -n "$VOLUMES" ]]; then
log_success "Framework volumes found"
echo "$VOLUMES" | while read -r line; do
log_info "$line"
done
else
log_warn "No framework-specific volumes found"
fi
}
# Check application health
check_app_health() {
log_section "Application Health"
# Main health endpoint
HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" https://michaelschiemer.de/health || echo "000")
if [[ "$HTTP_CODE" == "200" ]] || [[ "$HTTP_CODE" == "302" ]]; then
log_success "Application health endpoint: ${HTTP_CODE}"
else
log_error "Application health endpoint failed: ${HTTP_CODE}"
fi
# Database health
DB_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" https://michaelschiemer.de/health/database || echo "000")
if [[ "$DB_CODE" == "200" ]]; then
log_success "Database connectivity: OK"
else
log_warn "Database connectivity: ${DB_CODE}"
fi
# Redis health
REDIS_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" https://michaelschiemer.de/health/redis || echo "000")
if [[ "$REDIS_CODE" == "200" ]]; then
log_success "Redis connectivity: OK"
else
log_warn "Redis connectivity: ${REDIS_CODE}"
fi
}
# Check Docker secrets
check_secrets() {
log_section "Docker Secrets"
SECRETS=$(ssh_exec "docker secret ls --format '{{.Name}}' | wc -l")
if [[ "$SECRETS" -gt 0 ]]; then
log_success "Docker secrets configured: ${SECRETS} secrets"
else
log_warn "No Docker secrets found"
fi
}
# Check recent logs
check_logs() {
log_section "Recent Logs"
log_info "Last 20 lines from web service:"
ssh_exec "docker service logs ${STACK_NAME}_web --tail 20"
}
# Check Gitea runner
check_gitea_runner() {
log_section "Gitea Actions Runner"
RUNNER_STATUS=$(ssh_exec "systemctl is-active gitea-runner 2>/dev/null || echo 'not-found'")
if [[ "$RUNNER_STATUS" == "active" ]]; then
log_success "Gitea runner service is active"
elif [[ "$RUNNER_STATUS" == "not-found" ]]; then
log_warn "Gitea runner service not found (may not be installed yet)"
else
log_error "Gitea runner service is ${RUNNER_STATUS}"
fi
}
# Resource usage
check_resources() {
log_section "Resource Usage"
# Disk usage
DISK_USAGE=$(ssh_exec "df -h / | tail -1 | awk '{print \$5}'")
log_info "Disk usage: ${DISK_USAGE}"
# Memory usage
MEMORY_USAGE=$(ssh_exec "free -h | grep Mem | awk '{print \$3\"/\"\$2}'")
log_info "Memory usage: ${MEMORY_USAGE}"
# Docker disk usage
log_info "Docker disk usage:"
ssh_exec "docker system df"
}
# Parse arguments
for arg in "$@"; do
case $arg in
--quick)
QUICK_MODE=true
;;
--verbose)
VERBOSE=true
;;
esac
done
# Main diagnostics
main() {
echo ""
echo -e "${CYAN}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ DEPLOYMENT DIAGNOSTICS REPORT ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
check_local
check_ssh || { log_error "SSH connectivity failed - cannot continue"; exit 1; }
check_docker_swarm
check_services
check_images
check_app_health
if [[ "$QUICK_MODE" == false ]]; then
check_networks
check_volumes
check_secrets
check_gitea_runner
check_resources
if [[ "$VERBOSE" == true ]]; then
check_logs
fi
fi
echo ""
echo -e "${CYAN}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ DIAGNOSTICS COMPLETED ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
log_info "For detailed logs: ./scripts/deployment-diagnostics.sh --verbose"
log_info "For service recovery: ./scripts/service-recovery.sh recover"
echo ""
}
main "$@"

View File

@@ -0,0 +1,171 @@
#!/bin/bash
#
# Emergency Rollback Script
# Purpose: Fast rollback with minimal user interaction
#
# Usage:
# ./scripts/emergency-rollback.sh # Interactive mode
# ./scripts/emergency-rollback.sh <image-tag> # Direct rollback
# ./scripts/emergency-rollback.sh list # List available tags
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
ANSIBLE_DIR="${PROJECT_ROOT}/deployment/ansible"
INVENTORY="${ANSIBLE_DIR}/inventory/production.yml"
PRODUCTION_SERVER="94.16.110.151"
REGISTRY="git.michaelschiemer.de:5000"
IMAGE="framework"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
# List available image tags
list_tags() {
log_info "Fetching available image tags from production..."
ssh -i ~/.ssh/production deploy@"${PRODUCTION_SERVER}" \
"docker images ${REGISTRY}/${IMAGE} --format '{{.Tag}}' | grep -v buildcache | head -20"
echo ""
log_info "Current running version:"
ssh -i ~/.ssh/production deploy@"${PRODUCTION_SERVER}" \
"docker service inspect framework_web --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'"
}
# Get current image tag
get_current_tag() {
ssh -i ~/.ssh/production deploy@"${PRODUCTION_SERVER}" \
"docker service inspect framework_web --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' | cut -d':' -f2"
}
# Emergency rollback
emergency_rollback() {
local target_tag="$1"
echo ""
log_warn "╔════════════════════════════════════════════════════════╗"
log_warn "║ 🚨 EMERGENCY ROLLBACK INITIATED 🚨 ║"
log_warn "╚════════════════════════════════════════════════════════╝"
echo ""
local current_tag=$(get_current_tag)
echo "Current Version: ${current_tag}"
echo "Target Version: ${target_tag}"
echo ""
if [[ "${current_tag}" == "${target_tag}" ]]; then
log_warn "Already running ${target_tag}. No rollback needed."
exit 0
fi
log_warn "This will immediately rollback production WITHOUT health checks."
log_warn "Use only in emergency situations."
echo ""
read -p "Type 'ROLLBACK' to confirm: " -r
if [[ ! "$REPLY" == "ROLLBACK" ]]; then
log_info "Rollback cancelled"
exit 0
fi
log_info "Executing emergency rollback via Ansible..."
cd "${ANSIBLE_DIR}"
ansible-playbook \
-i "${INVENTORY}" \
playbooks/emergency-rollback.yml \
-e "rollback_tag=${target_tag}"
echo ""
log_warn "╔════════════════════════════════════════════════════════╗"
log_warn "║ MANUAL VERIFICATION REQUIRED ║"
log_warn "╚════════════════════════════════════════════════════════╝"
echo ""
log_warn "1. Check application: https://michaelschiemer.de"
log_warn "2. Run health check: cd deployment && ansible-playbook -i ansible/inventory/production.yml ansible/playbooks/health-check.yml"
log_warn "3. Check service logs: ssh deploy@${PRODUCTION_SERVER} 'docker service logs framework_web --tail 100'"
echo ""
}
# Interactive mode
interactive_rollback() {
log_info "🚨 Emergency Rollback - Interactive Mode"
echo ""
log_info "Available image tags (last 20):"
list_tags
echo ""
read -p "Enter image tag to rollback to: " -r target_tag
if [[ -z "$target_tag" ]]; then
log_error "No tag provided"
exit 1
fi
emergency_rollback "$target_tag"
}
# Main
main() {
case "${1:-interactive}" in
list)
list_tags
;;
interactive)
interactive_rollback
;;
help|--help|-h)
cat <<EOF
Emergency Rollback Script
Usage: $0 [command|tag]
Commands:
list List available image tags on production
interactive Interactive rollback mode (default)
<image-tag> Direct rollback to specific tag
help Show this help
Examples:
$0 list # List available versions
$0 # Interactive mode
$0 abc1234-123456 # Rollback to specific tag
Emergency Procedures:
1. List versions: $0 list
2. Choose version: $0 <tag>
3. Verify manually: https://michaelschiemer.de
4. Run health check: cd deployment && ansible-playbook -i ansible/inventory/production.yml ansible/playbooks/health-check.yml
EOF
;;
*)
# Direct rollback with provided tag
emergency_rollback "$1"
;;
esac
}
main "$@"

View File

@@ -0,0 +1,160 @@
#!/bin/bash
#
# Ansible Integration Library
# Provides helpers for Ansible operations
#
# Source common library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "${SCRIPT_DIR}/common.sh"
# Default Ansible paths
readonly ANSIBLE_DIR="${ANSIBLE_DIR:-${SCRIPT_DIR}/../../ansible}"
readonly ANSIBLE_INVENTORY="${ANSIBLE_INVENTORY:-${ANSIBLE_DIR}/inventory/production.yml}"
readonly ANSIBLE_PLAYBOOK_DIR="${ANSIBLE_PLAYBOOK_DIR:-${ANSIBLE_DIR}/playbooks}"
# Check Ansible installation
check_ansible() {
log_step "Checking Ansible installation..."
require_command "ansible" "sudo apt install ansible" || return 1
require_command "ansible-playbook" || return 1
local version
version=$(ansible --version | head -1)
log_success "Ansible installed: $version"
}
# Test Ansible connectivity
test_ansible_connectivity() {
local inventory="${1:-$ANSIBLE_INVENTORY}"
log_step "Testing Ansible connectivity..."
if ! ansible all -i "$inventory" -m ping &> /dev/null; then
log_error "Cannot connect to production server"
log_info "Check:"
log_info " - SSH key: ~/.ssh/production"
log_info " - Network connectivity"
log_info " - Server availability"
return 1
fi
log_success "Connection successful"
return 0
}
# Run Ansible playbook
run_ansible_playbook() {
local playbook="$1"
shift
local extra_args=("$@")
log_step "Running Ansible playbook: $(basename "$playbook")"
# Build command
local cmd="ansible-playbook -i ${ANSIBLE_INVENTORY} ${playbook}"
# Add extra args
if [[ ${#extra_args[@]} -gt 0 ]]; then
cmd="${cmd} ${extra_args[*]}"
fi
log_debug "Command: $cmd"
# Execute with proper error handling
if eval "$cmd"; then
log_success "Playbook completed successfully"
return 0
else
local exit_code=$?
log_error "Playbook failed with exit code $exit_code"
return $exit_code
fi
}
# Run deployment playbook
run_deployment() {
local git_repo_url="${1:-}"
local playbook="${ANSIBLE_PLAYBOOK_DIR}/deploy.yml"
if [[ ! -f "$playbook" ]]; then
log_error "Deployment playbook not found: $playbook"
return 1
fi
log_step "Starting deployment..."
local extra_args=()
if [[ -n "$git_repo_url" ]]; then
extra_args+=("-e" "git_repo_url=${git_repo_url}")
log_info "Git repository: $git_repo_url"
else
log_info "Using existing code on server"
fi
run_ansible_playbook "$playbook" "${extra_args[@]}"
}
# Get Ansible facts
get_ansible_facts() {
local inventory="${1:-$ANSIBLE_INVENTORY}"
local host="${2:-production_server}"
ansible "$host" -i "$inventory" -m setup
}
# Ansible dry-run
ansible_dry_run() {
local playbook="$1"
shift
local extra_args=("$@")
log_step "Running dry-run (check mode)..."
extra_args+=("--check" "--diff")
run_ansible_playbook "$playbook" "${extra_args[@]}"
}
# List Ansible hosts
list_ansible_hosts() {
local inventory="${1:-$ANSIBLE_INVENTORY}"
log_step "Listing Ansible hosts..."
ansible-inventory -i "$inventory" --list
}
# Check playbook syntax
check_playbook_syntax() {
local playbook="$1"
log_step "Checking playbook syntax..."
if ansible-playbook --syntax-check "$playbook" &> /dev/null; then
log_success "Syntax check passed"
return 0
else
log_error "Syntax check failed"
return 1
fi
}
# Execute Ansible ad-hoc command
ansible_adhoc() {
local host="$1"
local module="$2"
shift 2
local args=("$@")
log_step "Running ad-hoc command on $host..."
ansible "$host" -i "$ANSIBLE_INVENTORY" -m "$module" -a "${args[*]}"
}
# Export functions
export -f check_ansible test_ansible_connectivity run_ansible_playbook
export -f run_deployment get_ansible_facts ansible_dry_run
export -f list_ansible_hosts check_playbook_syntax ansible_adhoc

View File

@@ -0,0 +1,215 @@
#!/bin/bash
#
# Common Library Functions for Deployment Scripts
# Provides unified logging, error handling, and utilities
#
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly CYAN='\033[0;36m'
readonly MAGENTA='\033[0;35m'
readonly NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE} ${1}${NC}"
}
log_success() {
echo -e "${GREEN}${1}${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ ${1}${NC}"
}
log_error() {
echo -e "${RED}${1}${NC}"
}
log_debug() {
if [[ "${DEBUG:-0}" == "1" ]]; then
echo -e "${CYAN}🔍 ${1}${NC}"
fi
}
log_step() {
echo -e "${MAGENTA}▶️ ${1}${NC}"
}
# Error handling
die() {
log_error "$1"
exit "${2:-1}"
}
# Check if command exists
command_exists() {
command -v "$1" &> /dev/null
}
# Validate prerequisites
require_command() {
local cmd="$1"
local install_hint="${2:-}"
if ! command_exists "$cmd"; then
log_error "Required command not found: $cmd"
[[ -n "$install_hint" ]] && log_info "Install with: $install_hint"
return 1
fi
return 0
}
# Run command with retry logic
run_with_retry() {
local max_attempts="${1}"
local delay="${2}"
shift 2
local cmd=("$@")
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
if "${cmd[@]}"; then
return 0
fi
if [[ $attempt -lt $max_attempts ]]; then
log_warning "Command failed (attempt $attempt/$max_attempts). Retrying in ${delay}s..."
sleep "$delay"
fi
((attempt++))
done
log_error "Command failed after $max_attempts attempts"
return 1
}
# Execute command and capture output
execute() {
local cmd="$1"
log_debug "Executing: $cmd"
eval "$cmd"
}
# Spinner for long-running operations
spinner() {
local pid=$1
local delay=0.1
local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
while ps -p "$pid" > /dev/null 2>&1; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# Progress bar
progress_bar() {
local current=$1
local total=$2
local width=50
local percentage=$((current * 100 / total))
local completed=$((width * current / total))
local remaining=$((width - completed))
printf "\r["
printf "%${completed}s" | tr ' ' '█'
printf "%${remaining}s" | tr ' ' '░'
printf "] %3d%%" "$percentage"
if [[ $current -eq $total ]]; then
echo ""
fi
}
# Confirm action
confirm() {
local prompt="${1:-Are you sure?}"
local default="${2:-n}"
if [[ "$default" == "y" ]]; then
prompt="$prompt [Y/n] "
else
prompt="$prompt [y/N] "
fi
read -rp "$prompt" response
response=${response:-$default}
[[ "$response" =~ ^[Yy]$ ]]
}
# Parse YAML-like config
parse_config() {
local config_file="$1"
local key="$2"
if [[ ! -f "$config_file" ]]; then
log_error "Config file not found: $config_file"
return 1
fi
grep "^${key}:" "$config_file" | sed "s/^${key}:[[:space:]]*//" | tr -d '"'
}
# Timestamp functions
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
timestamp_file() {
date '+%Y%m%d_%H%M%S'
}
# Duration calculation
duration() {
local start=$1
local end=${2:-$(date +%s)}
local elapsed=$((end - start))
local hours=$((elapsed / 3600))
local minutes=$(((elapsed % 3600) / 60))
local seconds=$((elapsed % 60))
if [[ $hours -gt 0 ]]; then
printf "%dh %dm %ds" "$hours" "$minutes" "$seconds"
elif [[ $minutes -gt 0 ]]; then
printf "%dm %ds" "$minutes" "$seconds"
else
printf "%ds" "$seconds"
fi
}
# Cleanup handler
cleanup_handlers=()
register_cleanup() {
cleanup_handlers+=("$1")
}
cleanup() {
log_info "Running cleanup handlers..."
for handler in "${cleanup_handlers[@]}"; do
eval "$handler" || log_warning "Cleanup handler failed: $handler"
done
}
trap cleanup EXIT
# Export functions for use in other scripts
export -f log_info log_success log_warning log_error log_debug log_step
export -f die command_exists require_command run_with_retry execute
export -f spinner progress_bar confirm parse_config
export -f timestamp timestamp_file duration
export -f register_cleanup cleanup

View File

@@ -0,0 +1,184 @@
#!/bin/bash
#
# Manual Deployment Fallback Script
# Purpose: Deploy manually when Gitea Actions is unavailable
#
# Usage:
# ./scripts/manual-deploy-fallback.sh [branch] # Deploy specific branch
# ./scripts/manual-deploy-fallback.sh # Deploy current branch
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
ANSIBLE_DIR="${PROJECT_ROOT}/deployment/ansible"
INVENTORY="${ANSIBLE_DIR}/inventory/production.yml"
PRODUCTION_SERVER="94.16.110.151"
REGISTRY="git.michaelschiemer.de:5000"
IMAGE="framework"
BRANCH="${1:-$(git rev-parse --abbrev-ref HEAD)}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_step() {
echo -e "${BLUE}[STEP]${NC} $1"
}
# Check prerequisites
check_prerequisites() {
log_step "Checking prerequisites..."
# Check if git is clean
if [[ -n $(git status --porcelain) ]]; then
log_error "Git working directory is not clean. Commit or stash changes first."
exit 1
fi
# Check if ansible is installed
if ! command -v ansible-playbook &> /dev/null; then
log_error "ansible-playbook not found. Install Ansible first."
exit 1
fi
# Check if docker is available
if ! command -v docker &> /dev/null; then
log_error "docker not found. Install Docker first."
exit 1
fi
# Check SSH access to production server
if ! ssh -i ~/.ssh/production deploy@"${PRODUCTION_SERVER}" "echo 'SSH OK'" &> /dev/null; then
log_error "Cannot SSH to production server. Check your SSH key."
exit 1
fi
log_info "Prerequisites check passed"
}
# Build Docker image locally
build_image() {
log_step "Building Docker image for branch: ${BRANCH}"
cd "${PROJECT_ROOT}"
# Checkout branch
git checkout "${BRANCH}"
git pull origin "${BRANCH}"
# Get commit SHA
COMMIT_SHA=$(git rev-parse --short HEAD)
IMAGE_TAG="${COMMIT_SHA}-$(date +%s)"
log_info "Building image with tag: ${IMAGE_TAG}"
# Build image
docker build \
--file Dockerfile.production \
--tag "${REGISTRY}/${IMAGE}:${IMAGE_TAG}" \
--tag "${REGISTRY}/${IMAGE}:latest" \
--build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
--build-arg VCS_REF="${COMMIT_SHA}" \
.
log_info "Image built successfully"
}
# Push image to registry
push_image() {
log_step "Pushing image to registry..."
# Login to registry (prompt for password if needed)
log_info "Logging in to registry..."
docker login "${REGISTRY}"
# Push image
docker push "${REGISTRY}/${IMAGE}:${IMAGE_TAG}"
docker push "${REGISTRY}/${IMAGE}:latest"
log_info "Image pushed successfully"
}
# Deploy via Ansible
deploy_ansible() {
log_step "Deploying via Ansible..."
cd "${ANSIBLE_DIR}"
ansible-playbook \
-i "${INVENTORY}" \
playbooks/deploy-update.yml \
-e "image_tag=${IMAGE_TAG}" \
-e "git_commit_sha=${COMMIT_SHA}"
log_info "Ansible deployment completed"
}
# Run health checks
run_health_checks() {
log_step "Running health checks..."
cd "${ANSIBLE_DIR}"
ansible-playbook \
-i "${INVENTORY}" \
playbooks/health-check.yml
log_info "Health checks passed"
}
# Main deployment flow
main() {
echo ""
log_warn "╔════════════════════════════════════════════════════════╗"
log_warn "║ MANUAL DEPLOYMENT FALLBACK (No Gitea Actions) ║"
log_warn "╚════════════════════════════════════════════════════════╝"
echo ""
log_info "Branch: ${BRANCH}"
echo ""
read -p "Continue with manual deployment? (yes/no): " -r
if [[ ! "$REPLY" =~ ^[Yy][Ee][Ss]$ ]]; then
log_info "Deployment cancelled"
exit 0
fi
check_prerequisites
build_image
push_image
deploy_ansible
run_health_checks
echo ""
log_warn "╔════════════════════════════════════════════════════════╗"
log_warn "║ MANUAL DEPLOYMENT COMPLETED ║"
log_warn "╚════════════════════════════════════════════════════════╝"
echo ""
log_info "Deployed: ${REGISTRY}/${IMAGE}:${IMAGE_TAG}"
log_info "Commit: ${COMMIT_SHA}"
log_info "Branch: ${BRANCH}"
echo ""
log_info "Verify deployment: https://michaelschiemer.de"
echo ""
}
main "$@"

View File

@@ -0,0 +1,230 @@
#!/bin/bash
#
# Service Recovery Script
# Purpose: Quick recovery for common service failures
#
# Usage:
# ./scripts/service-recovery.sh status # Check service status
# ./scripts/service-recovery.sh restart # Restart services
# ./scripts/service-recovery.sh recover # Full recovery procedure
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
PRODUCTION_SERVER="94.16.110.151"
STACK_NAME="framework"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_step() {
echo -e "${BLUE}[STEP]${NC} $1"
}
# SSH helper
ssh_exec() {
ssh -i ~/.ssh/production deploy@"${PRODUCTION_SERVER}" "$@"
}
# Check service status
check_status() {
log_step "Checking service status..."
echo ""
log_info "Docker Swarm Services:"
ssh_exec "docker service ls --filter 'name=${STACK_NAME}'"
echo ""
log_info "Web Service Details:"
ssh_exec "docker service ps ${STACK_NAME}_web --no-trunc"
echo ""
log_info "Queue Worker Details:"
ssh_exec "docker service ps ${STACK_NAME}_queue-worker --no-trunc"
echo ""
log_info "Service Logs (last 50 lines):"
ssh_exec "docker service logs ${STACK_NAME}_web --tail 50"
}
# Restart services
restart_services() {
log_step "Restarting services..."
echo ""
log_warn "This will restart all framework services"
read -p "Continue? (yes/no): " -r
if [[ ! "$REPLY" =~ ^[Yy][Ee][Ss]$ ]]; then
log_info "Restart cancelled"
exit 0
fi
# Restart web service
log_info "Restarting web service..."
ssh_exec "docker service update --force ${STACK_NAME}_web"
# Restart worker service
log_info "Restarting queue worker..."
ssh_exec "docker service update --force ${STACK_NAME}_queue-worker"
# Wait for services to stabilize
log_info "Waiting for services to stabilize (30 seconds)..."
sleep 30
# Check status
check_status
}
# Full recovery procedure
full_recovery() {
log_step "Running full recovery procedure..."
echo ""
log_warn "╔════════════════════════════════════════════════════════╗"
log_warn "║ FULL SERVICE RECOVERY PROCEDURE ║"
log_warn "╚════════════════════════════════════════════════════════╝"
echo ""
# Step 1: Check current status
log_info "Step 1/5: Check current status"
check_status
# Step 2: Check Docker Swarm health
log_info "Step 2/5: Check Docker Swarm health"
SWARM_STATUS=$(ssh_exec "docker info | grep 'Swarm: active' || echo 'inactive'")
if [[ "$SWARM_STATUS" == "inactive" ]]; then
log_error "Docker Swarm is not active!"
log_info "Attempting to reinitialize Swarm..."
ssh_exec "docker swarm init --advertise-addr ${PRODUCTION_SERVER}" || true
else
log_info "Docker Swarm is active"
fi
# Step 3: Verify network and volumes
log_info "Step 3/5: Verify Docker resources"
ssh_exec "docker network ls | grep ${STACK_NAME} || docker network create --driver overlay ${STACK_NAME}_network"
# Step 4: Restart services
log_info "Step 4/5: Restart services"
ssh_exec "docker service update --force ${STACK_NAME}_web"
ssh_exec "docker service update --force ${STACK_NAME}_queue-worker"
log_info "Waiting for services to stabilize (45 seconds)..."
sleep 45
# Step 5: Health check
log_info "Step 5/5: Run health checks"
HEALTH_CHECK=$(curl -f -k https://michaelschiemer.de/health 2>/dev/null && echo "OK" || echo "FAILED")
if [[ "$HEALTH_CHECK" == "OK" ]]; then
log_info "✅ Health check passed"
else
log_error "❌ Health check failed"
log_warn "Manual intervention may be required"
log_warn "Check logs: ssh deploy@${PRODUCTION_SERVER} 'docker service logs ${STACK_NAME}_web --tail 100'"
exit 1
fi
echo ""
log_warn "╔════════════════════════════════════════════════════════╗"
log_warn "║ RECOVERY PROCEDURE COMPLETED ║"
log_warn "╚════════════════════════════════════════════════════════╝"
echo ""
log_info "Application: https://michaelschiemer.de"
log_info "Services recovered successfully"
echo ""
}
# Clear caches
clear_caches() {
log_step "Clearing application caches..."
# Clear Redis cache
log_info "Clearing Redis cache..."
ssh_exec "docker exec \$(docker ps -q -f name=${STACK_NAME}_redis) redis-cli FLUSHALL" || log_warn "Redis cache clear failed"
# Clear file caches
log_info "Clearing file caches..."
ssh_exec "docker exec \$(docker ps -q -f name=${STACK_NAME}_web | head -1) rm -rf /var/www/html/storage/cache/*" || log_warn "File cache clear failed"
log_info "Caches cleared"
}
# Show help
show_help() {
cat <<EOF
Service Recovery Script
Usage: $0 [command]
Commands:
status Check service status and logs
restart Restart all services
recover Run full recovery procedure (recommended)
clear-cache Clear application caches
help Show this help
Examples:
$0 status # Quick status check
$0 recover # Full automated recovery
$0 restart # Just restart services
$0 clear-cache # Clear caches only
Emergency Recovery:
1. Check status: $0 status
2. Run recovery: $0 recover
3. If still failing, check logs manually:
ssh deploy@${PRODUCTION_SERVER} 'docker service logs ${STACK_NAME}_web --tail 200'
EOF
}
# Main
main() {
case "${1:-help}" in
status)
check_status
;;
restart)
restart_services
;;
recover)
full_recovery
;;
clear-cache)
clear_caches
;;
help|--help|-h)
show_help
;;
*)
log_error "Unknown command: $1"
show_help
exit 1
;;
esac
}
main "$@"

View File

@@ -0,0 +1,262 @@
#!/bin/bash
#
# Production Secrets Setup Script
# Purpose: Initialize and manage production secrets with Ansible Vault
#
# Usage:
# ./scripts/setup-production-secrets.sh init # Initialize new vault
# ./scripts/setup-production-secrets.sh deploy # Deploy secrets to production
# ./scripts/setup-production-secrets.sh rotate # Rotate secrets
# ./scripts/setup-production-secrets.sh verify # Verify secrets on server
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
ANSIBLE_DIR="${PROJECT_ROOT}/deployment/ansible"
VAULT_FILE="${ANSIBLE_DIR}/secrets/production-vault.yml"
INVENTORY="${ANSIBLE_DIR}/inventory/production.yml"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
if ! command -v ansible-vault &> /dev/null; then
log_error "ansible-vault not found. Please install Ansible."
exit 1
fi
if ! command -v openssl &> /dev/null; then
log_error "openssl not found. Please install OpenSSL."
exit 1
fi
log_info "Prerequisites OK"
}
# Generate secure random password
generate_password() {
local length="${1:-32}"
openssl rand -base64 "$length" | tr -d "=+/" | cut -c1-"$length"
}
# Generate base64 encoded app key
generate_app_key() {
openssl rand -base64 32
}
# Initialize vault with secure defaults
init_vault() {
log_info "Initializing production secrets vault..."
if [[ -f "$VAULT_FILE" ]]; then
log_warn "Vault file already exists: $VAULT_FILE"
read -p "Do you want to overwrite it? (yes/no): " -r
if [[ ! $REPLY =~ ^[Yy]es$ ]]; then
log_info "Aborting initialization"
exit 0
fi
fi
# Generate secure secrets
log_info "Generating secure secrets..."
DB_PASSWORD=$(generate_password 32)
REDIS_PASSWORD=$(generate_password 32)
APP_KEY=$(generate_app_key)
JWT_SECRET=$(generate_password 64)
REGISTRY_PASSWORD=$(generate_password 24)
# Create vault file
cat > "$VAULT_FILE" <<EOF
---
# Production Secrets Vault
# Generated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Database Credentials
vault_db_name: framework_production
vault_db_user: framework_app
vault_db_password: ${DB_PASSWORD}
# Redis Credentials
vault_redis_password: ${REDIS_PASSWORD}
# Application Secrets
vault_app_key: ${APP_KEY}
vault_jwt_secret: ${JWT_SECRET}
# Docker Registry Credentials
vault_registry_url: git.michaelschiemer.de:5000
vault_registry_user: deploy
vault_registry_password: ${REGISTRY_PASSWORD}
# Security Configuration
vault_admin_allowed_ips: "127.0.0.1,::1,94.16.110.151"
# SMTP Configuration (update these manually)
vault_smtp_host: smtp.example.com
vault_smtp_port: 587
vault_smtp_user: noreply@michaelschiemer.de
vault_smtp_password: CHANGE_ME_SMTP_PASSWORD_HERE
EOF
log_info "Vault file created with generated secrets"
log_warn "IMPORTANT: Update SMTP credentials manually if needed"
# Encrypt vault
log_info "Encrypting vault file..."
ansible-vault encrypt "$VAULT_FILE"
log_info "✅ Vault initialized successfully"
log_warn "Store the vault password securely (e.g., in password manager)"
}
# Deploy secrets to production
deploy_secrets() {
log_info "Deploying secrets to production..."
if [[ ! -f "$VAULT_FILE" ]]; then
log_error "Vault file not found: $VAULT_FILE"
log_error "Run './setup-production-secrets.sh init' first"
exit 1
fi
cd "$ANSIBLE_DIR"
log_info "Running Ansible playbook..."
ansible-playbook \
-i "$INVENTORY" \
playbooks/setup-production-secrets.yml \
--ask-vault-pass
log_info "✅ Secrets deployed successfully"
}
# Rotate secrets (regenerate and redeploy)
rotate_secrets() {
log_warn "⚠️ Secret rotation will:"
log_warn " 1. Generate new passwords/keys"
log_warn " 2. Update vault file"
log_warn " 3. Deploy to production"
log_warn " 4. Restart services"
log_warn ""
read -p "Continue with rotation? (yes/no): " -r
if [[ ! $REPLY =~ ^[Yy]es$ ]]; then
log_info "Rotation cancelled"
exit 0
fi
# Backup current vault
BACKUP_FILE="${VAULT_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
log_info "Creating backup: $BACKUP_FILE"
cp "$VAULT_FILE" "$BACKUP_FILE"
# Decrypt vault
log_info "Decrypting vault..."
ansible-vault decrypt "$VAULT_FILE"
# Generate new secrets
log_info "Generating new secrets..."
DB_PASSWORD=$(generate_password 32)
REDIS_PASSWORD=$(generate_password 32)
APP_KEY=$(generate_app_key)
JWT_SECRET=$(generate_password 64)
# Update vault file (keep registry password)
sed -i "s/vault_db_password: .*/vault_db_password: ${DB_PASSWORD}/" "$VAULT_FILE"
sed -i "s/vault_redis_password: .*/vault_redis_password: ${REDIS_PASSWORD}/" "$VAULT_FILE"
sed -i "s/vault_app_key: .*/vault_app_key: ${APP_KEY}/" "$VAULT_FILE"
sed -i "s/vault_jwt_secret: .*/vault_jwt_secret: ${JWT_SECRET}/" "$VAULT_FILE"
# Re-encrypt vault
log_info "Re-encrypting vault..."
ansible-vault encrypt "$VAULT_FILE"
log_info "✅ Secrets rotated"
log_info "Backup saved to: $BACKUP_FILE"
# Deploy rotated secrets
deploy_secrets
}
# Verify secrets on server
verify_secrets() {
log_info "Verifying secrets on production server..."
cd "$ANSIBLE_DIR"
ansible production_server \
-i "$INVENTORY" \
-m shell \
-a "docker secret ls"
log_info "Checking environment file..."
ansible production_server \
-i "$INVENTORY" \
-m stat \
-a "path=/home/deploy/secrets/.env.production"
log_info "✅ Verification complete"
}
# Main command dispatcher
main() {
check_prerequisites
case "${1:-help}" in
init)
init_vault
;;
deploy)
deploy_secrets
;;
rotate)
rotate_secrets
;;
verify)
verify_secrets
;;
help|*)
cat <<EOF
Production Secrets Management
Usage: $0 <command>
Commands:
init Initialize new secrets vault with auto-generated secure values
deploy Deploy secrets from vault to production server
rotate Rotate secrets (generate new values and redeploy)
verify Verify secrets are properly deployed on server
Examples:
$0 init # First time setup
$0 deploy # Deploy after manual vault updates
$0 rotate # Monthly security rotation
$0 verify # Check deployment status
EOF
;;
esac
}
main "$@"