Created
July 16, 2026 17:27
-
-
Save jonasbn/b07da601d38f747d636a67215402250a to your computer and use it in GitHub Desktop.
Wait for a remote host to come back online after reboot and reconnect via SSH.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # SSH Reconnect Script | |
| # Waits for a remote host to come back online after reboot and reconnects via SSH | |
| # Supports SSH config file hostnames and aliases | |
| set -euo pipefail | |
| # Configuration | |
| DEFAULT_PING_INTERVAL=5 | |
| DEFAULT_SSH_TIMEOUT=10 | |
| DEFAULT_MAX_ATTEMPTS=120 # 10 minutes at 5-second intervals | |
| SSH_CONFIG_FILE="$HOME/.ssh/config" | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| CYAN='\033[0;36m' | |
| NC='\033[0m' # No Color | |
| # Function to display usage | |
| usage() { | |
| cat << EOF | |
| Usage: $0 [OPTIONS] HOST [USER] | |
| Wait for a remote host to come back online after reboot and reconnect via SSH. | |
| Supports SSH config hostnames and aliases from ~/.ssh/config. | |
| OPTIONS: | |
| -p, --port PORT SSH port (default: from SSH config or 22) | |
| -i, --interval SECONDS Ping interval in seconds (default: $DEFAULT_PING_INTERVAL) | |
| -t, --timeout SECONDS SSH connection timeout (default: $DEFAULT_SSH_TIMEOUT) | |
| -m, --max-attempts NUM Maximum ping attempts (default: $DEFAULT_MAX_ATTEMPTS) | |
| -k, --key-file FILE SSH private key file (default: from SSH config) | |
| -c, --command COMMAND Command to execute after connecting (optional) | |
| -q, --quiet Quiet mode - minimal output | |
| -l, --list-hosts List available hosts from SSH config | |
| -h, --help Show this help message | |
| ARGUMENTS: | |
| HOST Remote hostname, IP address, or SSH config alias | |
| USER SSH username (optional, defaults to SSH config or current user) | |
| EXAMPLES: | |
| $0 jumphost # Use SSH config alias | |
| $0 192.168.1.100 # Direct IP | |
| $0 -c "uptime" reverseproxy # Execute command on SSH config host | |
| $0 --list-hosts # Show available SSH config hosts | |
| SSH CONFIG INTEGRATION: | |
| The script automatically reads ~/.ssh/config and extracts: | |
| - HostName (actual IP/hostname to ping) | |
| - User (SSH username) | |
| - Port (SSH port) | |
| - IdentityFile (SSH private key) | |
| EOF | |
| } | |
| # Function to log messages | |
| log() { | |
| if [[ "${QUIET:-false}" != "true" ]]; then | |
| echo -e "${1}" | |
| fi | |
| } | |
| # Function to parse SSH config for a given host | |
| parse_ssh_config() { | |
| local host_alias="$1" | |
| local config_file="${SSH_CONFIG_FILE}" | |
| if [[ ! -f "$config_file" ]]; then | |
| log "${YELLOW}Warning: SSH config file not found at $config_file${NC}" | |
| return 1 | |
| fi | |
| # Use ssh -G to get the effective configuration for the host | |
| # This handles all the SSH config parsing including wildcards and includes | |
| if command -v ssh >/dev/null 2>&1; then | |
| ssh -G "$host_alias" 2>/dev/null || return 1 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # Function to extract value from SSH config output | |
| extract_config_value() { | |
| local config_output="$1" | |
| local key="$2" | |
| echo "$config_output" | grep -i "^${key} " | head -n1 | awk '{print $2}' || echo "" | |
| } | |
| # Function to list available hosts from SSH config | |
| list_ssh_hosts() { | |
| local config_file="${SSH_CONFIG_FILE}" | |
| if [[ ! -f "$config_file" ]]; then | |
| log "${RED}SSH config file not found at $config_file${NC}" | |
| return 1 | |
| fi | |
| log "${CYAN}Available SSH hosts from $config_file:${NC}" | |
| log "${BLUE}================================================${NC}" | |
| # Parse hosts from SSH config, excluding wildcards | |
| grep -i "^Host " "$config_file" | \ | |
| awk '{for(i=2;i<=NF;i++) print $i}' | \ | |
| grep -v '\*' | \ | |
| sort -u | \ | |
| while read -r host; do | |
| if [[ -n "$host" ]]; then | |
| # Get the actual hostname/IP | |
| local config_output | |
| if config_output=$(parse_ssh_config "$host" 2>/dev/null); then | |
| local hostname=$(extract_config_value "$config_output" "hostname") | |
| local user=$(extract_config_value "$config_output" "user") | |
| local port=$(extract_config_value "$config_output" "port") | |
| printf "${GREEN}%-20s${NC} -> ${YELLOW}%s@%s:%s${NC}\n" "$host" "$user" "$hostname" "$port" | |
| else | |
| printf "${GREEN}%-20s${NC} -> ${RED}(config parse error)${NC}\n" "$host" | |
| fi | |
| fi | |
| done | |
| } | |
| # Function to resolve SSH config details | |
| resolve_ssh_config() { | |
| local host_alias="$1" | |
| local provided_user="$2" | |
| local provided_port="$3" | |
| local provided_key="$4" | |
| # Try to parse SSH config | |
| local config_output | |
| if config_output=$(parse_ssh_config "$host_alias" 2>/dev/null); then | |
| # Extract values from SSH config | |
| local config_hostname=$(extract_config_value "$config_output" "hostname") | |
| local config_user=$(extract_config_value "$config_output" "user") | |
| local config_port=$(extract_config_value "$config_output" "port") | |
| local config_key=$(extract_config_value "$config_output" "identityfile") | |
| # Expand ~ in identity file path | |
| if [[ "$config_key" =~ ^~/ ]]; then | |
| config_key="${HOME}${config_key#\~}" | |
| fi | |
| # Use provided values or fall back to config values or defaults | |
| SSH_HOSTNAME="${config_hostname:-$host_alias}" | |
| SSH_USER="${provided_user:-${config_user:-$USER}}" | |
| SSH_PORT="${provided_port:-${config_port:-22}}" | |
| SSH_KEY="${provided_key:-$config_key}" | |
| log "${CYAN}Using SSH config for '$host_alias':${NC}" | |
| log "${BLUE} Hostname: $SSH_HOSTNAME${NC}" | |
| log "${BLUE} User: $SSH_USER${NC}" | |
| log "${BLUE} Port: $SSH_PORT${NC}" | |
| if [[ -n "$SSH_KEY" ]]; then | |
| log "${BLUE} Identity: $SSH_KEY${NC}" | |
| fi | |
| else | |
| # No SSH config found, use direct values | |
| SSH_HOSTNAME="$host_alias" | |
| SSH_USER="${provided_user:-$USER}" | |
| SSH_PORT="${provided_port:-22}" | |
| SSH_KEY="$provided_key" | |
| log "${YELLOW}No SSH config found for '$host_alias', using direct connection${NC}" | |
| fi | |
| } | |
| # Function to check if host is reachable | |
| check_host_reachable() { | |
| local host="$1" | |
| if ping -c 1 -W 3000 "$host" >/dev/null 2>&1; then | |
| return 0 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # Function to check if SSH is available | |
| check_ssh_available() { | |
| local host="$1" | |
| local port="$2" | |
| local timeout="$3" | |
| if timeout "$timeout" bash -c "</dev/tcp/$host/$port" 2>/dev/null; then | |
| return 0 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # Function to wait for host to come back online | |
| wait_for_host() { | |
| local host="$1" | |
| local port="$2" | |
| local interval="$3" | |
| local timeout="$4" | |
| local max_attempts="$5" | |
| local attempt=1 | |
| log "${BLUE}Waiting for host $host to come back online...${NC}" | |
| log "${YELLOW}Checking every $interval seconds (max $max_attempts attempts)${NC}" | |
| while [[ $attempt -le $max_attempts ]]; do | |
| log "${BLUE}Attempt $attempt/$max_attempts: Checking connectivity...${NC}" | |
| if check_host_reachable "$host"; then | |
| log "${GREEN}✓ Host $host is reachable${NC}" | |
| log "${BLUE}Checking SSH availability on port $port...${NC}" | |
| if check_ssh_available "$host" "$port" "$timeout"; then | |
| log "${GREEN}✓ SSH is available on $host:$port${NC}" | |
| return 0 | |
| else | |
| log "${YELLOW}⚠ Host reachable but SSH not yet available${NC}" | |
| fi | |
| else | |
| log "${RED}✗ Host $host not reachable${NC}" | |
| fi | |
| if [[ $attempt -lt $max_attempts ]]; then | |
| sleep "$interval" | |
| fi | |
| ((attempt++)) | |
| done | |
| log "${RED}✗ Maximum attempts reached. Host may not be available.${NC}" | |
| return 1 | |
| } | |
| # Function to connect via SSH | |
| ssh_connect() { | |
| local host_alias="$1" | |
| local command="$2" | |
| local ssh_args=() | |
| ssh_args+=("-o" "ConnectTimeout=$DEFAULT_SSH_TIMEOUT") | |
| ssh_args+=("-o" "ServerAliveInterval=30") | |
| ssh_args+=("-o" "ServerAliveCountMax=3") | |
| if [[ -n "$command" ]]; then | |
| log "${GREEN}Connecting to $host_alias and executing: $command${NC}" | |
| ssh "${ssh_args[@]}" "$host_alias" "$command" | |
| else | |
| log "${GREEN}Connecting to $host_alias...${NC}" | |
| ssh "${ssh_args[@]}" "$host_alias" | |
| fi | |
| } | |
| # Main function | |
| main() { | |
| local host="" | |
| local user="" | |
| local port="" | |
| local interval="$DEFAULT_PING_INTERVAL" | |
| local timeout="$DEFAULT_SSH_TIMEOUT" | |
| local max_attempts="$DEFAULT_MAX_ATTEMPTS" | |
| local key_file="" | |
| local command="" | |
| local quiet="false" | |
| local list_hosts="false" | |
| # Parse command line arguments | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -p|--port) | |
| port="$2" | |
| shift 2 | |
| ;; | |
| -i|--interval) | |
| interval="$2" | |
| shift 2 | |
| ;; | |
| -t|--timeout) | |
| timeout="$2" | |
| shift 2 | |
| ;; | |
| -m|--max-attempts) | |
| max_attempts="$2" | |
| shift 2 | |
| ;; | |
| -k|--key-file) | |
| key_file="$2" | |
| shift 2 | |
| ;; | |
| -c|--command) | |
| command="$2" | |
| shift 2 | |
| ;; | |
| -q|--quiet) | |
| quiet="true" | |
| shift | |
| ;; | |
| -l|--list-hosts) | |
| list_hosts="true" | |
| shift | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| -*) | |
| echo "Unknown option: $1" >&2 | |
| usage >&2 | |
| exit 1 | |
| ;; | |
| *) | |
| if [[ -z "$host" ]]; then | |
| host="$1" | |
| elif [[ -z "$user" ]]; then | |
| user="$1" | |
| else | |
| echo "Too many arguments" >&2 | |
| usage >&2 | |
| exit 1 | |
| fi | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Export QUIET for log function | |
| export QUIET="$quiet" | |
| # Handle list hosts option | |
| if [[ "$list_hosts" == "true" ]]; then | |
| list_ssh_hosts | |
| exit 0 | |
| fi | |
| # Validate required arguments | |
| if [[ -z "$host" ]]; then | |
| echo "Error: HOST is required" >&2 | |
| echo "Use --list-hosts to see available SSH config hosts" >&2 | |
| usage >&2 | |
| exit 1 | |
| fi | |
| # Validate key file if specified | |
| if [[ -n "$key_file" && ! -f "$key_file" ]]; then | |
| echo "Error: Key file '$key_file' not found" >&2 | |
| exit 1 | |
| fi | |
| log "${BLUE}Starting SSH reconnect script${NC}" | |
| # Resolve SSH configuration | |
| resolve_ssh_config "$host" "$user" "$port" "$key_file" | |
| # Validate resolved SSH key | |
| if [[ -n "$SSH_KEY" && ! -f "$SSH_KEY" ]]; then | |
| log "${YELLOW}Warning: SSH key file '$SSH_KEY' from config not found${NC}" | |
| SSH_KEY="" | |
| fi | |
| log "${BLUE}Target: $SSH_USER@$SSH_HOSTNAME:$SSH_PORT${NC}" | |
| # Wait for host to come back online | |
| if wait_for_host "$SSH_HOSTNAME" "$SSH_PORT" "$interval" "$timeout" "$max_attempts"; then | |
| log "${GREEN}Host is back online! Connecting...${NC}" | |
| sleep 2 # Brief pause to ensure SSH service is fully ready | |
| ssh_connect "$host" "$command" # Use original host alias for SSH connection | |
| else | |
| log "${RED}Failed to detect host coming back online${NC}" | |
| exit 1 | |
| fi | |
| } | |
| # Run main function with all arguments | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment