diff --git a/hub/offboard-spoke.sh b/hub/offboard-spoke.sh new file mode 100644 index 0000000..e907264 --- /dev/null +++ b/hub/offboard-spoke.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -euo pipefail + +RCLONE_CONF="${HOME}/.config/rclone/rclone.conf" +SSH_DIR="${HOME}/.ssh" +REGISTRY="${HOME}/.config/tinyboard/spokes" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } +header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; } + +check_deps() { + local missing=() + for cmd in "$@"; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing+=("$cmd") + fi + done + if [ ${#missing[@]} -gt 0 ]; then + die "Missing required dependencies: ${missing[*]}" + fi +} + +if [ "$(id -u)" -eq 0 ]; then + die "Run as the hub user, not root." +fi + +check_deps rclone crontab fusermount python3 + +header "TinyBoard Hub — Offboard Spoke" + +[ -f "$REGISTRY" ] || die "No spoke registry found at $REGISTRY. No spokes to offboard." + +echo "Registered spokes:" +echo "" +awk '{print " " $1 " (port " $2 ", mount " $4 ")"}' "$REGISTRY" +echo "" + +read -rp "Spoke name to offboard: " SPOKE_NAME +[ -n "$SPOKE_NAME" ] || die "Spoke name cannot be empty" + +SPOKE_LINE=$(grep "^$SPOKE_NAME " "$REGISTRY" 2>/dev/null || true) +[ -n "$SPOKE_LINE" ] || die "Spoke '$SPOKE_NAME' not found in registry." + +TUNNEL_PORT=$(echo "$SPOKE_LINE" | awk '{print $2}') +KEY_PATH=$(echo "$SPOKE_LINE" | awk '{print $3}') +MOUNT_POINT=$(echo "$SPOKE_LINE" | awk '{print $4}') + +echo "" +echo -e " Spoke: ${YELLOW}$SPOKE_NAME${NC}" +echo -e " Port: ${YELLOW}$TUNNEL_PORT${NC}" +echo -e " Key: ${YELLOW}$KEY_PATH${NC}" +echo -e " Mount: ${YELLOW}$MOUNT_POINT${NC}" +echo "" +read -rp "Are you sure you want to offboard $SPOKE_NAME? [y/N]: " CONFIRM +[[ "${CONFIRM,,}" == "y" ]] || die "Aborted." + +header "Unmounting Spoke" +if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then + if fusermount -u "$MOUNT_POINT" 2>/dev/null; then + info "Unmounted $MOUNT_POINT." + else + warn "Could not unmount $MOUNT_POINT — may already be unmounted." + fi +else + warn "$MOUNT_POINT is not currently mounted." +fi + +header "Removing Crontab Entry" +EXISTING=$(crontab -l 2>/dev/null || true) +UPDATED=$(echo "$EXISTING" | grep -v "${SPOKE_NAME}-remote:" || true) +if [ "$EXISTING" = "$UPDATED" ]; then + warn "No crontab entry found for $SPOKE_NAME." +elif [ -z "$UPDATED" ]; then + crontab -r 2>/dev/null || true + info "Crontab entry for $SPOKE_NAME removed (crontab now empty)." +else + echo "$UPDATED" | crontab - + info "Crontab entry for $SPOKE_NAME removed." +fi + +header "Removing rclone Remote" +if grep -q "\[${SPOKE_NAME}-remote\]" "$RCLONE_CONF" 2>/dev/null; then + python3 - "$RCLONE_CONF" "$SPOKE_NAME" <<'PYEOF' +import sys +path, name = sys.argv[1], sys.argv[2] +lines = open(path).readlines() +out, skip = [], False +for line in lines: + if line.strip() == f"[{name}-remote]": + skip = True + elif skip and line.strip().startswith("["): + skip = False + if not skip: + out.append(line) +open(path, 'w').writelines(out) +PYEOF + info "rclone remote [${SPOKE_NAME}-remote] removed from $RCLONE_CONF." +else + warn "rclone remote [${SPOKE_NAME}-remote] not found in $RCLONE_CONF." +fi + +header "Removing SSH Key" +read -rp "Remove hub SSH key for $SPOKE_NAME ($KEY_PATH)? [y/N]: " REMOVE_KEY +if [[ "${REMOVE_KEY,,}" == "y" ]]; then + if [ -f "$KEY_PATH" ]; then + rm -f "$KEY_PATH" "$KEY_PATH.pub" + info "SSH key removed." + else + warn "Key not found at $KEY_PATH." + fi +else + info "SSH key left in place." +fi + +header "Removing from Registry" +(grep -v "^$SPOKE_NAME " "$REGISTRY" || true) > "${REGISTRY}.tmp" && mv "${REGISTRY}.tmp" "$REGISTRY" +info "$SPOKE_NAME removed from registry." + +header "Offboarding Complete" +echo -e " Spoke ${GREEN}$SPOKE_NAME${NC} has been offboarded." +echo "" diff --git a/hub/rclone.conf b/hub/rclone.conf deleted file mode 100644 index 840dd71..0000000 --- a/hub/rclone.conf +++ /dev/null @@ -1,18 +0,0 @@ -[brie-remote] -type = sftp -host = localhost -port = 11111 -key_file = /home/armbian/.ssh/armbian-brie-202604 -shell_type = unix -md5sum_command = md5sum -sha1sum_command = sha1sum - -#[new-remote] -#type = sftp -#host = localhost -#port = 11112 -#key_file = /home/armbian/.ssh/a new priv key for tunnel back to new spoke -#shell_type = unix -#md5sum_command = md5sum -#sha1sum_command = sha1sum - diff --git a/hubspoke-helper.sh b/hubspoke-helper.sh deleted file mode 100755 index b466bf2..0000000 --- a/hubspoke-helper.sh +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env bash -# -# hubspoke-helper.sh - Manage hub/spoke rclone mounts -# Assumes spoke Docker files exist in ~/autossh-tunnel/ -# Simplified hub mount uses direct rclone commands (no systemd services) - -set -euo pipefail - -# ------------------------------------------------------------ -# Configuration (override with env vars if needed) -# ------------------------------------------------------------ -TUNNEL_DIR="${TUNNEL_DIR:-$HOME/tinyboard/spoke}" -COMPOSE_FILE="${COMPOSE_FILE:-$TUNNEL_DIR/compose.yaml}" -RCLONE_REMOTE="${RCLONE_REMOTE:-brie-remote}" -MOUNT_POINT="${MOUNT_POINT:-/mnt/hub/$RCLONE_REMOTE}" - -# ------------------------------------------------------------ -# Usage -# ------------------------------------------------------------ -usage() { - cat <&2 - exit 1 -} - -# ------------------------------------------------------------ -# Spoke actions (docker) -# ------------------------------------------------------------ -spoke_build() { - if [ ! -f "$COMPOSE_FILE" ]; then - die "docker-compose.yaml not found at $COMPOSE_FILE" - fi - cd "$TUNNEL_DIR" - docker build --build-arg UID=$(id -u armbian) --build-arg GID=$(id -g armbian) -t spoke-autossh . - echo "Image built. Use '$0 spoke start' to run." -} - -spoke_start() { - cd "$TUNNEL_DIR" - docker-compose up -d -} - -spoke_stop() { - cd "$TUNNEL_DIR" - docker-compose down -} - -spoke_restart() { - cd "$TUNNEL_DIR" - docker-compose restart -} - -spoke_status() { - docker ps --filter name=spoke-autossh --format "table {{.Names}}\t{{.Status}}" -} - -spoke_logs() { - cd "$TUNNEL_DIR" - docker-compose logs --tail=50 -f -} - -spoke_show_cmd() { - cat </dev/null 2>&1 & - echo "Mount started in background (PID: $!)" - echo "Check status with: $0 hub status" -} - -hub_start_background() { - # Internal function for crontab/auto-start - mkdir -p "$MOUNT_POINT" - rclone mount "${RCLONE_REMOTE}:" "$MOUNT_POINT" \ - --config "${HOME}/.config/rclone/rclone.conf" \ - --vfs-cache-mode writes \ - --allow-other \ - --daemon -} - -hub_stop() { - echo "Stopping rclone mount..." - if hub_unmount; then - echo "Mount stopped." - else - echo "Could not unmount. Trying force unmount..." - fusermount -uz "$MOUNT_POINT" 2>/dev/null && echo "Force unmounted." || echo "Still could not unmount." - fi -} - -hub_status() { - if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then - echo "Mount point $MOUNT_POINT is mounted." - mount | grep "$MOUNT_POINT" - else - echo "Mount point $MOUNT_POINT is NOT mounted." - echo "Check if rclone process is running:" - pgrep -af rclone || echo "No rclone mount processes found." - fi -} - -hub_mount() { - mkdir -p "$MOUNT_POINT" - echo "Mounting in foreground. Press Ctrl+C to unmount." - rclone mount "${RCLONE_REMOTE}:" "$MOUNT_POINT" \ - --config "${HOME}/.config/rclone/rclone.conf" \ - --vfs-cache-mode writes \ - --allow-other -} - -hub_unmount() { - fusermount -u "$MOUNT_POINT" 2>/dev/null && echo "Unmounted." || echo "Not mounted." -} - -# ------------------------------------------------------------ -# Dispatch -# ------------------------------------------------------------ -if [ $# -lt 2 ]; then - usage - exit 1 -fi - -ROLE="$1" -ACTION="$2" - -case "$ROLE" in -spoke) - case "$ACTION" in - build) spoke_build ;; - start) spoke_start ;; - stop) spoke_stop ;; - restart) spoke_restart ;; - status) spoke_status ;; - logs) spoke_logs ;; - show-cmd) spoke_show_cmd ;; - *) die "Unknown action for spoke: $ACTION" ;; - esac - ;; -hub) - case "$ACTION" in - install) hub_install ;; - start) hub_start ;; - start-background) hub_start_background ;; - stop) hub_stop ;; - status) hub_status ;; - mount) hub_mount ;; - unmount) hub_unmount ;; - *) die "Unknown action for hub: $ACTION" ;; - esac - ;; -*) - usage - exit 1 - ;; -esac diff --git a/spoke/aptprimary.sh b/spoke/aptprimary.sh deleted file mode 100755 index ec78648..0000000 --- a/spoke/aptprimary.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# Need armbian-config? - -apt install -y vim -apt install -y autossh -apt install -y docker.io docker-cli docker-compose -usermod -aG docker armbian diff --git a/spoke/autohostname.sh b/spoke/autohostname.sh deleted file mode 100755 index 467dbc0..0000000 --- a/spoke/autohostname.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -# Copy this along with .not_logged_in_yet to armbian root dir, then run after successful login - -# Refresh: extract MAC address of wlan0 -MAC=$(netplan status -f json | jq -r '.wlan0.macaddress') - -# Check that we actually got a MAC address -if [[ -z "$MAC" ]]; then - echo "Error: Could not retrieve MAC address from netplan." >&2 - exit 1 -fi - -echo "Detected MAC address: $MAC" - -# Assign cheese hostname based on MAC address -case "$MAC" in -38:9c:80:46:26:c8) # ← Replace with your first real MAC - HOSTNAME="brie" - ;; -68:f8:ea:22:e1:3d) # ← Replace with your second real MAC - HOSTNAME="gouda" - ;; -99:88:77:66:55:44) # ← Replace with your third real MAC - HOSTNAME="camembert" - ;; -*) - echo "Unknown MAC address: $MAC ... hostname not changed." >&2 - exit 1 - ;; -esac - -echo "Setting hostname to: $HOSTNAME" -sudo hostnamectl set-hostname "$HOSTNAME" - -# Optional: also update /etc/hostname (hostnamectl usually does this, but to be safe) -echo "$HOSTNAME" | sudo tee /etc/hostname >/dev/null - -echo "Hostname changed. Reboot or start a new shell to see the change." diff --git a/spoke/clean_sensitive.sh b/spoke/clean_sensitive.sh deleted file mode 100755 index 34d5f40..0000000 --- a/spoke/clean_sensitive.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# Script to clean sensitive WiFi credentials and passwords from configuration files -# Usage: ./clean_sensitive.sh [filename] - -FILE="${1:-/home/finn/code/tinyboard/armb-not_logged_in_yet}" - -echo "Cleaning sensitive data from: $FILE" - -# Clean WiFi SSID (both commented and uncommented lines) -sed -i 's/^\(#*PRESET_NET_WIFI_SSID=\).*$/\1"[REDACTED]"/' "$FILE" - -# Clean WiFi KEY (both commented and uncommented lines) -sed -i 's/^\(#*PRESET_NET_WIFI_KEY=\).*$/\1"[REDACTED]"/' "$FILE" - -# Clean root password -sed -i 's/^\(PRESET_ROOT_PASSWORD=\).*$/\1"[REDACTED]"/' "$FILE" - -# Clean user password -sed -i 's/^\(PRESET_USER_PASSWORD=\).*$/\1"[REDACTED]"/' "$FILE" - -echo "wiped fields"