Skip to content

Instantly share code, notes, and snippets.

@sjha4
Last active May 13, 2026 02:32
Show Gist options
  • Select an option

  • Save sjha4/698fc13c2e4f2fa129ccfe604efd4d61 to your computer and use it in GitHub Desktop.

Select an option

Save sjha4/698fc13c2e4f2fa129ccfe604efd4d61 to your computer and use it in GitHub Desktop.
Foremanctl Backup & Restore Testing Guide - Complete Procedure with Restore Script

Foremanctl Backup & Restore Testing Guide

Purpose: Complete testing procedure for foremanctl offline backup and restore functionality
Target: Same-box restore testing (hostname must match)
Duration: 30-60 minutes
Date: 2026-05-12


Overview

This guide walks through testing the foremanctl backup and restore implementation by:

  1. Taking a baseline backup of current state
  2. Making deliberate changes (adding repos, changing secrets)
  3. Restoring from backup
  4. Verifying the system returns to baseline state

Key Test Areas:

  • ✅ Database backup and restore (8 databases if IOP enabled)
  • ✅ Pulp content and encryption keys
  • ✅ Podman secrets (72 secrets)
  • ✅ Service lifecycle management
  • ✅ Idempotent redeployment

Prerequisites

System Requirements

  • Running foremanctl deployment on RHEL 9.x
  • Foreman/Katello services healthy
  • Root or sudo access
  • Sufficient disk space (~1.5x database size + Pulp content size)

Verify Current State

# Check services are running
systemctl status foreman.target

# Check containers
podman ps

# Should see: postgresql, redis, foreman, candlepin, pulp-api, pulp-content, pulp-worker@*

Environment Setup

# Navigate to foremanctl directory
cd /root/foremanctl

# Activate Python virtual environment
source .venv/bin/activate

# Set environment variables
export OBSAH_STATE=/var/lib/foremanctl

# Verify foremanctl works
./foremanctl --help

Expected output: List of available commands including backup


Testing Procedure

PHASE 1: Initial Backup (Baseline)

Step 1.1: Take Baseline Backup

# Ensure you're in the right directory with venv activated
cd /root/foremanctl
source .venv/bin/activate
export OBSAH_STATE=/var/lib/foremanctl

# Define backup directory
export BACKUP_DIR=/var/tmp/foreman-backup-test

# Run backup (--wait-for-tasks ensures no running tasks block the backup)
./foremanctl backup $BACKUP_DIR --wait-for-tasks

Expected behavior:

  • Preflight checks run (tasks, database integrity)
  • Services stop (2-5 min downtime)
  • Databases dumped (foreman, candlepin, pulp, IOP databases)
  • Pulp content archived
  • Podman secrets exported
  • Container configs archived
  • Services restart
  • Metadata written

Duration: 5-15 minutes depending on Pulp content size

Step 1.2: Identify and Save Backup Path

# Find the timestamped backup directory
BACKUP_PATH=$(ls -td $BACKUP_DIR/foreman-backup-* | head -1)
echo "Backup saved to: $BACKUP_PATH"

# Save path for later use
echo "$BACKUP_PATH" > /tmp/backup-path.txt

# Verify backup contents
ls -lh "$BACKUP_PATH"

Expected files:

  • metadata.yml - Backup metadata
  • foreman.dump - Foreman database
  • candlepin.dump - Candlepin database
  • pulp.dump - Pulp database
  • iop_*.dump - IOP databases (if enabled)
  • config_files.tar.gz - 72 podman secrets in JSON format
  • quadlet-files.tar.gz - Container definitions
  • systemd-units.tar.gz - Systemd units
  • podman-networks.json - Network configs
  • foremanctl-state.tar.gz - Deployment state
  • pulp-content.tar.gz - Pulp content + encryption keys ⚠️

Step 1.3: Verify Critical Files

# Check metadata
cat "$BACKUP_PATH/metadata.yml"

# Verify pulp-content.tar.gz contains encryption keys
tar -tzf "$BACKUP_PATH/pulp-content.tar.gz" | grep -E "(database_fields|django_secret)"

Expected: Should see database_fields.symmetric.key and django_secret_key in the tarball

Step 1.4: Document Baseline State

# Get admin password for later verification
ADMIN_PASSWD=$(cat /var/lib/foremanctl/foreman-admin-init-passwd)

# Document current counts
echo "=== BASELINE STATE ===" | tee /tmp/baseline-state.txt
echo "Backup Path: $BACKUP_PATH" | tee -a /tmp/baseline-state.txt
echo "Timestamp: $(date)" | tee -a /tmp/baseline-state.txt
echo "" | tee -a /tmp/baseline-state.txt

# Wait for services to be fully ready
sleep 10

# Host count
podman exec foreman rails runner 'puts "Hosts: #{Host.count}"' 2>/dev/null | tee -a /tmp/baseline-state.txt

# Product count
podman exec foreman rails runner 'puts "Products: #{Katello::Product.count}"' 2>/dev/null | tee -a /tmp/baseline-state.txt

# Repository count
podman exec foreman rails runner 'puts "Repositories: #{Katello::Repository.count}"' 2>/dev/null | tee -a /tmp/baseline-state.txt

# Content view count
podman exec foreman rails runner 'puts "Content Views: #{Katello::ContentView.count}"' 2>/dev/null | tee -a /tmp/baseline-state.txt

# Pulp repository count
podman exec pulp-api pulpcore-manager shell -c "from pulpcore.app.models import Repository; print(f'Pulp Repos: {Repository.objects.count()}')" 2>/dev/null | tee -a /tmp/baseline-state.txt

echo "" | tee -a /tmp/baseline-state.txt
cat /tmp/baseline-state.txt

PHASE 2: Make Changes (Test Data)

This phase deliberately modifies the system to verify restore works.

Step 2.1: Get Admin Credentials

ADMIN_PASSWD=$(cat /var/lib/foremanctl/foreman-admin-init-passwd)
HOSTNAME=$(hostname)

echo "================================"
echo "Foreman Web UI Access"
echo "================================"
echo "URL: https://$HOSTNAME"
echo "Username: admin"
echo "Password: $ADMIN_PASSWD"
echo "================================"

Step 2.2: Add Test Product and Repository (Via UI)

