From 82b9c089b8ea909d2c124eeb8cde13f1e08bc37c Mon Sep 17 00:00:00 2001 From: A <6723574+louisgv@users.noreply.github.com> Date: Fri, 13 Feb 2026 18:26:46 +0000 Subject: [PATCH] fix: add actionable guidance to SSH, API, and dependency error messages Improve error messages in shared utilities and cloud providers that previously showed bare "Failed to..." messages without telling users how to fix the problem. Shared (shared/common.sh): - generate_ssh_key_if_missing: handle ssh-keygen/mkdir failures with disk space and permission guidance - get_ssh_fingerprint: detect missing/corrupt public key files with regeneration instructions - generic_ssh_wait: structured "How to fix" with manual SSH test command and firewall check - _report_api_failure: add DNS/firewall/proxy guidance for network errors - ensure_jq: platform-specific install commands for unknown package managers, hash rehash hint after install - get_openrouter_api_key_manual: structured guidance after 3 failed attempts Cloud providers: - Contabo: actionable guidance for OAuth token failures - Exoscale: guidance for credential validation and CLI download failures - Netcup: network connectivity hint for API connection failure - Scaleway: structured guidance for project ID lookup failure Agent: ux-engineer Co-Authored-By: Claude Sonnet 4.5 --- contabo/lib/common.sh | 18 +++++++-- exoscale/lib/common.sh | 10 ++++- netcup/lib/common.sh | 1 + scaleway/lib/common.sh | 6 ++- shared/common.sh | 83 ++++++++++++++++++++++++++++++++++-------- 5 files changed, 98 insertions(+), 20 deletions(-) diff --git a/contabo/lib/common.sh b/contabo/lib/common.sh index 0f2fcc7ad..7072c6631 100644 --- a/contabo/lib/common.sh +++ b/contabo/lib/common.sh @@ -35,12 +35,22 @@ get_contabo_access_token() { -d "grant_type=password" \ "${CONTABO_AUTH_URL}" 2>&1) || { log_error "Failed to obtain Contabo OAuth token" - log_error "Response: $response" + log_error "" + log_error "How to fix:" + log_error " 1. Verify your credentials at: https://my.contabo.com/api/details" + log_error " 2. Check that CONTABO_CLIENT_ID, CONTABO_CLIENT_SECRET, CONTABO_API_USER," + log_error " and CONTABO_API_PASSWORD are all set correctly" + log_error " 3. The API password is separate from your Contabo login password" return 1 } if echo "$response" | grep -q '"error"'; then - log_error "OAuth authentication failed: $response" + log_error "Contabo OAuth authentication failed" + log_error "" + log_error "How to fix:" + log_error " 1. Verify credentials at: https://my.contabo.com/api/details" + log_error " 2. Ensure your API user has not been deactivated" + log_error " 3. Re-run to enter new credentials" return 1 fi @@ -48,7 +58,9 @@ get_contabo_access_token() { token=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('access_token',''))" 2>/dev/null) if [[ -z "$token" ]]; then - log_error "Failed to extract access token from response" + log_error "Failed to extract access token from Contabo OAuth response" + log_error "The API returned an unexpected response format." + log_error "Try again, or check Contabo's API status page." return 1 fi diff --git a/exoscale/lib/common.sh b/exoscale/lib/common.sh index 73e68c25d..25e84b3a9 100644 --- a/exoscale/lib/common.sh +++ b/exoscale/lib/common.sh @@ -60,7 +60,9 @@ ensure_exo_cli() { local temp_dir temp_dir=$(mktemp -d) if ! curl -fsSL "$exo_url" -o "${temp_dir}/exo.tar.gz"; then - log_error "Failed to download exo CLI" + log_error "Failed to download exo CLI from: $exo_url" + log_error "Check your internet connection and try again." + log_error "You can also install manually: https://community.exoscale.com/documentation/tools/exoscale-command-line-interface/" rm -rf "$temp_dir" return 1 fi @@ -77,6 +79,7 @@ ensure_exo_cli() { test_exoscale_creds() { if ! exo config list &>/dev/null; then log_error "Exoscale credentials not configured or invalid" + log_error "Get your API credentials at: https://portal.exoscale.com/iam/api-keys" return 1 fi @@ -128,6 +131,11 @@ ensure_exoscale_creds() { return 0 else log_error "Failed to configure Exoscale credentials" + log_error "" + log_error "How to fix:" + log_error " 1. Verify your API Key and Secret at: https://portal.exoscale.com/iam/api-keys" + log_error " 2. Ensure the API key has the required permissions (compute, SSH keys)" + log_error " 3. Re-run the command to enter new credentials" return 1 fi } diff --git a/netcup/lib/common.sh b/netcup/lib/common.sh index fbe377cf3..5bfa63767 100644 --- a/netcup/lib/common.sh +++ b/netcup/lib/common.sh @@ -72,6 +72,7 @@ netcup_get_session() { -H "Content-Type: application/json" \ -d "$body" 2>&1) || { log_error "Failed to connect to Netcup API" + log_error "Check your internet connection and try again." return 1 } diff --git a/scaleway/lib/common.sh b/scaleway/lib/common.sh index 6282cce0c..3741ced4a 100644 --- a/scaleway/lib/common.sh +++ b/scaleway/lib/common.sh @@ -88,7 +88,11 @@ get_scaleway_project_id() { if [[ -z "$project_id" ]]; then log_error "Failed to get Scaleway project ID" - log_warn "Set SCW_DEFAULT_PROJECT_ID environment variable or check API permissions" + log_error "" + log_error "How to fix:" + log_error " 1. Set SCW_DEFAULT_PROJECT_ID in your environment" + log_error " 2. Find your Project ID at: https://console.scaleway.com/project/settings" + log_error " 3. Ensure your API key has permission to list projects" return 1 fi diff --git a/shared/common.sh b/shared/common.sh index 26cccfeba..69f38be97 100644 --- a/shared/common.sh +++ b/shared/common.sh @@ -122,24 +122,33 @@ ensure_jq() { if [[ "$OSTYPE" == "darwin"* ]]; then if command -v brew &>/dev/null; then - brew install jq || { log_error "Failed to install jq via Homebrew"; return 1; } + brew install jq || { log_error "Failed to install jq via Homebrew. Run 'brew install jq' manually."; return 1; } else - log_error "Install jq: brew install jq (or https://jqlang.github.io/jq/download/)" + log_error "jq is required but not installed" + log_error "Install it with: brew install jq" + log_error "If Homebrew is not available: https://jqlang.github.io/jq/download/" return 1 fi elif command -v apt-get &>/dev/null; then - sudo apt-get update -qq && sudo apt-get install -y jq || { log_error "Failed to install jq via apt"; return 1; } + sudo apt-get update -qq && sudo apt-get install -y jq || { log_error "Failed to install jq via apt. Run 'sudo apt-get install -y jq' manually."; return 1; } elif command -v dnf &>/dev/null; then - sudo dnf install -y jq || { log_error "Failed to install jq via dnf"; return 1; } + sudo dnf install -y jq || { log_error "Failed to install jq via dnf. Run 'sudo dnf install -y jq' manually."; return 1; } elif command -v apk &>/dev/null; then - sudo apk add jq || { log_error "Failed to install jq via apk"; return 1; } + sudo apk add jq || { log_error "Failed to install jq via apk. Run 'sudo apk add jq' manually."; return 1; } else - log_error "jq is required but not installed. Install from https://jqlang.github.io/jq/download/" + log_error "jq is required but not installed" + log_error "" + log_error "Install jq for your system:" + log_error " Ubuntu/Debian: sudo apt-get install -y jq" + log_error " Fedora/RHEL: sudo dnf install -y jq" + log_error " macOS: brew install jq" + log_error " Other: https://jqlang.github.io/jq/download/" return 1 fi if ! command -v jq &>/dev/null; then - log_error "jq not found in PATH after installation" + log_error "jq was installed but is not found in PATH" + log_error "Try opening a new terminal or run: hash -r" return 1 fi @@ -444,7 +453,12 @@ get_openrouter_api_key_manual() { while [[ -z "${api_key}" ]]; do attempts=$((attempts + 1)) if [[ ${attempts} -gt ${max_attempts} ]]; then - log_error "Too many failed attempts. Set OPENROUTER_API_KEY environment variable and try again." + log_error "Too many failed attempts." + log_error "" + log_error "How to fix:" + log_error " 1. Get your key from: https://openrouter.ai/settings/keys" + log_error " 2. Set it before running spawn: export OPENROUTER_API_KEY=sk-or-v1-..." + log_error " 3. Then re-run: spawn " return 1 fi api_key=$(safe_read "Enter your OpenRouter API key: ") || return 1 @@ -1033,9 +1047,21 @@ generate_ssh_key_if_missing() { if [[ -f "${key_path}" ]]; then return 0 fi - log_step "Generating SSH key..." - mkdir -p "$(dirname "${key_path}")" - ssh-keygen -t ed25519 -f "${key_path}" -N "" -q + log_step "Generating SSH key at ${key_path}..." + mkdir -p "$(dirname "${key_path}")" || { + log_error "Failed to create SSH key directory: $(dirname "${key_path}")" + log_error "Check that you have write permissions to this directory." + return 1 + } + ssh-keygen -t ed25519 -f "${key_path}" -N "" -q || { + log_error "Failed to generate SSH key at ${key_path}" + log_error "" + log_error "How to fix:" + log_error " 1. Check disk space: df -h $(dirname "${key_path}")" + log_error " 2. Check permissions: ls -la $(dirname "${key_path}")" + log_error " 3. Generate manually: ssh-keygen -t ed25519 -f ${key_path}" + return 1 + } log_info "SSH key generated at ${key_path}" } @@ -1043,7 +1069,21 @@ generate_ssh_key_if_missing() { # Usage: get_ssh_fingerprint PUB_KEY_PATH get_ssh_fingerprint() { local pub_path="${1}" - ssh-keygen -lf "${pub_path}" -E md5 2>/dev/null | awk '{print $2}' | sed 's/MD5://' + if [[ ! -f "${pub_path}" ]]; then + log_error "SSH public key not found: ${pub_path}" + log_error "Expected a public key file alongside your private key." + log_error "Regenerate with: ssh-keygen -t ed25519 -f ${pub_path%.pub}" + return 1 + fi + local fingerprint + fingerprint=$(ssh-keygen -lf "${pub_path}" -E md5 2>/dev/null | awk '{print $2}' | sed 's/MD5://') + if [[ -z "${fingerprint}" ]]; then + log_error "Failed to read SSH public key fingerprint from ${pub_path}" + log_error "The key file may be corrupted or in an unsupported format." + log_error "Regenerate with: ssh-keygen -t ed25519 -f ${pub_path%.pub}" + return 1 + fi + echo "${fingerprint}" } # JSON-escape a string (for embedding in JSON bodies) @@ -1255,7 +1295,13 @@ _report_api_failure() { local max_retries="${2}" log_error "${retry_reason} after ${max_retries} attempts" if [[ "${retry_reason}" == "Cloud API network error" ]]; then - log_warn "Check your internet connection and verify the provider's API is reachable." + log_warn "Could not reach the cloud provider's API." + log_warn "" + log_warn "How to fix:" + log_warn " 1. Check your internet connection: curl -s https://httpbin.org/ip" + log_warn " 2. Check DNS resolution: nslookup the provider's API hostname" + log_warn " 3. If behind a proxy or firewall, ensure HTTPS traffic is allowed" + log_warn " 4. Try again in a few moments (the API may be temporarily down)" else log_warn "This is usually caused by rate limiting or temporary provider issues." log_warn "Wait a minute and try again, or check the provider's status page." @@ -1468,8 +1514,15 @@ generic_ssh_wait() { attempt=$((attempt + 1)) done - log_error "${description} timed out after ${elapsed_time}s" - log_warn "The server at ${ip} may still be booting. You can try again or check its status in your cloud provider dashboard." + log_error "${description} timed out after ${elapsed_time}s (server: ${ip})" + log_warn "" + log_warn "The server may still be booting or the connection may be blocked." + log_warn "" + log_warn "How to fix:" + log_warn " 1. Re-run the command to try again (the server may need more time)" + log_warn " 2. Check your cloud provider dashboard to verify the server is running" + log_warn " 3. Test SSH manually: ssh ${username}@${ip}" + log_warn " 4. Check that port 22 is open in the server's firewall/security group" return 1 }