diff --git a/hub/onboard-spoke.sh b/hub/onboard-spoke.sh index d0d574b..97e24b4 100755 --- a/hub/onboard-spoke.sh +++ b/hub/onboard-spoke.sh @@ -73,28 +73,11 @@ KEY_PATH="$SSH_DIR/$KEY_NAME" mkdir -p "$(dirname "$RCLONE_CONF")" -header "Select Tunnel Key" -AVAILABLE_KEYS=() -while IFS= read -r keyfile; do - AVAILABLE_KEYS+=("$keyfile") -done < <(find "$SSH_DIR" -maxdepth 1 -type f ! -name "*.pub" ! -name "known_hosts" ! -name "authorized_keys" ! -name "config" | sort) - -if [ ${#AVAILABLE_KEYS[@]} -eq 0 ]; then - die "No private keys found in $SSH_DIR." -fi - -echo "Available keys:" -for i in "${!AVAILABLE_KEYS[@]}"; do - echo " $i) ${AVAILABLE_KEYS[$i]}" -done -echo "" -read -rp "Choose key to use for tunnel access [0]: " KEY_CHOICE -KEY_CHOICE="${KEY_CHOICE:-0}" -[[ "$KEY_CHOICE" =~ ^[0-9]+$ ]] && [ "$KEY_CHOICE" -lt "${#AVAILABLE_KEYS[@]}" ] || die "Invalid choice." -TUNNEL_KEY="${AVAILABLE_KEYS[$KEY_CHOICE]}" -info "Using key: $TUNNEL_KEY" - header "Checking Tunnel" +info "Verifying spoke SSH service is reachable on port $TUNNEL_PORT..." +if ! timeout 5 bash -c "cat < /dev/null > /dev/tcp/localhost/$TUNNEL_PORT" 2>/dev/null; then + die "Cannot connect to port $TUNNEL_PORT on localhost — is the tunnel up?" +fi info "Scanning spoke host key..." KEYSCAN=$(ssh-keyscan -p "$TUNNEL_PORT" -H localhost 2>/dev/null) [ -n "$KEYSCAN" ] || die "Spoke not reachable on port $TUNNEL_PORT — is the tunnel up?" @@ -105,25 +88,20 @@ while IFS= read -r KEYSCAN_LINE; do fi done <<< "$KEYSCAN" -info "Verifying spoke is reachable on port $TUNNEL_PORT..." -retry_or_abort \ - "ssh -i \"$TUNNEL_KEY\" -o BatchMode=yes -o ConnectTimeout=10 -p \"$TUNNEL_PORT\" \"$SPOKE_USER\"@localhost exit" \ - "Spoke not reachable on port $TUNNEL_PORT. Make sure the tunnel is up." - -header "Generating Hub SSH Key" +header "Generating Hub-to-Spoke Access Key" if [ -f "$KEY_PATH" ]; then warn "Key $KEY_PATH already exists, skipping generation." else - ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" + ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "$KEY_NAME" info "Key generated: $KEY_PATH" fi chmod 600 "$KEY_PATH" info "Permissions set: $KEY_PATH is 600" -header "Installing Hub Access Key on Spoke" +header "Installing Hub-to-Spoke Access Key on Spoke" info "Copying hub public key to spoke's authorized_keys so the hub can SSH in for rclone..." info "(You will be prompted for the $SPOKE_USER password on the spoke)" -if ssh-copy-id -i "$KEY_PATH.pub" -o "IdentityFile=$TUNNEL_KEY" -p "$TUNNEL_PORT" "$SPOKE_USER"@localhost; then +if ssh-copy-id -i "$KEY_PATH.pub" -p "$TUNNEL_PORT" "$SPOKE_USER"@localhost; then info "Key copied." else warn "ssh-copy-id failed — password auth may be disabled on the spoke." @@ -136,7 +114,7 @@ else read -rp "Press ENTER once the key has been added to the spoke..." fi -header "Testing Hub -> Spoke Key Auth" +header "Testing Hub-to-Spoke Key Auth" retry_or_abort \ "ssh -i \"$KEY_PATH\" -o BatchMode=yes -o ConnectTimeout=10 -p \"$TUNNEL_PORT\" \"$SPOKE_USER\"@localhost exit" \ "Key auth failed. Check authorized_keys on the spoke." @@ -147,139 +125,6 @@ if grep -q "\[${SPOKE_NAME}-remote\]" "$RCLONE_CONF" 2>/dev/null; then warn "Remote [${SPOKE_NAME}-remote] already exists in $RCLONE_CONF, skipping." else [ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >> "$RCLONE_CONF" - cat >> "$RCLONE_CONF" <> "$RCLONE_CONF" info "Remote [${SPOKE_NAME}-remote] added to $RCLONE_CONF." fi - -header "Union Remote (optional)" -read -rp "Add this spoke to a union remote for redundancy? [y/N]: " ADD_UNION -ADD_UNION="${ADD_UNION:-n}" -if [[ "${ADD_UNION,,}" == "y" ]]; then - read -rp "Union remote name [shared-union]: " UNION_NAME - UNION_NAME="${UNION_NAME:-shared-union}" - read -rp "Subfolder path on this spoke (e.g. books, leave blank for root): " UNION_PATH - echo "" - echo "Upstream access mode for this spoke:" - echo " 0) None - full read/write (default)" - echo " 1) :ro - read only" - echo " 2) :nc - no create (read/write existing, no new files)" - echo " 3) :writeback - writeback cache" - echo "" - read -rp "Choose [0-3]: " UNION_MODE - UNION_MODE="${UNION_MODE:-0}" - case "$UNION_MODE" in - 0) UPSTREAM_TAG="" ;; - 1) UPSTREAM_TAG=":ro" ;; - 2) UPSTREAM_TAG=":nc" ;; - 3) UPSTREAM_TAG=":writeback" ;; - *) warn "Invalid choice, defaulting to full read/write."; UPSTREAM_TAG="" ;; - esac - if [ -n "$UNION_PATH" ]; then - UPSTREAM="${SPOKE_NAME}-remote:${UNION_PATH}${UPSTREAM_TAG}" - else - UPSTREAM="${SPOKE_NAME}-remote:${UPSTREAM_TAG}" - fi - if grep -q "^\[${UNION_NAME}\]" "$RCLONE_CONF" 2>/dev/null; then - ALREADY=$(python3 - "$RCLONE_CONF" "$UNION_NAME" "${SPOKE_NAME}-remote:" <<'PYEOF' -import sys -path, section, prefix = sys.argv[1], sys.argv[2], sys.argv[3] -with open(path) as f: - lines = f.readlines() -in_section = False -for line in lines: - if line.strip() == f"[{section}]": - in_section = True - elif line.strip().startswith("["): - in_section = False - if in_section and line.startswith("upstreams =") and prefix in line: - print("yes") - sys.exit(0) -print("no") -PYEOF -) - if [ "$ALREADY" = "yes" ]; then - warn "Upstream for ${SPOKE_NAME}-remote already in union remote [${UNION_NAME}], skipping." - else - python3 - "$RCLONE_CONF" "$UNION_NAME" "$UPSTREAM" <<'PYEOF' -import sys -path, section, upstream = sys.argv[1], sys.argv[2], sys.argv[3] -with open(path) as f: - lines = f.readlines() -out = [] -in_section = False -for line in lines: - if line.strip() == f"[{section}]": - in_section = True - elif line.strip().startswith("["): - in_section = False - if in_section and line.startswith("upstreams ="): - line = line.rstrip() + " " + upstream + "\n" - out.append(line) -with open(path, "w") as f: - f.writelines(out) -PYEOF - info "Added '$UPSTREAM' to union remote [${UNION_NAME}]." - fi - else - [ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >> "$RCLONE_CONF" - printf '\n[%s]\ntype = union\nupstreams = %s\n' "$UNION_NAME" "$UPSTREAM" >> "$RCLONE_CONF" - info "Union remote [${UNION_NAME}] created with upstream '$UPSTREAM'." - fi -fi - -header "Testing rclone Connection" -if rclone lsd "${SPOKE_NAME}-remote:" --config "$RCLONE_CONF" 2>/dev/null; then - info "rclone connection to $SPOKE_NAME successful." -else - warn "rclone test failed. Check the remote config in $RCLONE_CONF." -fi - -header "Registering Spoke" -mkdir -p "$(dirname "$REGISTRY")" -MOUNT_POINT="${HOME}/mnt/${SPOKE_NAME}" -mkdir -p "$MOUNT_POINT" -if grep -q "^${SPOKE_NAME} " "$REGISTRY" 2>/dev/null; then - warn "$SPOKE_NAME already in registry, updating." - grep -v "^${SPOKE_NAME} " "$REGISTRY" > "${REGISTRY}.tmp" 2>/dev/null || true - mv "${REGISTRY}.tmp" "$REGISTRY" -fi -echo "${SPOKE_NAME} ${TUNNEL_PORT} ${KEY_PATH} ${MOUNT_POINT}" >> "$REGISTRY" -info "$SPOKE_NAME registered." - -header "Setting Up Auto-Mount" -MOUNT_CMD="rclone mount ${SPOKE_NAME}-remote: ${MOUNT_POINT} --config ${HOME}/.config/rclone/rclone.conf --vfs-cache-mode writes --allow-other --daemon" -CRON_ENTRY="@reboot ${MOUNT_CMD}" -EXISTING=$(crontab -l 2>/dev/null || true) -if echo "$EXISTING" | grep -qF "${SPOKE_NAME}-remote:"; then - warn "Crontab entry for ${SPOKE_NAME}-remote already exists, skipping." -else - CRONTAB_BACKUP="${HOME}/.config/tinyboard/crontab.$(date +%Y%m%d%H%M%S)" - mkdir -p "$(dirname "$CRONTAB_BACKUP")" - echo "$EXISTING" > "$CRONTAB_BACKUP" - info "Crontab backed up to $CRONTAB_BACKUP" - { echo "$EXISTING"; echo "$CRON_ENTRY"; } | crontab - - info "Auto-mount crontab entry added for ${SPOKE_NAME}." -fi -info "Starting mount now..." -mkdir -p "$MOUNT_POINT" -eval "$MOUNT_CMD" 2>/dev/null && info "Mounted ${SPOKE_NAME} at ${MOUNT_POINT}." || warn "Mount failed — will retry on next reboot." - -header "Onboarding Complete" -echo -e " Spoke: ${GREEN}$SPOKE_NAME${NC}" -echo -e " Port: ${GREEN}$TUNNEL_PORT${NC}" -echo -e " Hub key: ${GREEN}$KEY_PATH${NC}" -echo -e " rclone: ${GREEN}${SPOKE_NAME}-remote${NC}" -echo "" -echo -e "${YELLOW}To mount this spoke:${NC}" -echo " RCLONE_REMOTE=${SPOKE_NAME}-remote hubspoke-helper.sh hub start" -echo ""