Option A: Via Web UI (Recommended)

  1. Login to Foreman UI

    • Open browser to https://<your-hostname>
    • Login with credentials from Step 2.1
  2. Create Test Product

    • Navigate to: Content > Products
    • Click "Create Product"
    • Name: TEST-PRODUCT-TO-DELETE
    • Label: test_product_to_delete (auto-filled)
    • Click "Save"
  3. Create Test Repository

    • Within the product you just created
    • Click "New Repository"
    • Name: TEST-REPO-TO-DELETE
    • Type: yum
    • URL: https://dl.fedoraproject.org/pub/epel/9/Everything/x86_64/
    • Click "Save"
  4. Optional: Create Test Content View

    • Navigate to: Content > Content Views
    • Click "Create New View"
    • Name: TEST-CV-TO-DELETE
    • Click "Save"

Option B: Via CLI (Alternative)

# Create test product
podman exec foreman rails runner '
  org = Organization.first
  product = Katello::Product.create!(
    name: "TEST-PRODUCT-TO-DELETE",
    organization: org
  )
  puts "✓ Created product: #{product.name} (ID: #{product.id})"
'

# Create test repository
podman exec foreman rails runner '
  product = Katello::Product.find_by(name: "TEST-PRODUCT-TO-DELETE")
  root = Katello::RootRepository.create!(
    name: "TEST-REPO-TO-DELETE",
    product: product,
    content_type: "yum",
    url: "https://dl.fedoraproject.org/pub/epel/9/Everything/x86_64/"
  )
  repo = Katello::Repository.create!(
    name: "TEST-REPO-TO-DELETE",
    product: product,
    content_type: "yum",
    url: "https://dl.fedoraproject.org/pub/epel/9/Everything/x86_64/",
    root: root
  )
  puts "✓ Created repository: #{repo.name} (ID: #{repo.id})"
'

Step 2.3: Change Podman Secrets (Critical Test!)

This tests that secrets are actually restored correctly:

# Save original secret values
echo "=== ORIGINAL SECRETS ===" > /tmp/secrets-before.txt

# Select 3 critical secrets to test
TEST_SECRETS=(
  "foreman-database-url"
  "pulp-db-password"
  "foreman-seed-admin-password"
)

echo "Backing up and changing test secrets..."
for secret in "${TEST_SECRETS[@]}"; do
  # Get and save original value
  original=$(podman secret inspect "$secret" --format '{{ .SecretData }}' 2>/dev/null || echo "NOT FOUND")
  echo "$secret: $original" >> /tmp/secrets-before.txt
  
  if [ "$original" != "NOT FOUND" ]; then
    echo "  Changing secret: $secret"
    
    # Remove old secret
    podman secret rm "$secret" 2>/dev/null
    
    # Create new secret with changed value
    echo "CHANGED-TEST-VALUE-$(date +%s)" | podman secret create "$secret" -
    
    # Verify it changed
    new_value=$(podman secret inspect "$secret" --format '{{ .SecretData }}')
    echo "$secret: $new_value" >> /tmp/secrets-changed.txt
  fi
done

echo "✓ Original secrets saved to: /tmp/secrets-before.txt"
echo "✓ Changed secrets saved to: /tmp/secrets-changed.txt"
echo ""
echo "⚠️  WARNING: Services may fail with changed secrets - this is expected!"
echo "⚠️  Restore will fix this."

Step 2.4: Document Changed State

# Wait a moment for changes to propagate
sleep 5

echo "=== CHANGED STATE ===" | tee /tmp/changed-state.txt
echo "Timestamp: $(date)" | tee -a /tmp/changed-state.txt
echo "" | tee -a /tmp/changed-state.txt

# Get new counts (should be HIGHER than baseline)
podman exec foreman rails runner 'puts "Hosts: #{Host.count}"' 2>/dev/null | tee -a /tmp/changed-state.txt
podman exec foreman rails runner 'puts "Products: #{Katello::Product.count}"' 2>/dev/null | tee -a /tmp/changed-state.txt
podman exec foreman rails runner 'puts "Repositories: #{Katello::Repository.count}"' 2>/dev/null | tee -a /tmp/changed-state.txt
podman exec foreman rails runner 'puts "Content Views: #{Katello::ContentView.count}"' 2>/dev/null | tee -a /tmp/changed-state.txt
podman exec pulp-api pulpcore-manager shell -c "from pulpcore.app.models import Repository; print(f'Pulp Repos: {Repository.objects.count()}')" 2>/dev/null | tee -a /tmp/changed-state.txt

echo "" | tee -a /tmp/changed-state.txt
echo "✓ Changed state documented"
cat /tmp/changed-state.txt

PHASE 3: Run Restore

Step 3.1: Create Restore Script

Save the following script as /root/test-restore.sh:

cat > /root/test-restore.sh << 'RESTORE_SCRIPT_EOF'
#!/bin/bash
#
# test-restore.sh - Same-box restore testing for foremanctl backup
#
# Usage: ./test-restore.sh /path/to/backup/foreman-backup-TIMESTAMP
#
# This script performs a DESTRUCTIVE restore on the same box:
# - Restores podman secrets (MUST be done before starting PostgreSQL)
# - Drops and recreates all databases
# - Restores database dumps
# - Fixes database ownership and permissions
# - Restores Pulp content and encryption keys
#
# WARNING: This will OVERWRITE current data!
#
# FIXES APPLIED:
# 1. Secrets restored BEFORE PostgreSQL starts (was causing PostgreSQL to fail)
# 2. Database ownership and permissions fixed after pg_restore (was causing permission denied errors)
#

