diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index da7c9b8..52cdc1e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -314,9 +314,11 @@ The twelve-factor **Build, Release, Run** stages apply to the application deploy #### Branch Naming -- **Format**: `{issue-number}-{short-description}` -- **Examples**: `42-add-mysql-support`, `15-fix-ssl-renewal` +- **Format**: `{issue-number}-{short-description-following-github-conventions}` +- **GitHub conventions**: Use lowercase, separate words with hyphens, descriptive but concise +- **Examples**: `42-add-mysql-support`, `15-fix-ssl-renewal`, `24-improve-ux-add-automatic-waiting-to-infra-apply-and-app-deploy-commands` - Always start with the GitHub issue number +- Follow GitHub's recommended branch naming: lowercase, hyphens for word separation, descriptive of the change #### Commit Messages diff --git a/Makefile b/Makefile index c5d46fb..c4ee0e3 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,12 @@ infra-plan: ## Plan infrastructure changes infra-apply: ## Provision infrastructure (platform setup) @echo "Provisioning infrastructure for $(ENVIRONMENT)..." @echo "⚠️ This command may prompt for your password for sudo operations" - $(SCRIPTS_DIR)/provision-infrastructure.sh $(ENVIRONMENT) apply + @if [ "$(SKIP_WAIT)" = "true" ]; then \ + echo "⚠️ SKIP_WAIT=true - Infrastructure will not wait for full readiness"; \ + else \ + echo "ℹ️ Infrastructure will wait for full readiness (use SKIP_WAIT=true to skip)"; \ + fi + SKIP_WAIT=$(SKIP_WAIT) $(SCRIPTS_DIR)/provision-infrastructure.sh $(ENVIRONMENT) apply infra-destroy: ## Destroy infrastructure @echo "Destroying infrastructure for $(ENVIRONMENT)..." @@ -116,7 +121,12 @@ infra-test-local: ## Run local-only infrastructure tests (requires virtualizatio app-deploy: ## Deploy application (Twelve-Factor Build + Release + Run stages) @echo "Deploying application for $(ENVIRONMENT)..." - $(SCRIPTS_DIR)/deploy-app.sh $(ENVIRONMENT) + @if [ "$(SKIP_WAIT)" = "true" ]; then \ + echo "⚠️ SKIP_WAIT=true - Application will not wait for service readiness"; \ + else \ + echo "ℹ️ Application will wait for service readiness (use SKIP_WAIT=true to skip)"; \ + fi + SKIP_WAIT=$(SKIP_WAIT) $(SCRIPTS_DIR)/deploy-app.sh $(ENVIRONMENT) app-redeploy: ## Redeploy application without infrastructure changes @echo "Redeploying application for $(ENVIRONMENT)..." diff --git a/docs/guides/cloud-deployment-guide.md b/docs/guides/cloud-deployment-guide.md index 6346c87..2ad2c35 100644 --- a/docs/guides/cloud-deployment-guide.md +++ b/docs/guides/cloud-deployment-guide.md @@ -66,10 +66,16 @@ make infra-config-local ```bash # Test deployment locally with KVM +# Commands wait for full readiness by default make infra-apply ENVIRONMENT=local make app-deploy ENVIRONMENT=local make app-health-check +# Advanced users: Skip waiting for faster execution +make infra-apply ENVIRONMENT=local SKIP_WAIT=true +make app-deploy ENVIRONMENT=local SKIP_WAIT=true +make app-health-check + # Access the local instance via SSH make vm-ssh @@ -247,6 +253,41 @@ should be tested in a staging environment first. ## Detailed Deployment Process +### ✅ Improved User Experience (Automatic Waiting) + +**Issue #24 - Enhanced Workflow**: The deployment commands now wait for full readiness by +default, providing a much better user experience: + +**Previous workflow problems**: + +- Commands completed before systems were actually ready +- Users had to manually wait between steps without clear indicators +- Following commands often failed if run too quickly + +**✅ Current improved workflow**: + +```bash +# Each command waits for full readiness by default +make infra-apply ENVIRONMENT=local # Waits for VM IP + cloud-init completion +make app-deploy ENVIRONMENT=local # Waits for all services to be healthy +make app-health-check # Validates everything is working +``` + +**Key improvements**: + +- ✅ **Clear progress indicators**: You see exactly what's happening during waits +- ✅ **Automatic readiness detection**: Commands complete when actually ready for next step +- ✅ **Reliable workflow**: No more timing-related failures between commands +- ✅ **Backwards compatibility**: Use `SKIP_WAIT=true` for original fast behavior + +**Advanced usage** (for CI/automation): + +```bash +# Skip waiting for faster execution (original behavior) +make infra-apply ENVIRONMENT=local SKIP_WAIT=true +make app-deploy ENVIRONMENT=local SKIP_WAIT=true +``` + ### Infrastructure Deployment The infrastructure deployment creates and configures the VM: @@ -262,6 +303,8 @@ make infra-apply ENVIRONMENT=production # 4. Sets up torrust user with SSH access # 5. Configures firewall rules # 6. Creates persistent data volume +# 7. ✅ NEW: Waits for VM IP assignment and cloud-init completion +# 8. ✅ NEW: Ensures infrastructure is ready for next step ``` ### Application Deployment @@ -284,6 +327,8 @@ make app-deploy ENVIRONMENT=production # - Grafana dashboards # 5. Configures automated maintenance tasks # 6. Validates all service health +# 7. ✅ NEW: Waits for all services to be healthy and ready +# 8. ✅ NEW: Ensures deployment is complete before finishing ``` ### Health Validation diff --git a/infrastructure/scripts/deploy-app.sh b/infrastructure/scripts/deploy-app.sh index b30fe49..fda2e14 100755 --- a/infrastructure/scripts/deploy-app.sh +++ b/infrastructure/scripts/deploy-app.sh @@ -14,6 +14,7 @@ TERRAFORM_DIR="${PROJECT_ROOT}/infrastructure/terraform" ENVIRONMENT="${1:-local}" VM_IP="${2:-}" SKIP_HEALTH_CHECK="${SKIP_HEALTH_CHECK:-false}" +SKIP_WAIT="${SKIP_WAIT:-false}" # New parameter for skipping waiting ENABLE_HTTPS="${ENABLE_SSL:-true}" # Enable HTTPS with self-signed certificates by default # Source shared shell utilities @@ -707,23 +708,27 @@ wait_for_services() { setup_backup_automation() { local vm_ip="$1" + log_info " Checking backup automation configuration..." + # Load environment variables from the generated .env file if [[ -f "${PROJECT_ROOT}/application/storage/compose/.env" ]]; then # shellcheck source=/dev/null source "${PROJECT_ROOT}/application/storage/compose/.env" + log_info " ✅ Loaded environment configuration" else - log_warning "Environment file not found, using defaults" + log_warning " ⚠️ Environment file not found, using defaults" fi # Check if backup automation is enabled if [[ "${ENABLE_DB_BACKUPS:-false}" != "true" ]]; then - log_info "Database backup automation disabled (ENABLE_DB_BACKUPS=false)" + log_info " ⏹️ Database backup automation disabled (ENABLE_DB_BACKUPS=${ENABLE_DB_BACKUPS:-false})" return 0 fi - log_info "Setting up automated database backups..." + log_info " ✅ Database backup automation enabled - proceeding with setup..." # Create backup directory and set permissions + log_info " ⏳ Creating backup directory and setting permissions..." vm_exec "${vm_ip}" " # Create backup directory if it doesn't exist sudo mkdir -p /var/lib/torrust/mysql/backups @@ -734,8 +739,10 @@ setup_backup_automation() { # Set appropriate permissions chmod 755 /var/lib/torrust/mysql/backups " "Setting up backup directory" + log_info " ✅ Backup directory setup completed" # Install crontab entry for automated backups + log_info " ⏳ Installing MySQL backup cron job..." vm_exec "${vm_ip}" " cd /home/torrust/github/torrust/torrust-tracker-demo @@ -752,8 +759,10 @@ setup_backup_automation() { echo 'Current crontab entries:' crontab -l || echo 'No crontab entries found' " "Installing MySQL backup cron job" + log_info " ✅ Cron job installation completed" # Test backup script functionality + log_info " ⏳ Validating backup script functionality..." vm_exec "${vm_ip}" " cd /home/torrust/github/torrust/torrust-tracker-demo/application @@ -775,8 +784,9 @@ setup_backup_automation() { echo '✅ Fixed backup script permissions' fi " "Validating backup script" + log_info " ✅ Backup script validation completed" - log_success "Database backup automation configured successfully" + log_success " 🎉 Database backup automation configured successfully" log_info "Backup schedule: Daily at 3:00 AM" log_info "Backup location: /var/lib/torrust/mysql/backups" log_info "Retention period: ${BACKUP_RETENTION_DAYS:-7} days" @@ -817,19 +827,32 @@ run_stage() { docker compose --env-file /var/lib/torrust/compose/.env up -d " "Starting application services" - # Wait for services to initialize - wait_for_services "${vm_ip}" + # Wait for services to initialize (unless skipped) + if [[ "${SKIP_WAIT}" != "true" ]]; then + log_info "⏳ Waiting for application services to be healthy..." + log_info " (Use SKIP_WAIT=true to skip this waiting)" + wait_for_services "${vm_ip}" + log_success "🎉 All application services are healthy and ready!" + else + log_warning "⚠️ Skipping wait for service health checks (SKIP_WAIT=true)" + log_info " Note: Services may not be ready immediately" + fi # Setup HTTPS with self-signed certificates (if enabled) if [[ "${ENABLE_HTTPS}" == "true" ]]; then + log_info "⏳ Setting up HTTPS certificates..." log_info "HTTPS certificates already generated - services should be running with HTTPS..." - log_success "HTTPS setup completed" + log_success "✅ HTTPS setup completed" + else + log_info "⏹️ HTTPS setup skipped (ENABLE_HTTPS=${ENABLE_HTTPS})" fi # Setup database backup automation (if enabled) + log_info "⏳ Setting up database backup automation..." setup_backup_automation "${vm_ip}" + log_success "✅ Database backup automation completed" - log_success "Run stage completed" + log_success "🎉 Run stage completed successfully" } # Validate deployment (Health checks) @@ -1018,10 +1041,14 @@ main() { test_ssh_connection "${vm_ip}" wait_for_system_ready "${vm_ip}" release_stage "${vm_ip}" - run_stage "${vm_ip}" + run_stage "${vm_ip}" # This already includes waiting for services if [[ "${SKIP_HEALTH_CHECK}" != "true" ]]; then + log_info "⏳ Running deployment validation..." validate_deployment "${vm_ip}" + log_success "✅ Deployment validation completed" + else + log_warning "⚠️ Skipping deployment validation (SKIP_HEALTH_CHECK=true)" fi show_connection_info "${vm_ip}" @@ -1040,6 +1067,7 @@ Arguments: Environment Variables: SKIP_HEALTH_CHECK Skip health check validation (true/false, default: false) + SKIP_WAIT Skip waiting for services to be ready (true/false, default: false) Examples: $0 local # Deploy to local environment (get IP from Terraform) diff --git a/infrastructure/scripts/provision-infrastructure.sh b/infrastructure/scripts/provision-infrastructure.sh index 2241e68..0306edf 100755 --- a/infrastructure/scripts/provision-infrastructure.sh +++ b/infrastructure/scripts/provision-infrastructure.sh @@ -11,11 +11,10 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" TERRAFORM_DIR="${PROJECT_ROOT}/infrastructure/terraform" # Default values -# Configuration -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" ENVIRONMENT="${1:-local}" ACTION="${2:-apply}" +SKIP_WAIT="${SKIP_WAIT:-false}" # New parameter for skipping waiting +SKIP_WAIT="${SKIP_WAIT:-false}" # New parameter for skipping waiting # Source shared shell utilities # shellcheck source=../../scripts/shell-utils.sh @@ -115,9 +114,32 @@ provision_infrastructure() { tofu apply -auto-approve -var-file="local.tfvars" + # Wait for infrastructure to be fully ready (unless skipped) + if [[ "${SKIP_WAIT}" != "true" ]]; then + log_info "⏳ Waiting for infrastructure to be fully ready..." + log_info " (Use SKIP_WAIT=true to skip this waiting)" + + # Wait for VM IP assignment + if ! wait_for_vm_ip "${ENVIRONMENT}" "${PROJECT_ROOT}"; then + log_error "Failed to wait for VM IP assignment" + exit 1 + fi + + # Wait for cloud-init completion + if ! wait_for_cloud_init_completion "${ENVIRONMENT}"; then + log_error "Failed to wait for cloud-init completion" + exit 1 + fi + + log_success "🎉 Infrastructure is fully ready for application deployment!" + else + log_warning "⚠️ Skipping wait for infrastructure readiness (SKIP_WAIT=true)" + log_info " Note: You may need to wait before running app-deploy" + fi + # Get VM IP and display connection info local vm_ip - vm_ip=$(tofu output -raw vm_ip 2>/dev/null || echo "") + vm_ip=$(cd "${TERRAFORM_DIR}" && tofu output -raw vm_ip 2>/dev/null || echo "") if [[ -n "${vm_ip}" ]]; then log_success "Infrastructure provisioned successfully" diff --git a/scripts/shell-utils.sh b/scripts/shell-utils.sh index d7f6c61..2705bdc 100644 --- a/scripts/shell-utils.sh +++ b/scripts/shell-utils.sh @@ -379,3 +379,137 @@ time_operation() { return 1 fi } + +# ============================================================================= +# Infrastructure Waiting Functions +# ============================================================================= + +# Helper function to get VM IP address from libvirt +get_vm_ip_from_libvirt() { + virsh domifaddr torrust-tracker-demo 2>/dev/null | grep ipv4 | awk '{print $4}' | cut -d'/' -f1 || echo "" +} + +# Helper function for SSH connections with standard options +ssh_to_vm() { + local vm_ip="$1" + local command="$2" + local output_redirect="${3:->/dev/null 2>&1}" + + eval ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no torrust@"${vm_ip}" "\"${command}\"" "${output_redirect}" +} + +# Wait for VM IP assignment after infrastructure provisioning +wait_for_vm_ip() { + local environment="${1:-local}" + local project_root="${2:-$(pwd)}" + + log_info "⏳ Waiting for VM IP assignment..." + local max_attempts=30 + local attempt=1 + local vm_ip="" + + while [[ ${attempt} -le ${max_attempts} ]]; do + log_info " Checking for VM IP (attempt ${attempt}/${max_attempts})..." + + # Try to get IP from terraform output + cd "${project_root}" || return 1 + vm_ip=$(make infra-status ENVIRONMENT="${environment}" 2>/dev/null | grep "vm_ip" | grep -v "No IP assigned yet" | awk -F '"' '{print $2}' || echo "") + + if [[ -n "${vm_ip}" && "${vm_ip}" != "No IP assigned yet" ]]; then + log_success "✅ VM IP assigned: ${vm_ip}" + return 0 + fi + + # Also check libvirt directly as fallback + vm_ip=$(get_vm_ip_from_libvirt) + if [[ -n "${vm_ip}" ]]; then + log_success "✅ VM IP assigned (via libvirt): ${vm_ip}" + # Refresh terraform state to sync with actual VM state + log_info " Refreshing terraform state to sync with VM..." + make infra-refresh-state ENVIRONMENT="${environment}" || true + return 0 + fi + + log_info " VM IP not yet assigned, waiting 10 seconds..." + sleep 10 + ((attempt++)) + done + + log_error "❌ Timeout waiting for VM IP assignment after $((max_attempts * 10)) seconds" + log_error " VM may still be starting or cloud-init may be running" + log_error " You can check manually with: virsh domifaddr torrust-tracker-demo" + return 1 +} + +# Wait for VM to be fully ready (cloud-init completion and Docker availability) +wait_for_cloud_init_completion() { + local environment="${1:-local}" + + log_info "⏳ Waiting for cloud-init to complete..." + local max_attempts=60 # 10 minutes total + local attempt=1 + local vm_ip="" + + # First get the VM IP + vm_ip=$(get_vm_ip_from_libvirt) + if [[ -z "${vm_ip}" ]]; then + log_error "❌ VM IP not available - cannot check readiness" + return 1 + fi + + log_info " VM IP: ${vm_ip} - checking cloud-init readiness..." + + while [[ ${attempt} -le ${max_attempts} ]]; do + log_info " Checking cloud-init status (attempt ${attempt}/${max_attempts})..." + + # Check if SSH is available + if ! ssh_to_vm "${vm_ip}" "echo 'SSH OK'"; then + log_info " SSH not ready yet, waiting 10 seconds..." + sleep 10 + ((attempt++)) + continue + fi + + # Primary check: Official cloud-init status + local cloud_init_status + cloud_init_status=$(ssh_to_vm "${vm_ip}" "cloud-init status" "2>/dev/null" || echo "unknown") + + if [[ "${cloud_init_status}" == *"done"* ]]; then + log_success "✅ Cloud-init reports completion: ${cloud_init_status}" + + # Secondary check: Custom completion marker file + if ssh_to_vm "${vm_ip}" "test -f /var/lib/cloud/torrust-setup-complete"; then + log_success "✅ Setup completion marker found" + + # Tertiary check: Verify critical services are available + # Note: This is not tied to specific software, just basic system readiness + if ssh_to_vm "${vm_ip}" "systemctl is-active docker >/dev/null 2>&1"; then + log_success "✅ Critical services are active" + log_success "🎉 VM is ready for application deployment" + return 0 + else + log_info " Critical services not ready yet, waiting 10 seconds..." + fi + else + log_info " Setup completion marker not found yet, waiting 10 seconds..." + fi + elif [[ "${cloud_init_status}" == *"error"* ]]; then + log_error "❌ Cloud-init failed with error status: ${cloud_init_status}" + + # Try to get more detailed error information + local cloud_init_result + cloud_init_result=$(ssh_to_vm "${vm_ip}" "cloud-init status --long" "2>/dev/null" || echo "unknown") + log_error " Cloud-init detailed status: ${cloud_init_result}" + return 1 + else + log_info " Cloud-init status: ${cloud_init_status}, waiting 10 seconds..." + fi + + sleep 10 + ((attempt++)) + done + + log_error "❌ Timeout waiting for cloud-init to finish after $((max_attempts * 10)) seconds" + log_error " You can check manually with: ssh torrust@${vm_ip} 'cloud-init status --long'" + return 1 +} diff --git a/tests/test-e2e.sh b/tests/test-e2e.sh index 4ed22c7..adf58b7 100755 --- a/tests/test-e2e.sh +++ b/tests/test-e2e.sh @@ -25,20 +25,6 @@ source "${PROJECT_ROOT}/scripts/shell-utils.sh" # Set log file for tee output export SHELL_UTILS_LOG_FILE="${TEST_LOG_FILE}" -# Helper function to get VM IP address from libvirt -get_vm_ip() { - virsh domifaddr torrust-tracker-demo 2>/dev/null | grep ipv4 | awk '{print $4}' | cut -d'/' -f1 || echo "" -} - -# Helper function for SSH connections with standard options -ssh_to_vm() { - local vm_ip="$1" - local command="$2" - local output_redirect="${3:->/dev/null 2>&1}" - - eval ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no torrust@"${vm_ip}" "\"${command}\"" "${output_redirect}" -} - # Track test start time TEST_START_TIME=$(date +%s) @@ -126,13 +112,13 @@ test_infrastructure_provisioning() { fi # Wait for VM to get IP address before proceeding to application deployment - if ! wait_for_vm_ip; then + if ! wait_for_vm_ip "${ENVIRONMENT}" "${PROJECT_ROOT}"; then log_error "VM IP address not available - cannot proceed with application deployment" return 1 fi # Wait for VM to be fully ready (cloud-init completion and Docker availability) - if ! wait_for_cloud_init_to_finish; then + if ! wait_for_cloud_init_completion "${ENVIRONMENT}"; then log_error "VM not ready for application deployment - cloud-init failed or timed out" return 1 fi @@ -178,7 +164,7 @@ test_health_validation() { # Get VM IP for direct testing local vm_ip - vm_ip=$(get_vm_ip) + vm_ip=$(get_vm_ip_from_libvirt) if [[ -n "${vm_ip}" ]]; then log_info "Testing application endpoints on ${vm_ip}..." @@ -211,7 +197,7 @@ test_smoke_testing() { # Get VM IP for testing local vm_ip - vm_ip=$(get_vm_ip) + vm_ip=$(get_vm_ip_from_libvirt) if [[ -z "${vm_ip}" ]]; then log_error "VM IP not available - cannot run mandatory smoke tests" @@ -353,117 +339,6 @@ show_password_warning() { fi } -# Wait for VM IP assignment after infrastructure provisioning -wait_for_vm_ip() { - log_info "Waiting for VM IP assignment..." - local max_attempts=30 - local attempt=1 - local vm_ip="" - - while [[ ${attempt} -le ${max_attempts} ]]; do - log_info "Checking for VM IP (attempt ${attempt}/${max_attempts})..." - - # Try to get IP from terraform output - cd "${PROJECT_ROOT}" - vm_ip=$(make infra-status ENVIRONMENT="${ENVIRONMENT}" 2>/dev/null | grep "vm_ip" | grep -v "No IP assigned yet" | awk -F '"' '{print $2}' || echo "") - - if [[ -n "${vm_ip}" && "${vm_ip}" != "No IP assigned yet" ]]; then - log_success "VM IP assigned: ${vm_ip}" - return 0 - fi - - # Also check libvirt directly as fallback - vm_ip=$(get_vm_ip) - if [[ -n "${vm_ip}" ]]; then - log_success "VM IP assigned (via libvirt): ${vm_ip}" - # Refresh terraform state to sync with actual VM state - log_info "Refreshing terraform state to sync with VM..." - make infra-refresh-state ENVIRONMENT="${ENVIRONMENT}" || true - return 0 - fi - - log_info "VM IP not yet assigned, waiting 10 seconds..." - sleep 10 - ((attempt++)) - done - - log_error "Timeout waiting for VM IP assignment after $((max_attempts * 10)) seconds" - log_error "VM may still be starting or cloud-init may be running" - log_error "You can check manually with: virsh domifaddr torrust-tracker-demo" - return 1 -} - -# Wait for VM to be fully ready (cloud-init completion and Docker availability) -wait_for_cloud_init_to_finish() { - log_info "Waiting for cloud-init to finish..." - local max_attempts=60 # 10 minutes total - local attempt=1 - local vm_ip="" - - # First get the VM IP - vm_ip=$(get_vm_ip) - if [[ -z "${vm_ip}" ]]; then - log_error "VM IP not available - cannot check readiness" - return 1 - fi - - log_info "VM IP: ${vm_ip} - checking cloud-init readiness..." - - while [[ ${attempt} -le ${max_attempts} ]]; do - log_info "Checking cloud-init status (attempt ${attempt}/${max_attempts})..." - - # Check if SSH is available - if ! ssh_to_vm "${vm_ip}" "echo 'SSH OK'"; then - log_info "SSH not ready yet, waiting 10 seconds..." - sleep 10 - ((attempt++)) - continue - fi - - # Primary check: Official cloud-init status - local cloud_init_status - cloud_init_status=$(ssh_to_vm "${vm_ip}" "cloud-init status" "2>/dev/null" || echo "unknown") - - if [[ "${cloud_init_status}" == *"done"* ]]; then - log_success "Cloud-init reports completion: ${cloud_init_status}" - - # Secondary check: Custom completion marker file - if ssh_to_vm "${vm_ip}" "test -f /var/lib/cloud/torrust-setup-complete"; then - log_success "Setup completion marker found" - - # Tertiary check: Verify critical services are available - # Note: This is not tied to specific software, just basic system readiness - if ssh_to_vm "${vm_ip}" "systemctl is-active docker >/dev/null 2>&1"; then - log_success "Critical services are active" - log_success "VM is ready for application deployment" - return 0 - else - log_info "Critical services not ready yet, waiting 10 seconds..." - fi - else - log_info "Setup completion marker not found yet, waiting 10 seconds..." - fi - elif [[ "${cloud_init_status}" == *"error"* ]]; then - log_error "Cloud-init failed with error status: ${cloud_init_status}" - - # Try to get more detailed error information - local cloud_init_result - cloud_init_result=$(ssh_to_vm "${vm_ip}" "cloud-init status --long" "2>/dev/null" || echo "unknown") - log_error "Cloud-init detailed status: ${cloud_init_result}" - return 1 - else - log_info "Cloud-init status: ${cloud_init_status}, waiting 10 seconds..." - fi - - sleep 10 - ((attempt++)) - done - - log_error "Timeout waiting for cloud-init to finish after $((max_attempts * 10)) seconds" - log_error "You can check manually with: ssh torrust@${vm_ip} 'cloud-init status --long'" - return 1 -} - # Main test execution run_e2e_test() { local failed=0