#!/usr/bin/env bash set -euo pipefail # Color codes for output formatting RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # No Color # Icons ICON_SUCCESS="✓" ICON_ERROR="✗" ICON_INFO="ℹ" ICON_WARNING="⚠" ICON_ARROW="→" ICON_DOWNLOAD="⬇" ICON_UPLOAD="⬆" ICON_INSTALL="📦" ICON_UPDATE="🔄" ICON_DEPLOY="🚀" ICON_TIMER="⏰" ICON_CHECK="🔍" ICON_CONFIG="⚙" ICON_NETWORK="🌐" ICON_VERSION="📋" ICON_LOG="📝" ICON_HOST="🖥" SSH_HOST="" HOSTS_FILE="" INSTALL_TIMER=false TIMER_TIME="daily" DEFAULT_PASSWORD="" REMOTE_ARGS=() SSH_TIMEOUT=30 # Arrays to track deployment results declare -a SUCCESS_HOSTS=() declare -a FAILED_HOSTS=() declare -a HOST_ACTIONS=() # Helper functions for formatted output print_header() { echo -e "\n${BLUE}${BOLD}╔═══════════════════════════════════════════════════════════════╗${NC}" echo -e "${BLUE}${BOLD}║ $1${NC}" echo -e "${BLUE}${BOLD}╚═══════════════════════════════════════════════════════════════╝${NC}\n" } print_info() { echo -e " ${BLUE}${ICON_INFO}${NC} $1" } print_success() { echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1" } print_error() { echo -e " ${RED}${ICON_ERROR}${NC} $1" } print_warning() { echo -e " ${YELLOW}${ICON_WARNING}${NC} $1" } print_status() { echo -e " ${CYAN}${ICON_ARROW}${NC} $1" } print_step() { echo -e " ${DIM}•${NC} $1" } while [[ $# -gt 0 ]]; do case "$1" in --ssh) SSH_HOST="$2" shift 2 ;; --hosts-file|--file) HOSTS_FILE="$2" shift 2 ;; --timer) INSTALL_TIMER=true REMOTE_ARGS+=("--timer") shift ;; --time) TIMER_TIME="$2" REMOTE_ARGS+=("--time" "$2") shift 2 ;; --password|-p) DEFAULT_PASSWORD="$2" shift 2 ;; *) echo "Unknown option: $1" exit 1 ;; esac done deploy_to_host() { local host="$1" local sudo_pass="${2:-}" echo -e "${CYAN}${BOLD}┌─────────────────────────────────────────────────────────────┐${NC}" echo -e "${CYAN}${BOLD}│${NC} ${ICON_DEPLOY} ${YELLOW}${host}${NC}" echo -e "${CYAN}${BOLD}└─────────────────────────────────────────────────────────────┘${NC}" REMOTE_SCRIPT="/tmp/.netbird-install-update-$$" REMOTE_ACTION_FILE="/tmp/.netbird-action-$$" LOCAL_ACTION_FILE="/tmp/.netbird-deploy-action-$$" print_step "Copying script..." if ! scp -o ConnectTimeout=$SSH_TIMEOUT -o BatchMode=yes "$0" "$host:$REMOTE_SCRIPT" 2>&1; then print_error "Failed to copy script to $host (timeout or connection error)" return 1 fi print_step "Executing script..." # Build SSH command based on whether we have a password if [ -n "$sudo_pass" ]; then # Write password to remote temp file using printf to handle special characters # Use base64 to safely transfer the password local encoded_pass=$(echo -n "$sudo_pass" | base64) if ! ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "echo '$encoded_pass' | base64 -d > /tmp/.netbird-pass-$$ && chmod 600 /tmp/.netbird-pass-$$" 2>&1; then print_error "Failed to setup password file on $host" return 1 fi # Execute script with sudo, reading password from file if ! ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "sudo -S bash $REMOTE_SCRIPT ${REMOTE_ARGS[*]} < /tmp/.netbird-pass-$$" 2>&1; then print_error "Failed to execute script on $host" ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "rm -f $REMOTE_SCRIPT $REMOTE_ACTION_FILE /tmp/.netbird-pass-$$" 2>/dev/null || true return 1 fi # Clean up password file ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "rm -f /tmp/.netbird-pass-$$" 2>/dev/null || true else # Interactive sudo with terminal if ! ssh -o ConnectTimeout=$SSH_TIMEOUT -o BatchMode=yes -t "$host" "sudo bash $REMOTE_SCRIPT ${REMOTE_ARGS[*]}" 2>&1; then print_error "Failed to execute script on $host" ssh -o ConnectTimeout=$SSH_TIMEOUT -o BatchMode=yes "$host" "rm -f $REMOTE_SCRIPT $REMOTE_ACTION_FILE" 2>/dev/null || true return 1 fi fi # Read the action from the remote action file and write to local action file if action_output=$(ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "cat $REMOTE_ACTION_FILE 2>/dev/null" < /dev/null 2>&1); then echo "$action_output" > "$LOCAL_ACTION_FILE" else echo "unknown" > "$LOCAL_ACTION_FILE" fi # Clean up remote files ssh -o ConnectTimeout=$SSH_TIMEOUT -o StrictHostKeyChecking=no "$host" "rm -f $REMOTE_SCRIPT $REMOTE_ACTION_FILE" < /dev/null 2>/dev/null || true print_success "Completed: $host" return 0 } if [ -n "$HOSTS_FILE" ]; then if [ ! -f "$HOSTS_FILE" ]; then print_error "Hosts file not found: $HOSTS_FILE" exit 1 fi print_header "${ICON_NETWORK} Multi-Host Deployment" print_info "Reading hosts from: ${YELLOW}$HOSTS_FILE${NC}" print_info "SSH timeout: ${YELLOW}${SSH_TIMEOUT}s${NC}" echo "" SUCCESS_COUNT=0 FAIL_COUNT=0 ACTION_FILE="/tmp/.netbird-deploy-action-$$" # Use file descriptor 3 to read hosts file (prevents SSH from consuming stdin) exec 3< "$HOSTS_FILE" while IFS= read -r line <&3 || [ -n "$line" ]; do # Skip empty lines and comments [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue # Trim whitespace line=$(echo "$line" | xargs) # Parse hostname and optional password host=$(echo "$line" | awk '{print $1}') sudo_pass=$(echo "$line" | awk '{print $2}') # Use default password if no host-specific password is provided if [ -z "$sudo_pass" ] && [ -n "$DEFAULT_PASSWORD" ]; then sudo_pass="$DEFAULT_PASSWORD" fi # Run deployment (output streams to terminal) if deploy_to_host "$host" "$sudo_pass"; then # Read the action from the temp file action="unknown" if [ -f "$ACTION_FILE" ]; then action=$(cat "$ACTION_FILE") rm -f "$ACTION_FILE" fi SUCCESS_HOSTS+=("$host") HOST_ACTIONS+=("$action") ((SUCCESS_COUNT++)) || true else FAILED_HOSTS+=("$host") ((FAIL_COUNT++)) || true fi echo "" done exec 3<&- # Print detailed summary echo -e "\n${GREEN}${BOLD}╔═══════════════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}${BOLD}║${NC} ${ICON_SUCCESS} ${GREEN}${BOLD}Deployment Summary${NC}" echo -e "${GREEN}${BOLD}╠═══════════════════════════════════════════════════════════════╣${NC}" echo -e "${GREEN}${BOLD}║${NC} ${GREEN}${ICON_SUCCESS}${NC} Successful: ${BOLD}$SUCCESS_COUNT${NC}" echo -e "${GREEN}${BOLD}║${NC} ${RED}${ICON_ERROR}${NC} Failed: ${BOLD}$FAIL_COUNT${NC}" echo -e "${GREEN}${BOLD}╚═══════════════════════════════════════════════════════════════╝${NC}" # Show successful hosts with actions if [ ${#SUCCESS_HOSTS[@]} -gt 0 ]; then echo -e "\n${GREEN}${BOLD}Successful Hosts:${NC}" for i in "${!SUCCESS_HOSTS[@]}"; do host="${SUCCESS_HOSTS[$i]}" action="${HOST_ACTIONS[$i]}" echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${YELLOW}$host${NC} ${DIM}($action)${NC}" done fi # Show failed hosts if [ ${#FAILED_HOSTS[@]} -gt 0 ]; then echo -e "\n${RED}${BOLD}Failed Hosts:${NC}" for host in "${FAILED_HOSTS[@]}"; do echo -e " ${RED}${ICON_ERROR}${NC} ${YELLOW}$host${NC}" done fi echo "" exit 0 fi if [ -n "$SSH_HOST" ]; then print_header "${ICON_DEPLOY} Single Host Deployment" ACTION_FILE="/tmp/.netbird-deploy-action-$$" if deploy_to_host "$SSH_HOST" "$DEFAULT_PASSWORD"; then # Read and display the action if [ -f "$ACTION_FILE" ]; then action=$(cat "$ACTION_FILE") rm -f "$ACTION_FILE" print_info "Action performed: ${YELLOW}$action${NC}" fi fi echo "" exit 0 fi if [ "$EUID" -ne 0 ]; then print_warning "Elevating to root..." exec sudo "$0" "$@" fi WORK_DIR=$(mktemp -d) trap 'rm -rf "$WORK_DIR"' EXIT detect_install() { if ! command -v netbird &>/dev/null; then echo "none" return fi if [ -f /etc/netbird/install.conf ]; then local pm pm=$(grep -oP 'package_manager=\K.*' /etc/netbird/install.conf 2>/dev/null || true) if [ "$pm" = "bin" ]; then echo "bin" return elif [ -n "$pm" ]; then echo "pkg:$pm" return fi fi if dpkg -s netbird &>/dev/null; then echo "pkg:apt" return fi if rpm -q netbird &>/dev/null; then if command -v dnf &>/dev/null; then echo "pkg:dnf" else echo "pkg:yum" fi return fi if pacman -Q netbird &>/dev/null; then echo "pkg:pacman" return fi echo "bin" } install_timer() { print_header "${ICON_TIMER} Installing Auto-Update Timer" if systemctl is-active --quiet netbird-update.timer; then print_warning "Updating existing timer..." systemctl stop netbird-update.timer systemctl disable netbird-update.timer fi cat > /etc/systemd/system/netbird-update.service <<'UNIT' [Unit] Description=Netbird Auto-Update [Service] Type=oneshot ExecStart=/bin/bash -c 'curl -fsSL https://pkgs.netbird.io/install.sh | UPDATE_NETBIRD=true sh && chown root:root /usr/bin/netbird 2>/dev/null || true && chmod +x /usr/bin/netbird 2>/dev/null || true && restorecon -v /usr/bin/netbird 2>/dev/null || true && systemctl restart netbird 2>/dev/null || true' StandardOutput=append:/var/log/netbird-update.log StandardError=append:/var/log/netbird-update.log UNIT cat > /etc/systemd/system/netbird-update.timer </dev/null || true restorecon -v /usr/bin/netbird 2>/dev/null || true systemctl restart netbird 2>/dev/null || true print_success "Netbird installed successfully" echo "fresh install" > /tmp/.netbird-action-$$ ;; pkg:*) print_header "${ICON_UPDATE} Package Manager Replacement" pm="${state#pkg:}" print_info "Netbird installed via ${YELLOW}$pm${NC}" print_info "Replacing with binary install..." cat > /tmp/.netbird_replace.sh <<'EOF' #!/usr/bin/env bash set -euo pipefail sleep 3 systemctl stop netbird 2>/dev/null || true case "$1" in apt) apt-get remove -y netbird netbird-ui 2>/dev/null || true ;; yum) yum remove -y netbird netbird-ui 2>/dev/null || true ;; dnf) dnf remove -y netbird netbird-ui 2>/dev/null || true ;; zypper) zypper remove -y netbird netbird-ui 2>/dev/null || true ;; rpm-ostree) rpm-ostree uninstall -y netbird netbird-ui 2>/dev/null || true ;; pacman) pacman -Rns --noconfirm netbird netbird-ui 2>/dev/null || true ;; esac USE_BIN_INSTALL=true SKIP_UI_APP=true bash /tmp/.netbird_install.sh chown root:root /usr/bin/netbird 2>/dev/null || true chmod +x /usr/bin/netbird 2>/dev/null || true restorecon -v /usr/bin/netbird 2>/dev/null || true systemctl restart netbird 2>/dev/null || true rm -f /tmp/.netbird_replace.sh /tmp/.netbird_install.sh EOF chmod +x /tmp/.netbird_replace.sh cp "$WORK_DIR/install.sh" /tmp/.netbird_install.sh setsid bash /tmp/.netbird_replace.sh "$pm" /var/log/netbird-update.log 2>&1 & print_warning "Replacement scheduled in background" print_info "Connection may drop momentarily" print_info "Check ${YELLOW}/var/log/netbird-update.log${NC} for progress" echo "pkg replacement" > /tmp/.netbird-action-$$ ;; bin) print_header "${ICON_VERSION} Binary Update Check" if [ ! -f /etc/netbird/install.conf ]; then mkdir -p /etc/netbird echo "package_manager=bin" > /etc/netbird/install.conf fi current=$(netbird version 2>/dev/null || echo "unknown") latest=$(curl -fsSL https://pkgs.netbird.io/releases/latest 2>/dev/null | grep -oP '"tag_name":\s*"v?\K[^"]+' || echo "unknown") print_info "Current version: ${YELLOW}$current${NC}" print_info "Latest version: ${YELLOW}$latest${NC}" echo "" if [ "$current" = "$latest" ] && [ "$current" != "unknown" ]; then print_success "Netbird is up to date" echo "up to date" > /tmp/.netbird-action-$$ else print_info "Updating Netbird: ${YELLOW}$current${NC} → ${GREEN}$latest${NC}" cp "$WORK_DIR/install.sh" /tmp/.netbird_update.sh setsid bash -c 'sleep 3 && bash /tmp/.netbird_update.sh --update && chown root:root /usr/bin/netbird 2>/dev/null || true && chmod +x /usr/bin/netbird 2>/dev/null || true && restorecon -v /usr/bin/netbird 2>/dev/null || true && systemctl restart netbird 2>/dev/null || true && rm -f /tmp/.netbird_update.sh' \ /var/log/netbird-update.log 2>&1 & print_warning "Update scheduled in background" print_info "Connection may drop momentarily" print_info "Check ${YELLOW}/var/log/netbird-update.log${NC} for progress" echo "updated" > /tmp/.netbird-action-$$ fi ;; esac if $INSTALL_TIMER; then install_timer fi echo ""