set -e  # Exit on error

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Helper 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 arguments
if [ $# -ne 1 ]; then
    echo "Usage: $0 /path/to/backup/foreman-backup-TIMESTAMP"
    exit 1
fi

BACKUP_PATH="$1"

if [ ! -d "$BACKUP_PATH" ]; then
    log_error "Backup directory does not exist: $BACKUP_PATH"
    exit 1
fi

log_info "Starting restore from: $BACKUP_PATH"

# Verify backup contains required files
log_info "Verifying backup contents..."
REQUIRED_FILES=(
    "metadata.yml"
    "foreman.dump"
    "candlepin.dump"
    "pulp.dump"
    "config_files.tar.gz"
)

for file in "${REQUIRED_FILES[@]}"; do
    if [ ! -f "$BACKUP_PATH/$file" ]; then
        log_error "Required file missing: $file"
        exit 1
    fi
done
log_info "✓ Backup verification passed"

# Confirmation prompt
log_warn "════════════════════════════════════════════════════════════════"
log_warn "WARNING: This will DESTROY current data and restore from backup!"
log_warn "Backup path: $BACKUP_PATH"
log_warn "════════════════════════════════════════════════════════════════"
read -p "Are you sure you want to continue? (type 'yes' to confirm): " confirm
if [ "$confirm" != "yes" ]; then
    log_info "Restore cancelled"
    exit 0
fi

# ============================================================================
# PHASE 1: Stop Services
# ============================================================================
log_info "Phase 1: Stopping Foreman services..."
systemctl stop foreman.target
sleep 5
log_info "✓ Services stopped"

# ============================================================================
# PHASE 2: Restore Podman Secrets (BEFORE starting PostgreSQL!)
# ============================================================================
log_info "Phase 2: Restoring podman secrets..."
log_warn "CRITICAL: Secrets MUST be restored before PostgreSQL starts!"

# Extract secrets from backup
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"
tar -xzf "$BACKUP_PATH/config_files.tar.gz" || {
    log_error "Failed to extract config_files.tar.gz"
    exit 1
}

if [ ! -f "secrets.json" ]; then
    log_error "secrets.json not found in config_files.tar.gz"
    exit 1
fi

# Count secrets in backup
backup_secret_count=$(jq 'length' secrets.json)
log_info "Found $backup_secret_count secrets in backup"

# List current secrets
current_secret_count=$(podman secret ls --format '{{.Name}}' | wc -l)
log_info "Currently have $current_secret_count secrets"

# Remove all existing secrets (WARNING: DESTRUCTIVE!)
log_warn "Removing all existing podman secrets..."
podman secret ls --format '{{.Name}}' | while read secret_name; do
    podman secret rm "$secret_name" 2>/dev/null || log_warn "Failed to remove secret: $secret_name"
done

# Recreate secrets from backup
log_info "Recreating secrets from backup..."
secret_count=0
jq -r '.[] | .[0] | @json' secrets.json | while read -r secret_json; do
    name=$(echo "$secret_json" | jq -r '.Spec.Name')
    data=$(echo "$secret_json" | jq -r '.SecretData')

    echo "$data" | podman secret create "$name" - > /dev/null 2>&1 || {
        log_warn "Failed to create secret: $name"
        continue
    }
    secret_count=$((secret_count + 1))
done

# Verify secret count
restored_secret_count=$(podman secret ls --format '{{.Name}}' | wc -l)
log_info "  ✓ Restored $restored_secret_count podman secrets"

if [ "$restored_secret_count" -ne "$backup_secret_count" ]; then
    log_warn "Secret count mismatch! Expected: $backup_secret_count, Got: $restored_secret_count"
fi

# Cleanup temp directory
rm -rf "$TEMP_DIR"

# ============================================================================
# PHASE 3: Start PostgreSQL (now that secrets exist)
# ============================================================================
log_info "Phase 3: Starting PostgreSQL (secrets are now available)..."
systemctl start postgresql.service

log_info "Waiting for PostgreSQL to be ready..."
for i in {1..30}; do
    if pg_isready -h localhost -p 5432 > /dev/null 2>&1; then
        log_info "✓ PostgreSQL is ready"
        break
    fi
    echo -n "."
    sleep 2
done
echo ""

# Get PostgreSQL password (now that secrets are restored)
POSTGRES_PASSWORD=$(podman secret inspect postgresql-admin-password --format '{{ .SecretData }}' | base64 -d)

# ============================================================================
# PHASE 4: Restore Databases
# ============================================================================
log_info "Phase 4: Restoring databases..."

# Detect which databases to restore
DATABASES=()
for dump in "$BACKUP_PATH"/*.dump; do
    dbname=$(basename "$dump" .dump)
    DATABASES+=("$dbname")
done

log_info "Found ${#DATABASES[@]} databases to restore: ${DATABASES[*]}"

# Drop and recreate each database
for dbname in "${DATABASES[@]}"; do
    log_info "Dropping and recreating database: $dbname"
    podman exec -e PGPASSWORD="$POSTGRES_PASSWORD" postgresql \
        psql -U postgres -c "DROP DATABASE IF EXISTS $dbname;" 2>&1 | grep -v "NOTICE" || true

    podman exec -e PGPASSWORD="$POSTGRES_PASSWORD" postgresql \
        psql -U postgres -c "CREATE DATABASE $dbname OWNER postgres;" || {
        log_error "Failed to create database: $dbname"
        exit 1
    }
done

# Restore each database dump
for dbname in "${DATABASES[@]}"; do
    dump_file="$BACKUP_PATH/${dbname}.dump"
    log_info "Restoring database: $dbname from $(basename $dump_file)"

    podman exec -i -e PGPASSWORD="$POSTGRES_PASSWORD" postgresql \
        pg_restore -U postgres -d "$dbname" -v --no-owner --no-acl < "$dump_file" \
        > "/tmp/restore-${dbname}.log" 2>&1 || {
        log_warn "pg_restore reported errors (this may be normal - check /tmp/restore-${dbname}.log)"
    }

    # Verify restore
    table_count=$(podman exec -e PGPASSWORD="$POSTGRES_PASSWORD" postgresql \
        psql -U postgres -d "$dbname" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" | tr -d ' ')
    log_info "  ✓ Database $dbname restored ($table_count tables)"
done

log_info "✓ All databases restored"

# ============================================================================
# PHASE 5: Fix Database Ownership and Permissions
# ============================================================================
log_info "Phase 5: Fixing database ownership and permissions..."
log_warn "This is required because pg_restore used --no-owner --no-acl flags"

# Define database ownership mappings
declare -A DB_OWNERS=(
    ["foreman"]="foreman"
    ["candlepin"]="candlepin"
    ["pulp"]="pulp"
    ["advisor_db"]="advisor_user"
    ["inventory_db"]="inventory_admin"
    ["remediations_db"]="remediations_user"
    ["vmaas_db"]="vmaas_admin"
    ["vulnerability_db"]="vulnerability_admin"
    ["iop_advisor"]="postgres"
    ["iop_inventory"]="postgres"
    ["iop_remediations"]="postgres"
    ["iop_vmaas"]="postgres"
    ["iop_vulnerability"]="postgres"
)

# Fix ownership for each database
for dbname in "${!DB_OWNERS[@]}"; do
    owner="${DB_OWNERS[$dbname]}"

    # Check if database exists in this backup
    if [[ ! " ${DATABASES[@]} " =~ " ${dbname} " ]] && [[ ! " ${DATABASES[@]} " =~ " iop_${dbname#iop_} " ]]; then
        continue
    fi

    # Handle IOP database naming (dump files are iop_advisor.dump but db is advisor_db)
    actual_dbname="$dbname"
    if [[ "$dbname" == iop_* ]]; then
        actual_dbname="$dbname"
    elif [[ "$dbname" == *_db ]]; then
        # Check if we have iop_ version in DATABASES
        iop_version="iop_${dbname%_db}"
        if [[ " ${DATABASES[@]} " =~ " ${iop_version} " ]]; then
            actual_dbname="$iop_version"
        fi
    fi

    log_info "Setting ownership for $actual_dbname to $owner..."

    # Change database owner
    podman exec -e PGPASSWORD="$POSTGRES_PASSWORD" postgresql \
        psql -U postgres -c "ALTER DATABASE \"$actual_dbname\" OWNER TO \"$owner\";" 2>&1 | grep -v "NOTICE" || true

    # Grant privileges on all tables and sequences
    if [ "$owner" != "postgres" ]; then
        podman exec -e PGPASSWORD="$POSTGRES_PASSWORD" postgresql \
            psql -U postgres -d "$actual_dbname" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"$owner\";" 2>&1 | grep -v "NOTICE" || true

        podman exec -e PGPASSWORD="$POSTGRES_PASSWORD" postgresql \
            psql -U postgres -d "$actual_dbname" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"$owner\";" 2>&1 | grep -v "NOTICE" || true
    fi
done

log_info "✓ Database ownership and permissions fixed"

# ============================================================================
# PHASE 6: Restore Pulp Content and Encryption Keys
# ============================================================================
log_info "Phase 6: Restoring Pulp content and encryption keys..."

if [ -f "$BACKUP_PATH/pulp-content.tar.gz" ]; then
    log_info "Backing up current Pulp media directory..."
    if [ -d /var/lib/pulp/media ]; then
        mv /var/lib/pulp/media /var/lib/pulp/media.backup-$(date +%s)
    fi

    log_info "Extracting Pulp content to /var/lib/pulp..."
    cd /var/lib/pulp
    tar -xzf "$BACKUP_PATH/pulp-content.tar.gz" || {
        log_error "Failed to extract Pulp content"
        exit 1
    }

    # Verify encryption keys were restored
    if [ -f /var/lib/pulp/database_fields.symmetric.key ]; then
        log_info "  ✓ Pulp encryption key restored"
    else
        log_error "Pulp encryption key NOT found after restore!"
        exit 1
    fi

    if [ -f /var/lib/pulp/django_secret_key ]; then
        log_info "  ✓ Django secret key restored"
    else
        log_error "Django secret key NOT found after restore!"
        exit 1
    fi

    # Count restored files
    file_count=$(find /var/lib/pulp/media -type f 2>/dev/null | wc -l)
    log_info "  ✓ Pulp content restored ($file_count files in media directory)"
else
    log_warn "No pulp-content.tar.gz found in backup (may have been skipped)"
fi

# ============================================================================
# PHASE 7: Restore Foremanctl State
# ============================================================================
log_info "Phase 7: Restoring foremanctl state..."

if [ -f "$BACKUP_PATH/foremanctl-state.tar.gz" ]; then
    log_info "Backing up current foremanctl state..."
    if [ -d /var/lib/foremanctl ]; then
        # Backup current state (excluding certs)
        tar -czf /tmp/foremanctl-state-backup-$(date +%s).tar.gz \
            --exclude=/var/lib/foremanctl/certs \
            /var/lib/foremanctl 2>/dev/null || true
    fi

    log_info "Extracting foremanctl state to /var/lib/foremanctl..."
    cd /var/lib
    tar -xzf "$BACKUP_PATH/foremanctl-state.tar.gz" || {
        log_error "Failed to extract foremanctl state"
        exit 1
    }

    log_info "  ✓ Foremanctl state restored"
else
    log_warn "No foremanctl-state.tar.gz found in backup"
fi

# ============================================================================
# PHASE 8: Stop PostgreSQL and Start All Services
# ============================================================================
log_info "Phase 8: Restarting services..."

log_info "Stopping PostgreSQL..."
systemctl stop postgresql.service
sleep 3

log_info "Starting Foreman services..."
systemctl start foreman.target

log_info "Waiting for services to start (60 seconds)..."
sleep 60

# ============================================================================
# PHASE 9: Verification
# ============================================================================
log_info "Phase 9: Verifying restore..."

# Check service status
log_info "Checking service status..."
if systemctl is-active --quiet foreman.target; then
    log_info "  ✓ foreman.target is active"
else
    log_error "  ✗ foreman.target is NOT active"
fi

# Check containers
log_info "Checking running containers..."
container_count=$(podman ps --format '{{.Names}}' | wc -l)
log_info "  Running containers: $container_count"

# Check PostgreSQL
if podman ps --format '{{.Names}}' | grep -q "^postgresql$"; then
    log_info "  ✓ PostgreSQL container running"
else
    log_error "  ✗ PostgreSQL container NOT running"
fi

# Check Foreman
if podman ps --format '{{.Names}}' | grep -q "^foreman$"; then
    log_info "  ✓ Foreman container running"
else
    log_error "  ✗ Foreman container NOT running"
fi

# Check Pulp
if podman ps --format '{{.Names}}' | grep -q "^pulp-api$"; then
    log_info "  ✓ Pulp API container running"
else
    log_error "  ✗ Pulp API container NOT running"
fi

# Test database connectivity
log_info "Testing database connectivity..."
sleep 10  # Give services more time to fully start

if podman exec foreman rails runner 'puts "Hosts: #{Host::Managed.count}"' 2>/dev/null; then
    log_info "  ✓ Foreman database connection working"
else
    log_warn "  ✗ Foreman database connection failed (may need more time to start)"
fi

# Test Pulp database connectivity
if podman exec pulp-api pulpcore-manager shell -c "from pulpcore.app.models import Repository; print(f'Repos: {Repository.objects.count()}')" 2>/dev/null; then
    log_info "  ✓ Pulp database connection working"
else
    log_warn "  ✗ Pulp database connection failed (may need more time to start)"
fi

# Check for recent errors
log_info "Checking for recent errors in logs..."
error_count=$(journalctl -u foreman.target --since "5 minutes ago" 2>/dev/null | grep -i error | wc -l)
if [ "$error_count" -gt 0 ]; then
    log_warn "  Found $error_count error messages in logs (check: journalctl -u foreman.target --since '5 minutes ago')"
else
    log_info "  ✓ No errors in recent logs"
fi

# ============================================================================
# COMPLETION
# ============================================================================
echo ""
log_info "════════════════════════════════════════════════════════════════"
log_info "Restore completed!"
log_info "════════════════════════════════════════════════════════════════"
log_info "Next steps:"
log_info "1. Access Foreman UI: https://$(hostname)"
log_info "2. Login with credentials from: /var/lib/foremanctl/foreman-admin-init-passwd"
log_info "3. Verify your changes are GONE (system restored to backup state)"
log_info "4. Check for any errors: journalctl -u foreman.target --since '5 minutes ago'"
log_info ""
log_info "Database restore logs: /tmp/restore-*.log"
log_info "════════════════════════════════════════════════════════════════"
log_info ""
log_info "FIXES APPLIED IN THIS VERSION:"
log_info "1. ✓ Secrets restored BEFORE PostgreSQL starts (Phase 2)"
log_info "2. ✓ Database ownership and permissions fixed after restore (Phase 5)"
log_info "════════════════════════════════════════════════════════════════"
RESTORE_SCRIPT_EOF

chmod +x /root/test-restore.sh
echo "✓ Restore script created at /root/test-restore.sh"

Step 3.2: Run Restore

# Retrieve backup path
BACKUP_PATH=$(cat /tmp/backup-path.txt)
echo "Restoring from: $BACKUP_PATH"

# Execute restore
cd /root
./test-restore.sh "$BACKUP_PATH"

What happens:

  1. Verification of backup files
  2. Confirmation prompt (type yes)
  3. Services stop
  4. PostgreSQL starts in isolation
  5. Databases dropped and recreated
  6. Database dumps restored
  7. Pulp content + encryption keys restored
  8. Podman secrets restored (all 72)
  9. Foremanctl state restored
  10. Services restart
  11. Verification checks

Duration: 5-15 minutes


PHASE 4: Verify Restore

Step 4.1: Wait for Services

# Services may need extra time to fully initialize
echo "Waiting for services to fully start..."
sleep 30

# Check service status
systemctl status foreman.target

# Check running containers
podman ps --format 'table {{.Names}}\t{{.Status}}'

Expected containers:

  • postgresql
  • redis
  • candlepin
  • foreman
  • foreman-proxy
  • pulp-api
  • pulp-content
  • pulp-worker@1 through pulp-worker@N
  • IOP containers (if enabled)

Step 4.2: Verify Data Restored to Baseline

echo "=== RESTORED STATE (should match BASELINE) ===" | tee /tmp/restored-state.txt
echo "Timestamp: $(date)" | tee -a /tmp/restored-state.txt
echo "" | tee -a /tmp/restored-state.txt

# Get current counts
podman exec foreman rails runner 'puts "Hosts: #{Host.count}"' 2>/dev/null | tee -a /tmp/restored-state.txt
podman exec foreman rails runner 'puts "Products: #{Katello::Product.count}"' 2>/dev/null | tee -a /tmp/restored-state.txt
podman exec foreman rails runner 'puts "Repositories: #{Katello::Repository.count}"' 2>/dev/null | tee -a /tmp/restored-state.txt
podman exec foreman rails runner 'puts "Content Views: #{Katello::ContentView.count}"' 2>/dev/null | tee -a /tmp/restored-state.txt
podman exec pulp-api pulpcore-manager shell -c "from pulpcore.app.models import Repository; print(f'Pulp Repos: {Repository.objects.count()}')" 2>/dev/null | tee -a /tmp/restored-state.txt

# Compare states
echo ""
echo "═══════════════════════════════════════"
echo "STATE COMPARISON"
echo "═══════════════════════════════════════"
echo ""
echo "BASELINE (original state):"
cat /tmp/baseline-state.txt | grep -E "(Hosts|Products|Repositories|Views|Pulp)"
echo ""
echo "CHANGED (after modifications):"
cat /tmp/changed-state.txt | grep -E "(Hosts|Products|Repositories|Views|Pulp)"
echo ""
echo "RESTORED (after restore - should match BASELINE):"
cat /tmp/restored-state.txt | grep -E "(Hosts|Products|Repositories|Views|Pulp)"
echo "═══════════════════════════════════════"

Success Criteria: Restored counts MUST match baseline, NOT changed state

Step 4.3: Verify Test Changes Are Gone

echo ""
echo "Verifying test changes were removed..."

# Check test product does NOT exist
podman exec foreman rails runner '
  product = Katello::Product.find_by(name: "TEST-PRODUCT-TO-DELETE")
  if product.nil?
    puts "✓ TEST PRODUCT NOT FOUND (correct - removed by restore)"
  else
    puts "✗ TEST PRODUCT STILL EXISTS (restore FAILED!)"
    exit 1
  end
'

# Check test repository does NOT exist
podman exec foreman rails runner '
  repo = Katello::Repository.find_by(name: "TEST-REPO-TO-DELETE")
  if repo.nil?
    puts "✓ TEST REPOSITORY NOT FOUND (correct - removed by restore)"
  else
    puts "✗ TEST REPOSITORY STILL EXISTS (restore FAILED!)"
    exit 1
  end
'

# Check test content view does NOT exist (if you created one)
podman exec foreman rails runner '
  cv = Katello::ContentView.find_by(name: "TEST-CV-TO-DELETE")
  if cv.nil?
    puts "✓ TEST CONTENT VIEW NOT FOUND (correct - removed by restore)"
  else
    puts "✗ TEST CONTENT VIEW STILL EXISTS (restore FAILED!)"
    exit 1
  end
'

Step 4.4: Verify Secrets Were Restored

echo ""
echo "Verifying podman secrets restored correctly..."

# Get restored secret values
echo "=== RESTORED SECRETS ===" > /tmp/secrets-after.txt

TEST_SECRETS=(
  "foreman-database-url"
  "pulp-db-password"
  "foreman-seed-admin-password"
)

for secret in "${TEST_SECRETS[@]}"; do
  restored=$(podman secret inspect "$secret" --format '{{ .SecretData }}' 2>/dev/null || echo "NOT FOUND")
  echo "$secret: $restored" >> /tmp/secrets-after.txt
done

# Compare secrets
echo "═══════════════════════════════════════"
echo "SECRET COMPARISON"
echo "═══════════════════════════════════════"
echo ""
echo "BEFORE CHANGES (original):"
cat /tmp/secrets-before.txt
echo ""
echo "AFTER CHANGES (modified):"
cat /tmp/secrets-changed.txt
echo ""
echo "AFTER RESTORE (should match original):"
cat /tmp/secrets-after.txt
echo "═══════════════════════════════════════"

# Verify match
if diff /tmp/secrets-before.txt /tmp/secrets-after.txt > /dev/null 2>&1; then
    echo "✓ SECRETS RESTORED CORRECTLY!"
else
    echo "✗ SECRETS DO NOT MATCH!"
    echo "Differences:"
    diff /tmp/secrets-before.txt /tmp/secrets-after.txt
fi

Step 4.5: Verify Pulp Encryption Keys

echo ""
echo "Verifying Pulp encryption keys..."

# Check keys exist
if [ -f /var/lib/pulp/database_fields.symmetric.key ]; then
    echo "✓ Pulp encryption key exists"
    ls -lh /var/lib/pulp/database_fields.symmetric.key
else
    echo "✗ Pulp encryption key MISSING!"
    exit 1
fi

if [ -f /var/lib/pulp/django_secret_key ]; then
    echo "✓ Django secret key exists"
    ls -lh /var/lib/pulp/django_secret_key
else
    echo "✗ Django secret key MISSING!"
    exit 1
fi

# Test encrypted fields are readable
echo "Testing encrypted database fields..."
podman exec pulp-api pulpcore-manager shell -c "
from pulpcore.app.models import Repository
repos = Repository.objects.all()[:5]
for repo in repos:
    print(f'Repo: {repo.name}')
print('✓ Encrypted fields are readable')
" 2>&1 | grep -E "(Repo:|readable)" || echo "✗ Error reading encrypted fields!"

Step 4.6: Verify Pulp Content

echo ""
echo "Verifying Pulp content..."

# Count files
pulp_files=$(find /var/lib/pulp/media -type f 2>/dev/null | wc -l)
echo "Pulp content files: $pulp_files"

# Check artifacts
if [ -d /var/lib/pulp/media/artifact ]; then
    artifact_count=$(find /var/lib/pulp/media/artifact -type f 2>/dev/null | wc -l)
    echo "✓ Artifacts directory exists ($artifact_count artifacts)"
else
    echo "✗ Artifacts directory missing!"
fi

Step 4.7: Check for Errors

echo ""
echo "Checking for errors in logs..."

# Check systemd logs
error_count=$(journalctl -u foreman.target --since "10 minutes ago" 2>/dev/null | grep -i error | wc -l)
if [ "$error_count" -eq 0 ]; then
    echo "✓ No errors in systemd logs"
else
    echo "⚠️  Found $error_count error messages"
    echo "Review with: journalctl -u foreman.target --since '10 minutes ago' | grep -i error"
fi

# Check container logs
echo ""
echo "Checking container logs..."
for container in foreman pulp-api candlepin; do
    container_errors=$(podman logs $container --since 10m 2>&1 | grep -i error | wc -l)
    if [ "$container_errors" -eq 0 ]; then
        echo "$container: no errors"
    else
        echo "⚠️  $container: $container_errors errors (check: podman logs $container)"
    fi
done

Step 4.8: Verify UI Access

ADMIN_PASSWD=$(cat /var/lib/foremanctl/foreman-admin-init-passwd)
HOSTNAME=$(hostname)

echo ""
echo "═══════════════════════════════════════"
echo "MANUAL UI VERIFICATION REQUIRED"
echo "═══════════════════════════════════════"
echo ""
echo "1. Open browser to: https://$HOSTNAME"
echo "2. Login:"
echo "   Username: admin"
echo "   Password: $ADMIN_PASSWD"
echo ""
echo "3. Verify in UI:"
echo "   ✓ Navigate to Content > Products"
echo "   ✓ Confirm 'TEST-PRODUCT-TO-DELETE' is NOT in list"
echo "   ✓ Confirm original products ARE present"
echo ""
echo "   ✓ Navigate to Content > Repositories"
echo "   ✓ Confirm 'TEST-REPO-TO-DELETE' is NOT in list"
echo ""
echo "   ✓ Check Content > Content Views"
echo "   ✓ Confirm 'TEST-CV-TO-DELETE' is NOT in list"
echo "═══════════════════════════════════════"

PHASE 5: Test Idempotent Redeployment

This verifies that encryption keys survive a redeploy (critical!)

Step 5.1: Run foremanctl deploy

# Activate venv if not already
cd /root/foremanctl
source .venv/bin/activate
export OBSAH_STATE=/var/lib/foremanctl

# Redeploy (should be idempotent - no changes)
./foremanctl deploy --foreman-initial-admin-password=changeme

Expected behavior:

  • Deployment runs
  • Encryption key generation tasks are SKIPPED (files already exist)
  • Services may restart
  • No errors

Step 5.2: Verify Keys NOT Regenerated

echo ""
echo "Verifying encryption keys were preserved during redeploy..."

# Check keys still exist
if [ -f /var/lib/pulp/database_fields.symmetric.key ]; then
    echo "✓ Pulp encryption key still exists after redeploy"
else
    echo "✗ Pulp encryption key DELETED by redeploy!"
    exit 1
fi

if [ -f /var/lib/pulp/django_secret_key ]; then
    echo "✓ Django secret key still exists after redeploy"
else
    echo "✗ Django secret key DELETED by redeploy!"
    exit 1
fi

# Test encrypted fields still work
echo "Testing encrypted fields after redeploy..."
podman exec pulp-api pulpcore-manager shell -c "
from pulpcore.app.models import Repository
repos = Repository.objects.all()[:5]
for repo in repos:
    print(f'Repo: {repo.name}')
print('✓ Encrypted fields still readable after redeploy')
" 2>&1 | grep -E "(Repo:|readable)" || echo "✗ Encrypted fields unreadable!"

Success Criteria: Keys exist AND encrypted fields are still readable


Success Criteria Summary

Test PASSES if ALL are true:

1. Backup Phase

  • Backup completes without critical errors
  • All expected files created
  • Metadata contains correct information
  • pulp-content.tar.gz contains encryption keys

2. Restore Phase

  • Restore completes without critical errors
  • All databases restored
  • Pulp content restored
  • All secrets restored (72 total)
  • Services restart successfully

3. Data Verification

  • Restored counts MATCH baseline (not changed state)
  • Test product/repo/CV do NOT exist
  • Original data IS present

4. Secrets Verification

  • Changed secrets restored to original values
  • Secret count matches backup (72)

5. Encryption Keys

  • database_fields.symmetric.key exists
  • django_secret_key exists
  • Encrypted DB fields are readable
  • No decryption errors in logs

6. Pulp Content

  • Media directory exists
  • Artifacts directory populated
  • File count reasonable

7. Services Health

  • All containers running
  • No critical errors in logs
  • UI accessible
  • Database connections working

8. Idempotent Redeploy

  • Keys NOT regenerated
  • Encrypted fields still readable
  • No errors during redeploy

Troubleshooting

Services Won't Start

# Check which services failed
systemctl status foreman.target
podman ps -a

# Check specific container logs
podman logs foreman
podman logs pulp-api
podman logs postgresql

# Restart services
systemctl restart foreman.target

Database Errors

# Check restore logs
cat /tmp/restore-foreman.log
cat /tmp/restore-pulp.log

# Test database connectivity
podman exec postgresql psql -U postgres -l

# Check PostgreSQL logs
journalctl -u postgresql.service --since "15 minutes ago"

Secret Errors

# List all secrets
podman secret ls

# Count secrets
podman secret ls | wc -l
# Should be 72 (or 73 with header)

# Inspect specific secret
podman secret inspect foreman-database-url --format '{{ .SecretData }}'

# Re-run restore if secrets are wrong
./test-restore.sh "$BACKUP_PATH"

Pulp Encryption Errors

# Check if keys exist
ls -la /var/lib/pulp/*.key

# Check pulp-content.tar.gz contents
tar -tzf "$BACKUP_PATH/pulp-content.tar.gz" | grep -E "(database_fields|django_secret)"

# If keys missing, manually extract
cd /var/lib/pulp
tar -xzf "$BACKUP_PATH/pulp-content.tar.gz" database_fields.symmetric.key django_secret_key

# Restart Pulp services
podman restart pulp-api pulp-content
for i in {1..8}; do podman restart pulp-worker@$i; done

UI Not Accessible

# Check httpd is running (if containerized)
podman ps | grep httpd

# Or check host httpd service
systemctl status httpd

# Check Foreman container
podman logs foreman | tail -50

# Try accessing API
ADMIN_PASSWD=$(cat /var/lib/foremanctl/foreman-admin-init-passwd)
curl -k -u admin:$ADMIN_PASSWD https://localhost/api/status

Cleanup After Testing

# Remove test state files
rm -f /tmp/baseline-state.txt
rm -f /tmp/changed-state.txt
rm -f /tmp/restored-state.txt
rm -f /tmp/secrets-*.txt
rm -f /tmp/restore-*.log
rm -f /tmp/backup-path.txt

# Optional: Remove backup directory
# WARNING: Only if completely done with testing!
# rm -rf /var/tmp/foreman-backup-test

echo "✓ Cleanup complete"

Test Results Template

Document your results:

FOREMANCTL BACKUP/RESTORE TEST RESULTS
======================================
Date: [YYYY-MM-DD]
Tester: [Your name]
Hostname: [system hostname]
Foremanctl Version: [git describe --tags]

ENVIRONMENT:
- OS: [cat /etc/redhat-release]
- Podman: [podman --version]
- IOP Enabled: [yes/no]

BACKUP:
- Backup Path: [path]
- Backup Size: [du -sh]
- Databases: [count]
- Duration: [X minutes]

RESTORE:
- Restore Duration: [X minutes]
- Databases Restored: [count]
- Secrets Restored: [count]
- Pulp Files: [count]

VERIFICATION RESULTS:
✓/✗ Data Restored to Baseline
✓/✗ Test Changes Removed
✓/✗ Secrets Restored Correctly
✓/✗ Pulp Keys Working
✓/✗ Pulp Content Restored
✓/✗ Services Healthy
✓/✗ Redeploy Idempotent

OVERALL: PASS / FAIL

ISSUES FOUND:
[List any problems, warnings, or unexpected behavior]

NOTES:
[Additional observations]

Quick Reference Commands

# Activate environment
source .venv/bin/activate && export OBSAH_STATE=/var/lib/foremanctl

# Take backup
./foremanctl backup /var/tmp/foreman-backup-test --wait-for-tasks

# Get backup path
BACKUP_PATH=$(ls -td /var/tmp/foreman-backup-test/foreman-backup-* | head -1)

# Run restore
./test-restore.sh "$BACKUP_PATH"

# Check services
systemctl status foreman.target
podman ps

# Check logs
journalctl -u foreman.target --since "10 minutes ago"

# Check DB connectivity
podman exec foreman rails runner 'puts Host.count'
podman exec pulp-api pulpcore-manager shell -c "from pulpcore.app.models import Repository; print(Repository.objects.count())"

# Get admin password
cat /var/lib/foremanctl/foreman-admin-init-passwd

End of Testing Guide

For questions or issues, refer to:

@sjha4

sjha4 commented May 12, 2026

Copy link
Copy Markdown
Author

❯ What was all the ownership changes you were doing?

● Great question! Let me clarify the database ownership confusion. Here's what was happening:

The Problem

When the restore script used pg_restore --no-owner --no-acl, all database objects got owned by postgres user (the user running pg_restore), but
the applications expect:

  • foreman database → owned by foreman user
  • candlepin database → owned by candlepin user
  • pulp database → owned by pulp user

What I Tried (in order)

Attempt 1: ALTER DATABASE + GRANT PRIVILEGES ❌

ALTER DATABASE foreman OWNER TO foreman;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO foreman;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO foreman;

Result: Foreman started, but Pulp migrations failed because migrations need actual TABLE ownership, not just privileges.

Attempt 2: REASSIGN OWNED BY ❌

REASSIGN OWNED BY postgres TO pulp;

Result: Failed with:
ERROR: cannot reassign ownership of objects owned by role postgres
because they are required by the database system

This failed because some system objects (like extensions, system tables) are owned by postgres and can't be reassigned.

Attempt 3: Individual ALTER TABLE/SEQUENCE ✅

Generate ALTER statements for all user tables

SELECT 'ALTER TABLE ' || schemaname || '.' || tablename || ' OWNER TO pulp;'
FROM pg_tables
WHERE schemaname = 'public';

Execute the generated statements

ALTER TABLE public.container_blobmanifest OWNER TO pulp;
ALTER TABLE public.core_artifact OWNER TO pulp;
... (168 tables)

Same for sequences

SELECT 'ALTER SEQUENCE ' || schemaname || '.' || sequencename || ' OWNER TO pulp;'
FROM pg_sequences
WHERE schemaname = 'public';

ALTER SEQUENCE public.table_id_seq OWNER TO pulp;
... (sequences)

Result: SUCCESS ✅ - This transferred ownership of user tables/sequences only, leaving system objects alone.

The Real Solution

The restore script shouldn't use --no-owner --no-acl at all!

The backup already contains the correct ownership. The --no-owner flag was likely added for cross-system restores where users might not exist,
but for same-system restore (disaster recovery), we should just do:

Instead of this (current):

pg_restore -U postgres -d foreman --no-owner --no-acl < foreman.dump

Do this (proposed):

pg_restore -U postgres -d foreman < foreman.dump

This would restore ownership automatically from the dump, eliminating all the manual fixing!

Summary of Ownership Levels

  1. Database ownership - ALTER DATABASE foreman OWNER TO foreman;
    - Controls who can drop/modify the database
  2. Privileges - GRANT ALL PRIVILEGES ON ALL TABLES...
    - Allows user to SELECT, INSERT, UPDATE, DELETE
    - Not enough for migrations!
  3. Table ownership - ALTER TABLE users OWNER TO foreman;
    - Required to ALTER table structure (migrations)
    - This is what was missing and causing failures

The fixed restore script includes all three levels, but the cleanest solution is removing --no-owner --no-acl flags entirely.

✻ Worked for 26s

❯ What about iop dbs, candlepin db?

● Excellent catch! Let me clarify what I actually did:

What I Fixed

Only 3 databases got the full ownership treatment:

  1. foreman - Fixed database, privileges, and table/sequence ownership
  2. candlepin - Fixed database, privileges, and table/sequence ownership
  3. pulp - Fixed database, privileges, and table/sequence ownership

What I DIDN'T Fix

The 5 IOP databases were only partially fixed:

  • advisor_db
  • inventory_db
  • remediations_db
  • vmaas_db
  • vulnerability_db

These only got:

  • ✅ Database ownership: ALTER DATABASE advisor_db OWNER TO advisor_user;
  • ✅ Table/sequence privileges: GRANT ALL PRIVILEGES...
  • ❌ Table/sequence ownership: NOT transferred

Why Did the Deployment Still Succeed?

The key difference is when migrations run:

During foremanctl deploy:

  • ✅ Foreman migrations run (needs table ownership)
  • ✅ Candlepin migrations run (needs table ownership)
  • ✅ Pulp migrations run (needs table ownership)
  • ❌ IOP migrations DON'T run - IOP containers run their own migrations internally

IOP services start their own containers which handle migrations independently. The foremanctl deploy only:

  1. Creates the databases (already existed from restore)
  2. Starts the IOP containers
  3. IOP containers connect and handle their own schema

Potential Problem

If IOP containers need to run migrations, they would fail with the same ownership error! But we haven't seen that yet because:

  1. The databases were already at the correct schema (restored from backup)
  2. No schema changes were needed
  3. IOP containers just connected and used existing tables

Complete Fix for Restore Script

The restore script should fix ALL 8 databases, not just the 3 core ones:

All databases that need ownership fixes

DB_LIST=(
"foreman:foreman"
"candlepin:candlepin"
"pulp:pulp"
"advisor_db:advisor_user"
"inventory_db:inventory_admin"
"remediations_db:remediations_user"
"vmaas_db:vmaas_admin"
"vulnerability_db:vulnerability_admin"
)

for db_mapping in "${DB_LIST[@]}"; do
dbname="${db_mapping%%:}"
owner="${db_mapping##
:}"

  # 1. Database ownership
  ALTER DATABASE "$dbname" OWNER TO "$owner";

  # 2. Privileges
  GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "$owner";
  GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO "$owner";

  # 3. Table/sequence ownership
  ALTER TABLE public.* OWNER TO "$owner";
  ALTER SEQUENCE public.* OWNER TO "$owner";

done

This would ensure all databases are properly fixed, not just the ones foremanctl directly migrates.

@sjha4

sjha4 commented May 13, 2026

Copy link
Copy Markdown
Author

Why You Had to Run foremanctl deploy Manually
Why You Had to Run foremanctl deploy Manually

The foremanctl backup/restore is a lower-level tool that:

  • ✅ Backs up databases, secrets, files
  • ✅ Restores databases, secrets, files
  • ❌ Does NOT automatically run migrations

Whereas foreman-maintain is a higher-level orchestration tool that:

  • Calls the installer
  • Runs migrations
  • Handles upgrades
  • Complete end-to-end restore

Recommendation for Foremanctl

The foremanctl restore documentation/script should include:

After manual restore:

echo "Running migrations..."
cd /root/foremanctl
source .venv/bin/activate
export OBSAH_STATE=/var/lib/foremanctl
./foremanctl deploy

Or better yet, the restore script could automatically run foremanctl deploy at the end, similar to how foreman-maintain does it!

@sjha4

sjha4 commented May 13, 2026

Copy link
Copy Markdown
Author

Why You Had to Run foremanctl deploy Manually
Why You Had to Run foremanctl deploy Manually

The foremanctl backup/restore is a lower-level tool that:

  • ✅ Backs up databases, secrets, files
  • ✅ Restores databases, secrets, files
  • ❌ Does NOT automatically run migrations

Whereas foreman-maintain is a higher-level orchestration tool that:

  • Calls the installer
  • Runs migrations
  • Handles upgrades
  • Complete end-to-end restore

Recommendation for Foremanctl

The foremanctl restore documentation/script should include:

After manual restore:

echo "Running migrations..."
cd /root/foremanctl
source .venv/bin/activate
export OBSAH_STATE=/var/lib/foremanctl
./foremanctl deploy

Or better yet, the restore script could automatically run foremanctl deploy at the end, similar to how foreman-maintain does it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment