forked from finn/tinyboard
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dea8d2df4d | |||
| 8a7fe7b4de | |||
| e3a12c0f6e | |||
| a8b10a1814 | |||
| 92a2af8c5c | |||
| a5cf3d1f8b | |||
| ebb366e4bc | |||
| 84b3b7ce1d | |||
| 86688c43c7 | |||
| 972dbef11c | |||
| e55ab898ef | |||
| 81d0bebd5e | |||
| 6fe164a6ae | |||
| b76e890857 | |||
| 4e2f17266a | |||
| 0af3c30f79 | |||
| b0e63a2e01 | |||
| 22eced7607 | |||
| 58c641d603 | |||
| 1cc50f8ff0 | |||
| 97aff6a741 | |||
| eaff38477c | |||
| e2ed499e58 | |||
| 48ba67e351 | |||
| 5a9e55b673 | |||
| e5bdf95dcf | |||
| 0553420d04 | |||
| 4cdddd649d | |||
| 0fd7d94d58 | |||
| f3c9cf2344 | |||
| f486795154 | |||
| fe3f2c5b77 | |||
| 4e1e9282ac | |||
| 07f4601bad | |||
| 9bdd12ebbd |
@@ -35,6 +35,24 @@ cd tinyboard
|
|||||||
./setup.sh # option 1 (configure new spoke)
|
./setup.sh # option 1 (configure new spoke)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Adding the Spoke's Public Key to the Hub
|
||||||
|
|
||||||
|
During `setup-spoke.sh`, a key pair is generated on the spoke for the autossh tunnel. The script will display the public key and pause. Before pressing ENTER, the hub owner must add the public key to the hub user's `authorized_keys`. Run this on the hub as the hub user (e.g. `armbian`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "<paste public key here>" >> ~/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
Or as root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "<paste public key here>" >> /home/armbian/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the key is added, press ENTER on the spoke to continue. The script will test the SSH connection and if successful, bring up the tunnel.
|
||||||
|
|
||||||
|
The private key never leaves the spoke — only the public key is shared.
|
||||||
|
|
||||||
### Onboarding a Spoke from the Hub
|
### Onboarding a Spoke from the Hub
|
||||||
|
|
||||||
Once the spoke tunnel is up, run on the hub:
|
Once the spoke tunnel is up, run on the hub:
|
||||||
|
|||||||
Executable
+214
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
ok() { echo -e " ${GREEN}[OK]${NC} $*"; }
|
||||||
|
fail() { echo -e " ${RED}[FAIL]${NC} $*"; }
|
||||||
|
warn() { echo -e " ${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
HUB_USER="${SUDO_USER:-${USER}}"
|
||||||
|
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ]; then
|
||||||
|
HUB_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
|
||||||
|
else
|
||||||
|
HUB_HOME="$HOME"
|
||||||
|
fi
|
||||||
|
if [ "$(id -u)" -eq 0 ] && [ "$HUB_HOME" = "/root" ]; then
|
||||||
|
for u in armbian; do
|
||||||
|
CANDIDATE=$(getent passwd "$u" 2>/dev/null | cut -d: -f6)
|
||||||
|
if [ -f "${CANDIDATE}/.config/tinyboard/spokes" ]; then
|
||||||
|
HUB_HOME="$CANDIDATE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
REGISTRY="${HUB_HOME}/.config/tinyboard/spokes"
|
||||||
|
COMPOSE="$SCRIPT_DIR/spoke/compose.yaml"
|
||||||
|
RCLONE_CONF="${HUB_HOME}/.config/rclone/rclone.conf"
|
||||||
|
|
||||||
|
IS_SPOKE=false
|
||||||
|
IS_HUB=false
|
||||||
|
|
||||||
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qi autossh; then
|
||||||
|
IS_SPOKE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$REGISTRY" ]; then
|
||||||
|
IS_HUB=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$IS_SPOKE" = false ] && [ "$IS_HUB" = false ]; then
|
||||||
|
echo -e "${YELLOW}Could not detect hub or spoke configuration. Is tinyboard set up?${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_common() {
|
||||||
|
header "System"
|
||||||
|
|
||||||
|
local ssh_svc=""
|
||||||
|
if systemctl list-unit-files ssh.service >/dev/null 2>&1; then
|
||||||
|
ssh_svc="ssh"
|
||||||
|
elif systemctl list-unit-files sshd.service >/dev/null 2>&1; then
|
||||||
|
ssh_svc="sshd"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$ssh_svc" ]; then
|
||||||
|
if systemctl is-active "$ssh_svc" >/dev/null 2>&1; then
|
||||||
|
ok "SSH server running ($ssh_svc)"
|
||||||
|
else
|
||||||
|
fail "SSH server not running ($ssh_svc)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not detect SSH service"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$IS_SPOKE" = true ]; then
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
ok "docker installed"
|
||||||
|
else
|
||||||
|
fail "docker not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
ok "docker daemon running"
|
||||||
|
else
|
||||||
|
fail "docker daemon not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local st_container
|
||||||
|
st_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i syncthing | head -1 || true)
|
||||||
|
if [ -n "$st_container" ]; then
|
||||||
|
ok "Syncthing container running ($st_container)"
|
||||||
|
if curl -sf http://127.0.0.1:8384 >/dev/null 2>&1; then
|
||||||
|
ok "Syncthing API reachable"
|
||||||
|
else
|
||||||
|
warn "Syncthing container running but API not reachable on :8384"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "No Syncthing container running"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_spoke() {
|
||||||
|
header "Spoke"
|
||||||
|
|
||||||
|
local autossh_container
|
||||||
|
autossh_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i autossh | head -1 || true)
|
||||||
|
if [ -n "$autossh_container" ]; then
|
||||||
|
ok "autossh container running ($autossh_container)"
|
||||||
|
|
||||||
|
local logs
|
||||||
|
logs=$(docker logs "$autossh_container" 2>&1 | tail -20 || true)
|
||||||
|
if echo "$logs" | grep -q "remote port forwarding failed"; then
|
||||||
|
fail "Tunnel reports port forwarding failed — check hub authorized_keys"
|
||||||
|
else
|
||||||
|
ok "No tunnel errors in recent logs"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "No autossh container running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$autossh_container" ]; then
|
||||||
|
local tunnel_port hub_host
|
||||||
|
tunnel_port=$(docker inspect "$autossh_container" 2>/dev/null | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
cmd = ' '.join(data[0].get('Config', {}).get('Cmd', []))
|
||||||
|
import re
|
||||||
|
m = re.search(r'-R (\d+):localhost', cmd)
|
||||||
|
print(m.group(1) if m else '')
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
hub_host=$(docker inspect "$autossh_container" 2>/dev/null | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
cmd = ' '.join(data[0].get('Config', {}).get('Cmd', []))
|
||||||
|
import re
|
||||||
|
m = re.search(r'[a-zA-Z0-9._-]+@([a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)', cmd)
|
||||||
|
print(m.group(1) if m else '')
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
if [ -n "$tunnel_port" ] && [ -n "$hub_host" ]; then
|
||||||
|
ok "Tunnel configured: port $tunnel_port → $hub_host"
|
||||||
|
else
|
||||||
|
warn "Could not parse tunnel config from running container"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local st_data="/home/armbian/st/data"
|
||||||
|
if [ -d "$st_data" ]; then
|
||||||
|
ok "Syncthing data directory exists ($st_data)"
|
||||||
|
else
|
||||||
|
warn "Syncthing data directory not found ($st_data)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_hub() {
|
||||||
|
header "Hub"
|
||||||
|
|
||||||
|
local spoke_count
|
||||||
|
spoke_count=$(wc -l < "$REGISTRY" 2>/dev/null || echo 0)
|
||||||
|
ok "$spoke_count spoke(s) in registry"
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
local spoke_name tunnel_port key_path mount_point
|
||||||
|
spoke_name=$(echo "$line" | awk '{print $1}')
|
||||||
|
tunnel_port=$(echo "$line" | awk '{print $2}')
|
||||||
|
key_path=$(echo "$line" | awk '{print $3}')
|
||||||
|
mount_point=$(echo "$line" | awk '{print $4}')
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${CYAN}Spoke: $spoke_name${NC}"
|
||||||
|
|
||||||
|
if ss -tlnp 2>/dev/null | grep -q ":${tunnel_port}"; then
|
||||||
|
ok "Tunnel port $tunnel_port is listening"
|
||||||
|
else
|
||||||
|
fail "Tunnel port $tunnel_port not listening — is the spoke connected?"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$key_path" ]; then
|
||||||
|
ok "Hub key exists ($key_path)"
|
||||||
|
else
|
||||||
|
fail "Hub key missing ($key_path)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mountpoint -q "$mount_point" 2>/dev/null; then
|
||||||
|
ok "Mounted at $mount_point"
|
||||||
|
else
|
||||||
|
fail "Not mounted at $mount_point"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "\[${spoke_name}-remote\]" "$RCLONE_CONF" 2>/dev/null; then
|
||||||
|
ok "rclone remote [${spoke_name}-remote] configured"
|
||||||
|
else
|
||||||
|
fail "rclone remote [${spoke_name}-remote] not found in rclone.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if crontab -u "$(basename "$HUB_HOME")" -l 2>/dev/null | grep -q "${spoke_name}-remote:"; then
|
||||||
|
ok "Auto-mount crontab entry present"
|
||||||
|
else
|
||||||
|
warn "No auto-mount crontab entry for $spoke_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
done < "$REGISTRY"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
check_common
|
||||||
|
|
||||||
|
if [ "$IS_SPOKE" = true ]; then
|
||||||
|
check_spoke
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$IS_HUB" = true ]; then
|
||||||
|
check_hub
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
+143
-119
@@ -11,44 +11,51 @@ YELLOW='\033[1;33m'
|
|||||||
CYAN='\033[0;36m'
|
CYAN='\033[0;36m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
die() {
|
||||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
header() {
|
||||||
|
echo -e "\n${CYAN}══════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} $*${NC}"
|
||||||
|
echo -e "${CYAN}══════════════════════════════════════════${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
check_deps() {
|
check_deps() {
|
||||||
local missing=()
|
local missing=()
|
||||||
for cmd in "$@"; do
|
for cmd in "$@"; do
|
||||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
missing+=("$cmd")
|
missing+=("$cmd")
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ ${#missing[@]} -gt 0 ]; then
|
|
||||||
die "Missing required dependencies: ${missing[*]}"
|
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
|
if [ ${#missing[@]} -gt 0 ]; then
|
||||||
|
die "Missing required dependencies: ${missing[*]}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
retry_or_abort() {
|
retry_or_abort() {
|
||||||
local test_cmd="$1"
|
local test_cmd="$1"
|
||||||
local fail_msg="$2"
|
local fail_msg="$2"
|
||||||
while true; do
|
while true; do
|
||||||
if eval "$test_cmd" 2>/dev/null; then
|
if eval "$test_cmd" 2>/dev/null; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
warn "$fail_msg"
|
warn "$fail_msg"
|
||||||
echo -e " ${YELLOW}[R]${NC} Retry ${RED}[A]${NC} Abort"
|
echo -e " ${YELLOW}[R]${NC} Retry ${RED}[A]${NC} Abort"
|
||||||
read -rp "Choice: " CHOICE
|
read -rp "Choice: " CHOICE
|
||||||
case "${CHOICE,,}" in
|
case "${CHOICE,,}" in
|
||||||
r) info "Retrying..." ;;
|
r) info "Retrying..." ;;
|
||||||
a) die "Aborted." ;;
|
a) die "Aborted." ;;
|
||||||
*) warn "Press R to retry or A to abort." ;;
|
*) warn "Press R to retry or A to abort." ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
die "Running as root — keys will be written to /root/.ssh. Run as the hub user instead."
|
die "Running as root — keys will be written to /root/.ssh. Run as the hub user instead."
|
||||||
fi
|
fi
|
||||||
mkdir -p "$SSH_DIR"
|
mkdir -p "$SSH_DIR"
|
||||||
touch "$SSH_DIR/known_hosts"
|
touch "$SSH_DIR/known_hosts"
|
||||||
@@ -74,102 +81,100 @@ KEY_PATH="$SSH_DIR/$KEY_NAME"
|
|||||||
mkdir -p "$(dirname "$RCLONE_CONF")"
|
mkdir -p "$(dirname "$RCLONE_CONF")"
|
||||||
|
|
||||||
header "Checking Tunnel"
|
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..."
|
info "Scanning spoke host key..."
|
||||||
KEYSCAN=$(ssh-keyscan -p "$TUNNEL_PORT" -H localhost 2>/dev/null)
|
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?"
|
[ -n "$KEYSCAN" ] || die "Spoke not reachable on port $TUNNEL_PORT — is the tunnel up?"
|
||||||
while IFS= read -r KEYSCAN_LINE; do
|
while IFS= read -r KEYSCAN_LINE; do
|
||||||
KEYSCAN_KEY=$(echo "$KEYSCAN_LINE" | awk '{print $2, $3}')
|
KEYSCAN_KEY=$(echo "$KEYSCAN_LINE" | awk '{print $2, $3}')
|
||||||
if ! grep -qF "$KEYSCAN_KEY" "$SSH_DIR/known_hosts" 2>/dev/null; then
|
if ! grep -qF "$KEYSCAN_KEY" "$SSH_DIR/known_hosts" 2>/dev/null; then
|
||||||
echo "$KEYSCAN_LINE" >> "$SSH_DIR/known_hosts"
|
echo "$KEYSCAN_LINE" >>"$SSH_DIR/known_hosts"
|
||||||
fi
|
fi
|
||||||
done <<< "$KEYSCAN"
|
done <<<"$KEYSCAN"
|
||||||
|
|
||||||
info "Verifying spoke is reachable on port $TUNNEL_PORT..."
|
header "Generating Hub-to-Spoke Access Key"
|
||||||
retry_or_abort \
|
|
||||||
"ssh -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"
|
|
||||||
if [ -f "$KEY_PATH" ]; then
|
if [ -f "$KEY_PATH" ]; then
|
||||||
warn "Key $KEY_PATH already exists, skipping generation."
|
warn "Key $KEY_PATH already exists, skipping generation."
|
||||||
else
|
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"
|
info "Key generated: $KEY_PATH"
|
||||||
fi
|
fi
|
||||||
chmod 600 "$KEY_PATH"
|
chmod 600 "$KEY_PATH"
|
||||||
info "Permissions set: $KEY_PATH is 600"
|
info "Permissions set: $KEY_PATH is 600"
|
||||||
|
|
||||||
header "Copying Hub Key to Spoke"
|
header "Installing Hub-to-Spoke Access Key on Spoke"
|
||||||
info "Running ssh-copy-id to $SPOKE_USER@localhost:$TUNNEL_PORT..."
|
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)"
|
info "(You will be prompted for the $SPOKE_USER password on the spoke)"
|
||||||
if ssh-copy-id -i "$KEY_PATH.pub" -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."
|
info "Key copied."
|
||||||
else
|
else
|
||||||
warn "ssh-copy-id failed — password auth may be disabled on the spoke."
|
warn "ssh-copy-id failed — password auth may be disabled on the spoke."
|
||||||
warn "Manually append the hub public key to the spoke's authorized_keys:"
|
warn "Manually append the hub public key to the spoke's authorized_keys:"
|
||||||
echo ""
|
echo ""
|
||||||
echo " cat $KEY_PATH.pub"
|
echo " cat $KEY_PATH.pub"
|
||||||
echo " Then on the spoke, append the output to:"
|
echo " Then on the spoke, append the output to:"
|
||||||
echo " /home/$SPOKE_USER/.ssh/authorized_keys"
|
echo " /home/$SPOKE_USER/.ssh/authorized_keys"
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "Press ENTER once the key has been added to the spoke..."
|
read -rp "Press ENTER once the key has been added to the spoke..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
header "Testing Hub -> Spoke Key Auth"
|
header "Testing Hub-to-Spoke Key Auth"
|
||||||
retry_or_abort \
|
retry_or_abort \
|
||||||
"ssh -i \"$KEY_PATH\" -o BatchMode=yes -o ConnectTimeout=10 -p \"$TUNNEL_PORT\" \"$SPOKE_USER\"@localhost exit" \
|
"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."
|
"Key auth failed. Check authorized_keys on the spoke."
|
||||||
info "Key auth to spoke successful."
|
info "Key auth to spoke successful."
|
||||||
|
|
||||||
header "Adding rclone Remote"
|
header "Adding rclone Remote"
|
||||||
if grep -q "\[${SPOKE_NAME}-remote\]" "$RCLONE_CONF" 2>/dev/null; then
|
if grep -q "\[${SPOKE_NAME}-remote\]" "$RCLONE_CONF" 2>/dev/null; then
|
||||||
warn "Remote [${SPOKE_NAME}-remote] already exists in $RCLONE_CONF, skipping."
|
warn "Remote [${SPOKE_NAME}-remote] already exists in $RCLONE_CONF, skipping."
|
||||||
else
|
else
|
||||||
[ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >> "$RCLONE_CONF"
|
[ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >>"$RCLONE_CONF"
|
||||||
cat >> "$RCLONE_CONF" <<EOF
|
python3 - "$RCLONE_CONF" "$SPOKE_NAME" "$TUNNEL_PORT" "$KEY_PATH" <<'PYEOF'
|
||||||
|
import sys
|
||||||
[${SPOKE_NAME}-remote]
|
path, name, port, key = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
||||||
type = sftp
|
with open(path, 'a') as f:
|
||||||
host = localhost
|
f.write(f"\n[{name}-remote]\ntype = sftp\nhost = localhost\nport = {port}\nkey_file = {key}\nshell_type = unix\nmd5sum_command = md5sum\nsha1sum_command = sha1sum\n")
|
||||||
port = $TUNNEL_PORT
|
PYEOF
|
||||||
key_file = $KEY_PATH
|
info "Remote [${SPOKE_NAME}-remote] added to $RCLONE_CONF."
|
||||||
shell_type = unix
|
|
||||||
md5sum_command = md5sum
|
|
||||||
sha1sum_command = sha1sum
|
|
||||||
EOF
|
|
||||||
info "Remote [${SPOKE_NAME}-remote] added to $RCLONE_CONF."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
header "Union Remote (optional)"
|
header "Union Remote (optional)"
|
||||||
read -rp "Add this spoke to a union remote for redundancy? [y/N]: " ADD_UNION
|
read -rp "Add this spoke to a union remote for redundancy? [y/N]: " ADD_UNION
|
||||||
ADD_UNION="${ADD_UNION:-n}"
|
ADD_UNION="${ADD_UNION:-n}"
|
||||||
if [[ "${ADD_UNION,,}" == "y" ]]; then
|
if [[ "${ADD_UNION,,}" == "y" ]]; then
|
||||||
read -rp "Union remote name [shared-union]: " UNION_NAME
|
read -rp "Union remote name [shared-union]: " UNION_NAME
|
||||||
UNION_NAME="${UNION_NAME:-shared-union}"
|
UNION_NAME="${UNION_NAME:-shared-union}"
|
||||||
read -rp "Subfolder path on this spoke (e.g. books, leave blank for root): " UNION_PATH
|
read -rp "Subfolder path on the spoke being onboarded (e.g. books, leave blank for root): " UNION_PATH
|
||||||
echo ""
|
echo ""
|
||||||
echo "Upstream access mode for this spoke:"
|
echo "Upstream access mode for this spoke:"
|
||||||
echo " 0) None - full read/write (default)"
|
echo " 0) None - full read/write (default)"
|
||||||
echo " 1) :ro - read only"
|
echo " 1) :ro - read only"
|
||||||
echo " 2) :nc - no create (read/write existing, no new files)"
|
echo " 2) :nc - no create (read/write existing, no new files)"
|
||||||
echo " 3) :writeback - writeback cache"
|
echo " 3) :writeback - writeback cache"
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "Choose [0-3]: " UNION_MODE
|
read -rp "Choose [0-3]: " UNION_MODE
|
||||||
UNION_MODE="${UNION_MODE:-0}"
|
UNION_MODE="${UNION_MODE:-0}"
|
||||||
case "$UNION_MODE" in
|
case "$UNION_MODE" in
|
||||||
0) UPSTREAM_TAG="" ;;
|
0) UPSTREAM_TAG="" ;;
|
||||||
1) UPSTREAM_TAG=":ro" ;;
|
1) UPSTREAM_TAG=":ro" ;;
|
||||||
2) UPSTREAM_TAG=":nc" ;;
|
2) UPSTREAM_TAG=":nc" ;;
|
||||||
3) UPSTREAM_TAG=":writeback" ;;
|
3) UPSTREAM_TAG=":writeback" ;;
|
||||||
*) warn "Invalid choice, defaulting to full read/write."; UPSTREAM_TAG="" ;;
|
*)
|
||||||
esac
|
warn "Invalid choice, defaulting to full read/write."
|
||||||
if [ -n "$UNION_PATH" ]; then
|
UPSTREAM_TAG=""
|
||||||
UPSTREAM="${SPOKE_NAME}-remote:${UNION_PATH}${UPSTREAM_TAG}"
|
;;
|
||||||
else
|
esac
|
||||||
UPSTREAM="${SPOKE_NAME}-remote:${UPSTREAM_TAG}"
|
if [ -n "$UNION_PATH" ]; then
|
||||||
fi
|
UPSTREAM="${SPOKE_NAME}-remote:${UNION_PATH}${UPSTREAM_TAG}"
|
||||||
if grep -q "^\[${UNION_NAME}\]" "$RCLONE_CONF" 2>/dev/null; then
|
else
|
||||||
ALREADY=$(python3 - "$RCLONE_CONF" "$UNION_NAME" "${SPOKE_NAME}-remote:" <<'PYEOF'
|
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:" <<'PYEOF2'
|
||||||
import sys
|
import sys
|
||||||
path, section, prefix = sys.argv[1], sys.argv[2], sys.argv[3]
|
path, section, prefix = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
@@ -184,12 +189,12 @@ for line in lines:
|
|||||||
print("yes")
|
print("yes")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
print("no")
|
print("no")
|
||||||
PYEOF
|
PYEOF2
|
||||||
)
|
)
|
||||||
if [ "$ALREADY" = "yes" ]; then
|
if [ "$ALREADY" = "yes" ]; then
|
||||||
warn "Upstream for ${SPOKE_NAME}-remote already in union remote [${UNION_NAME}], skipping."
|
warn "Upstream for ${SPOKE_NAME}-remote already in union remote [${UNION_NAME}], skipping."
|
||||||
else
|
else
|
||||||
python3 - "$RCLONE_CONF" "$UNION_NAME" "$UPSTREAM" <<'PYEOF'
|
python3 - "$RCLONE_CONF" "$UNION_NAME" "$UPSTREAM" <<'PYEOF2'
|
||||||
import sys
|
import sys
|
||||||
path, section, upstream = sys.argv[1], sys.argv[2], sys.argv[3]
|
path, section, upstream = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
@@ -206,21 +211,21 @@ for line in lines:
|
|||||||
out.append(line)
|
out.append(line)
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
f.writelines(out)
|
f.writelines(out)
|
||||||
PYEOF
|
PYEOF2
|
||||||
info "Added '$UPSTREAM' to union remote [${UNION_NAME}]."
|
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
|
||||||
|
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
|
fi
|
||||||
|
|
||||||
header "Testing rclone Connection"
|
header "Testing rclone Connection"
|
||||||
if rclone lsd "${SPOKE_NAME}-remote:" --config "$RCLONE_CONF" 2>/dev/null; then
|
if rclone lsd "${SPOKE_NAME}-remote:" --config "$RCLONE_CONF" 2>/dev/null; then
|
||||||
info "rclone connection to $SPOKE_NAME successful."
|
info "rclone connection to $SPOKE_NAME successful."
|
||||||
else
|
else
|
||||||
warn "rclone test failed. Check the remote config in $RCLONE_CONF."
|
warn "rclone test failed. Check the remote config in $RCLONE_CONF."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
header "Registering Spoke"
|
header "Registering Spoke"
|
||||||
@@ -228,19 +233,38 @@ mkdir -p "$(dirname "$REGISTRY")"
|
|||||||
MOUNT_POINT="${HOME}/mnt/${SPOKE_NAME}"
|
MOUNT_POINT="${HOME}/mnt/${SPOKE_NAME}"
|
||||||
mkdir -p "$MOUNT_POINT"
|
mkdir -p "$MOUNT_POINT"
|
||||||
if grep -q "^${SPOKE_NAME} " "$REGISTRY" 2>/dev/null; then
|
if grep -q "^${SPOKE_NAME} " "$REGISTRY" 2>/dev/null; then
|
||||||
warn "$SPOKE_NAME already in registry, updating."
|
warn "$SPOKE_NAME already in registry, updating."
|
||||||
grep -v "^${SPOKE_NAME} " "$REGISTRY" > "${REGISTRY}.tmp" 2>/dev/null || true
|
grep -v "^${SPOKE_NAME} " "$REGISTRY" >"${REGISTRY}.tmp" 2>/dev/null || true
|
||||||
mv "${REGISTRY}.tmp" "$REGISTRY"
|
mv "${REGISTRY}.tmp" "$REGISTRY"
|
||||||
fi
|
fi
|
||||||
echo "${SPOKE_NAME} ${TUNNEL_PORT} ${KEY_PATH} ${MOUNT_POINT}" >> "$REGISTRY"
|
echo "${SPOKE_NAME} ${TUNNEL_PORT} ${KEY_PATH} ${MOUNT_POINT}" >>"$REGISTRY"
|
||||||
info "$SPOKE_NAME registered."
|
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"
|
||||||
|
MOUNT_CMD="sleep 55 && rclone mount ${SPOKE_NAME}-remote: ${MOUNT_POINT} --config ${HOME}/.config/rclone/rclone.conf --vfs-cache-mode full --vfs-cache-max-size 2G --vfs-read-ahead 256M --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"
|
header "Onboarding Complete"
|
||||||
echo -e " Spoke: ${GREEN}$SPOKE_NAME${NC}"
|
echo -e " Spoke: ${GREEN}$SPOKE_NAME${NC}"
|
||||||
echo -e " Port: ${GREEN}$TUNNEL_PORT${NC}"
|
echo -e " Port: ${GREEN}$TUNNEL_PORT${NC}"
|
||||||
echo -e " Hub key: ${GREEN}$KEY_PATH${NC}"
|
echo -e " Hub key: ${GREEN}$KEY_PATH${NC}"
|
||||||
echo -e " rclone: ${GREEN}${SPOKE_NAME}-remote${NC}"
|
echo -e " rclone: ${GREEN}${SPOKE_NAME}-remote${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}To mount this spoke:${NC}"
|
|
||||||
echo " RCLONE_REMOTE=${SPOKE_NAME}-remote hubspoke-helper.sh hub start"
|
|
||||||
echo ""
|
|
||||||
|
|||||||
+5
-5
@@ -63,7 +63,7 @@ if command -v apt-get >/dev/null 2>&1; then
|
|||||||
PKG_MANAGER="apt"
|
PKG_MANAGER="apt"
|
||||||
PKG_INSTALL="apt-get install -y -q"
|
PKG_INSTALL="apt-get install -y -q"
|
||||||
OPENSSH_PKG="openssh-server"
|
OPENSSH_PKG="openssh-server"
|
||||||
FUSE_PKG="fuse"
|
FUSE_PKG="fuse3"
|
||||||
info "Detected: apt (Debian/Ubuntu)"
|
info "Detected: apt (Debian/Ubuntu)"
|
||||||
apt-get update -q
|
apt-get update -q
|
||||||
elif command -v dnf >/dev/null 2>&1; then
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
@@ -136,7 +136,7 @@ header "SSH Server Configuration"
|
|||||||
SSHD_CONF="/etc/ssh/sshd_config"
|
SSHD_CONF="/etc/ssh/sshd_config"
|
||||||
[ -f "$SSHD_CONF" ] || die "sshd_config not found at $SSHD_CONF"
|
[ -f "$SSHD_CONF" ] || die "sshd_config not found at $SSHD_CONF"
|
||||||
|
|
||||||
for DIRECTIVE in "GatewayPorts no" "AllowTcpForwarding local" "ClientAliveInterval 60" "ClientAliveCountMax 3"; do
|
for DIRECTIVE in "GatewayPorts no" "AllowTcpForwarding yes" "ClientAliveInterval 60" "ClientAliveCountMax 3"; do
|
||||||
KEY="${DIRECTIVE%% *}"
|
KEY="${DIRECTIVE%% *}"
|
||||||
if grep -q "^$KEY" "$SSHD_CONF"; then
|
if grep -q "^$KEY" "$SSHD_CONF"; then
|
||||||
sed -i "s|^$KEY.*|$DIRECTIVE|" "$SSHD_CONF"
|
sed -i "s|^$KEY.*|$DIRECTIVE|" "$SSHD_CONF"
|
||||||
@@ -161,8 +161,8 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
header "Password Authentication"
|
header "Password Authentication"
|
||||||
read -rp "Disable password auth for $HUB_USER and use keys only? [Y/n]: " DISABLE_PASS
|
read -rp "Disable password auth for $HUB_USER and use keys only? [y/N]: " DISABLE_PASS
|
||||||
DISABLE_PASS="${DISABLE_PASS:-y}"
|
DISABLE_PASS="${DISABLE_PASS:-n}"
|
||||||
if [[ "${DISABLE_PASS,,}" == "y" ]]; then
|
if [[ "${DISABLE_PASS,,}" == "y" ]]; then
|
||||||
if [ ! -s "$SSH_DIR/authorized_keys" ]; then
|
if [ ! -s "$SSH_DIR/authorized_keys" ]; then
|
||||||
warn "No keys found in $SSH_DIR/authorized_keys — skipping password auth disable to avoid lockout."
|
warn "No keys found in $SSH_DIR/authorized_keys — skipping password auth disable to avoid lockout."
|
||||||
@@ -251,7 +251,7 @@ info "Mount point created at $MOUNT_POINT."
|
|||||||
|
|
||||||
header "Hub Setup Complete"
|
header "Hub Setup Complete"
|
||||||
echo -e " Hub user: ${GREEN}$HUB_USER${NC}"
|
echo -e " Hub user: ${GREEN}$HUB_USER${NC}"
|
||||||
echo -e " SSH config: ${GREEN}GatewayPorts no, AllowTcpForwarding local, ClientAliveInterval 60${NC}"
|
echo -e " SSH config: ${GREEN}GatewayPorts no, AllowTcpForwarding yes, ClientAliveInterval 60${NC}"
|
||||||
echo -e " FUSE: ${GREEN}user_allow_other enabled${NC}"
|
echo -e " FUSE: ${GREEN}user_allow_other enabled${NC}"
|
||||||
echo -e " rclone config: ${GREEN}$RCLONE_CONF${NC}"
|
echo -e " rclone config: ${GREEN}$RCLONE_CONF${NC}"
|
||||||
echo -e " Mount point: ${GREEN}$MOUNT_POINT${NC}"
|
echo -e " Mount point: ${GREEN}$MOUNT_POINT${NC}"
|
||||||
|
|||||||
+7
-10
@@ -11,25 +11,22 @@ services:
|
|||||||
-o "ServerAliveInterval=60"
|
-o "ServerAliveInterval=60"
|
||||||
-o "ServerAliveCountMax=3"
|
-o "ServerAliveCountMax=3"
|
||||||
-R 11111:localhost:22
|
-R 11111:localhost:22
|
||||||
-i /home/armbian/.ssh/oilykey2026
|
-i /home/armbian/.ssh/hubkey
|
||||||
armbian@oily.dad
|
armbian@hub.example.com
|
||||||
volumes:
|
volumes:
|
||||||
- /home/armbian/.ssh/oilykey2026:/home/armbian/.ssh/oilykey2026:ro
|
- /home/armbian/.ssh/hubkey:/home/armbian/.ssh/hubkey:ro
|
||||||
- /home/armbian/.ssh/known_hosts:/home/armbian/.ssh/known_hosts:ro
|
- /home/armbian/.ssh/known_hosts:/home/armbian/.ssh/known_hosts:ro
|
||||||
syncthing:
|
syncthing:
|
||||||
image: syncthing/syncthing
|
image: syncthing/syncthing
|
||||||
container_name: spoke-syncthing
|
container_name: spoke-syncthing
|
||||||
hostname: spoke-syncthing
|
hostname: spoke-syncthing
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
user: "1000:1000"
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000
|
- HOME=/var/syncthing
|
||||||
- PGID=1000
|
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8384:8384"
|
- "8384:8384"
|
||||||
- "22000:22000"
|
- "22000:22000"
|
||||||
volumes:
|
volumes:
|
||||||
- syncthing-config:/var/syncthing/config
|
- /home/armbian/st/config:/var/syncthing/config
|
||||||
- /home/armbian/st/data:/var/syncthing/data
|
- /home/armbian/st/data:/var/syncthing/data
|
||||||
|
|
||||||
volumes:
|
|
||||||
syncthing-config:
|
|
||||||
|
|||||||
+29
-2
@@ -32,6 +32,8 @@ header "TinyBoard Network Setup"
|
|||||||
echo ""
|
echo ""
|
||||||
echo " 0) Change hostname"
|
echo " 0) Change hostname"
|
||||||
echo " 1) Configure static IP"
|
echo " 1) Configure static IP"
|
||||||
|
echo " 2) Prefer IPv4 over IPv6"
|
||||||
|
echo " 3) Prefer IPv6 over IPv4"
|
||||||
echo " q) Quit"
|
echo " q) Quit"
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "Choose: " NET_OPT
|
read -rp "Choose: " NET_OPT
|
||||||
@@ -53,6 +55,22 @@ case "$NET_OPT" in
|
|||||||
;;
|
;;
|
||||||
1)
|
1)
|
||||||
;;
|
;;
|
||||||
|
2)
|
||||||
|
header "Prefer IPv4 over IPv6"
|
||||||
|
if grep -q "precedence ::ffff:0:0/96" /etc/gai.conf 2>/dev/null; then
|
||||||
|
warn "IPv4 preference already set."
|
||||||
|
else
|
||||||
|
echo "precedence ::ffff:0:0/96 100" >> /etc/gai.conf
|
||||||
|
info "IPv4 preference set. Outgoing connections will prefer IPv4."
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
header "Prefer IPv6 over IPv4"
|
||||||
|
sed -i '/precedence ::ffff:0:0\/96/d' /etc/gai.conf 2>/dev/null || true
|
||||||
|
info "IPv4 preference removed. System will use default IPv6-first behavior."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
q|Q)
|
q|Q)
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
@@ -159,10 +177,19 @@ PYEOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
header "Writing Netplan Config"
|
header "Writing Netplan Config"
|
||||||
|
NETPLAN_BACKUP_DIR="/root/.config/tinyboard/netplan-backups"
|
||||||
|
mkdir -p "$NETPLAN_BACKUP_DIR"
|
||||||
BACKUP_FILE=""
|
BACKUP_FILE=""
|
||||||
|
|
||||||
|
for OTHER_FILE in /etc/netplan/*.yaml; do
|
||||||
|
[ "$OTHER_FILE" = "$NETPLAN_FILE" ] && continue
|
||||||
|
BACKUP_OTHER="$NETPLAN_BACKUP_DIR/$(basename "${OTHER_FILE}").$(date +%Y%m%d%H%M%S)"
|
||||||
|
cp "$OTHER_FILE" "$BACKUP_OTHER"
|
||||||
|
rm "$OTHER_FILE"
|
||||||
|
warn "Removed conflicting netplan file: $OTHER_FILE (backed up to $BACKUP_OTHER)"
|
||||||
|
done
|
||||||
|
|
||||||
if [ -f "$NETPLAN_FILE" ]; then
|
if [ -f "$NETPLAN_FILE" ]; then
|
||||||
NETPLAN_BACKUP_DIR="/root/.config/tinyboard/netplan-backups"
|
|
||||||
mkdir -p "$NETPLAN_BACKUP_DIR"
|
|
||||||
BACKUP_FILE="$NETPLAN_BACKUP_DIR/$(basename "${NETPLAN_FILE}").$(date +%Y%m%d%H%M%S)"
|
BACKUP_FILE="$NETPLAN_BACKUP_DIR/$(basename "${NETPLAN_FILE}").$(date +%Y%m%d%H%M%S)"
|
||||||
cp "$NETPLAN_FILE" "$BACKUP_FILE"
|
cp "$NETPLAN_FILE" "$BACKUP_FILE"
|
||||||
info "Netplan config backed up to $BACKUP_FILE"
|
info "Netplan config backed up to $BACKUP_FILE"
|
||||||
|
|||||||
+57
-28
@@ -136,7 +136,7 @@ $PKG_INSTALL vim "$AUTOSSH_PKG" "$OPENSSH_PKG" git
|
|||||||
info "Installing Docker..."
|
info "Installing Docker..."
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
if [ "$PKG_MANAGER" = "apt" ]; then
|
if [ "$PKG_MANAGER" = "apt" ]; then
|
||||||
$PKG_INSTALL docker.io docker-compose-plugin
|
$PKG_INSTALL docker.io docker-cli docker-compose
|
||||||
else
|
else
|
||||||
curl -fsSL https://get.docker.com | bash
|
curl -fsSL https://get.docker.com | bash
|
||||||
fi
|
fi
|
||||||
@@ -146,7 +146,7 @@ fi
|
|||||||
|
|
||||||
if ! docker compose version >/dev/null 2>&1; then
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
if [ "$PKG_MANAGER" = "apt" ]; then
|
if [ "$PKG_MANAGER" = "apt" ]; then
|
||||||
$PKG_INSTALL docker-compose-plugin
|
$PKG_INSTALL docker-compose
|
||||||
else
|
else
|
||||||
warn "docker compose not available — Docker install script should have included it."
|
warn "docker compose not available — Docker install script should have included it."
|
||||||
fi
|
fi
|
||||||
@@ -174,7 +174,7 @@ SSHD_CONF="/etc/ssh/sshd_config"
|
|||||||
header "Hostname Setup"
|
header "Hostname Setup"
|
||||||
CURRENT_HOSTNAME=$(hostname)
|
CURRENT_HOSTNAME=$(hostname)
|
||||||
echo -e "Current hostname: ${YELLOW}$CURRENT_HOSTNAME${NC}"
|
echo -e "Current hostname: ${YELLOW}$CURRENT_HOSTNAME${NC}"
|
||||||
read -rp "Enter a hostname for this spoke (e.g. rocky, gouda, camembert): " SPOKE_NAME
|
read -rp "Enter a hostname for this spoke [${CURRENT_HOSTNAME}]: " SPOKE_NAME
|
||||||
SPOKE_NAME="${SPOKE_NAME:-$CURRENT_HOSTNAME}"
|
SPOKE_NAME="${SPOKE_NAME:-$CURRENT_HOSTNAME}"
|
||||||
[[ "$SPOKE_NAME" =~ ^[a-zA-Z0-9._-]+$ ]] || die "Spoke name '$SPOKE_NAME' contains invalid characters. Use only letters, numbers, dots, underscores, hyphens."
|
[[ "$SPOKE_NAME" =~ ^[a-zA-Z0-9._-]+$ ]] || die "Spoke name '$SPOKE_NAME' contains invalid characters. Use only letters, numbers, dots, underscores, hyphens."
|
||||||
hostnamectl set-hostname "$SPOKE_NAME"
|
hostnamectl set-hostname "$SPOKE_NAME"
|
||||||
@@ -184,9 +184,10 @@ info "Hostname set to: $SPOKE_NAME"
|
|||||||
header "SSH Key Setup"
|
header "SSH Key Setup"
|
||||||
echo "How would you like to handle the SSH key for the tunnel to $HUB_HOST?"
|
echo "How would you like to handle the SSH key for the tunnel to $HUB_HOST?"
|
||||||
echo " 1) Generate a new key automatically"
|
echo " 1) Generate a new key automatically"
|
||||||
echo " 2) Use an existing key (paste the private key)"
|
echo " 2) Choose an existing key from $SSH_DIR"
|
||||||
|
echo " 3) Paste a private key manually"
|
||||||
echo ""
|
echo ""
|
||||||
read -rp "Choose [1/2]: " KEY_CHOICE
|
read -rp "Choose [1/2/3]: " KEY_CHOICE
|
||||||
|
|
||||||
case "$KEY_CHOICE" in
|
case "$KEY_CHOICE" in
|
||||||
1)
|
1)
|
||||||
@@ -214,9 +215,41 @@ case "$KEY_CHOICE" in
|
|||||||
cat "$KEY_PATH.pub"
|
cat "$KEY_PATH.pub"
|
||||||
echo -e "${YELLOW}══════════════════════════════════════════${NC}"
|
echo -e "${YELLOW}══════════════════════════════════════════${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo -e "${YELLOW} On the hub, run as ${HUB_USER}:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " echo "$(cat "$KEY_PATH.pub")" >> /home/${HUB_USER}/.ssh/authorized_keys"
|
||||||
|
echo ""
|
||||||
read -rp "Press ENTER once the key has been added to ${HUB_HOST}..."
|
read -rp "Press ENTER once the key has been added to ${HUB_HOST}..."
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
|
mkdir -p "$SSH_DIR"
|
||||||
|
chown "$SPOKE_USER":"$SPOKE_USER" "$SSH_DIR"
|
||||||
|
chmod 700 "$SSH_DIR"
|
||||||
|
|
||||||
|
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 [0]: " KEY_IDX
|
||||||
|
KEY_IDX="${KEY_IDX:-0}"
|
||||||
|
[[ "$KEY_IDX" =~ ^[0-9]+$ ]] && [ "$KEY_IDX" -lt "${#AVAILABLE_KEYS[@]}" ] || die "Invalid choice."
|
||||||
|
KEY_PATH="${AVAILABLE_KEYS[$KEY_IDX]}"
|
||||||
|
KEY_NAME="$(basename "$KEY_PATH")"
|
||||||
|
info "Using existing key: $KEY_PATH"
|
||||||
|
echo ""
|
||||||
|
read -rp "Press ENTER once the public key has been added to ${HUB_HOST} authorized_keys..."
|
||||||
|
;;
|
||||||
|
3)
|
||||||
read -rp "Enter a name for the key file [hubkey]: " KEY_NAME
|
read -rp "Enter a name for the key file [hubkey]: " KEY_NAME
|
||||||
KEY_NAME="${KEY_NAME:-hubkey}"
|
KEY_NAME="${KEY_NAME:-hubkey}"
|
||||||
KEY_PATH="$SSH_DIR/$KEY_NAME"
|
KEY_PATH="$SSH_DIR/$KEY_NAME"
|
||||||
@@ -237,8 +270,11 @@ case "$KEY_CHOICE" in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
header "Password Authentication"
|
header "Password Authentication"
|
||||||
read -rp "Disable password auth for $SPOKE_USER and use keys only? [Y/n]: " DISABLE_PASS
|
warn "Do not disable password auth yet — the hub still needs password access to install its key via ssh-copy-id."
|
||||||
DISABLE_PASS="${DISABLE_PASS:-y}"
|
warn "Only disable this after running onboard-spoke.sh on the hub."
|
||||||
|
echo ""
|
||||||
|
read -rp "Disable password auth for $SPOKE_USER and use keys only? [y/N]: " DISABLE_PASS
|
||||||
|
DISABLE_PASS="${DISABLE_PASS:-n}"
|
||||||
if [[ "${DISABLE_PASS,,}" == "y" ]]; then
|
if [[ "${DISABLE_PASS,,}" == "y" ]]; then
|
||||||
if [ ! -f "$KEY_PATH" ]; then
|
if [ ! -f "$KEY_PATH" ]; then
|
||||||
warn "No key found at $KEY_PATH — skipping password auth disable to avoid lockout."
|
warn "No key found at $KEY_PATH — skipping password auth disable to avoid lockout."
|
||||||
@@ -283,6 +319,12 @@ if [ -n "$HUB_KEYSCAN" ]; then
|
|||||||
if ! grep -qF "$KEYSCAN_KEY" "$SSH_DIR/known_hosts" 2>/dev/null; then
|
if ! grep -qF "$KEYSCAN_KEY" "$SSH_DIR/known_hosts" 2>/dev/null; then
|
||||||
echo "$KEYSCAN_LINE" >> "$SSH_DIR/known_hosts"
|
echo "$KEYSCAN_LINE" >> "$SSH_DIR/known_hosts"
|
||||||
fi
|
fi
|
||||||
|
mkdir -p /root/.ssh
|
||||||
|
touch /root/.ssh/known_hosts
|
||||||
|
chmod 600 /root/.ssh/known_hosts
|
||||||
|
if ! grep -qF "$KEYSCAN_KEY" /root/.ssh/known_hosts 2>/dev/null; then
|
||||||
|
echo "$KEYSCAN_LINE" >> /root/.ssh/known_hosts
|
||||||
|
fi
|
||||||
done <<< "$HUB_KEYSCAN"
|
done <<< "$HUB_KEYSCAN"
|
||||||
fi
|
fi
|
||||||
check_permissions "$SSH_DIR/known_hosts" "known_hosts"
|
check_permissions "$SSH_DIR/known_hosts" "known_hosts"
|
||||||
@@ -305,7 +347,7 @@ find_free_port() {
|
|||||||
echo "$port"
|
echo "$port"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
warn "Port $port is in use, trying next..."
|
echo -e "${YELLOW}[!]${NC} Port $port is in use, trying next..." >&2
|
||||||
done
|
done
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -317,8 +359,9 @@ header "Configuring compose.yaml"
|
|||||||
info "Setting port to $TUNNEL_PORT and key to $KEY_NAME..."
|
info "Setting port to $TUNNEL_PORT and key to $KEY_NAME..."
|
||||||
|
|
||||||
SYNCTHING_MOUNT="$ARMBIAN_HOME/st/data"
|
SYNCTHING_MOUNT="$ARMBIAN_HOME/st/data"
|
||||||
mkdir -p "$SYNCTHING_MOUNT"
|
SYNCTHING_CONFIG="$ARMBIAN_HOME/st/config"
|
||||||
chown "$SPOKE_USER":"$SPOKE_USER" "$SYNCTHING_MOUNT"
|
mkdir -p "$SYNCTHING_MOUNT" "$SYNCTHING_CONFIG"
|
||||||
|
chown "$SPOKE_USER":"$SPOKE_USER" "$SYNCTHING_MOUNT" "$SYNCTHING_CONFIG"
|
||||||
|
|
||||||
SPOKE_UID=$(id -u "$SPOKE_USER")
|
SPOKE_UID=$(id -u "$SPOKE_USER")
|
||||||
SPOKE_GID=$(id -g "$SPOKE_USER")
|
SPOKE_GID=$(id -g "$SPOKE_USER")
|
||||||
@@ -329,8 +372,8 @@ sed -i "/known_hosts/!s|/home/[^/]*/\.ssh/[^:]*:/home/[^/]*/\.ssh/[^:]*:ro|${SSH
|
|||||||
sed -i "s|/home/[^/]*/\.ssh/known_hosts|${SSH_DIR}/known_hosts|g" "$COMPOSE"
|
sed -i "s|/home/[^/]*/\.ssh/known_hosts|${SSH_DIR}/known_hosts|g" "$COMPOSE"
|
||||||
sed -i "s| [a-zA-Z0-9._-]*@[a-zA-Z0-9._-]*\.[a-zA-Z0-9._-]*[[:space:]]*\$| ${HUB_USER}@${HUB_HOST}|" "$COMPOSE"
|
sed -i "s| [a-zA-Z0-9._-]*@[a-zA-Z0-9._-]*\.[a-zA-Z0-9._-]*[[:space:]]*\$| ${HUB_USER}@${HUB_HOST}|" "$COMPOSE"
|
||||||
sed -i "s|/home/[^/]*/st/data:|${SYNCTHING_MOUNT}:|g" "$COMPOSE"
|
sed -i "s|/home/[^/]*/st/data:|${SYNCTHING_MOUNT}:|g" "$COMPOSE"
|
||||||
sed -i "s|PUID=[0-9]*|PUID=${SPOKE_UID}|g" "$COMPOSE"
|
sed -i "s|/home/[^/]*/st/config:|${SYNCTHING_CONFIG}:|g" "$COMPOSE"
|
||||||
sed -i "s|PGID=[0-9]*|PGID=${SPOKE_GID}|g" "$COMPOSE"
|
sed -i "s|user: \"[0-9]*:[0-9]*\"|user: \"${SPOKE_UID}:${SPOKE_GID}\"|" "$COMPOSE"
|
||||||
sed -i "s|container_name: spoke-autossh|container_name: ${SPOKE_NAME}-autossh|g" "$COMPOSE"
|
sed -i "s|container_name: spoke-autossh|container_name: ${SPOKE_NAME}-autossh|g" "$COMPOSE"
|
||||||
sed -i "s|container_name: spoke-syncthing|container_name: ${SPOKE_NAME}-syncthing|g" "$COMPOSE"
|
sed -i "s|container_name: spoke-syncthing|container_name: ${SPOKE_NAME}-syncthing|g" "$COMPOSE"
|
||||||
sed -i "s|hostname: spoke-syncthing|hostname: ${SPOKE_NAME}-syncthing|g" "$COMPOSE"
|
sed -i "s|hostname: spoke-syncthing|hostname: ${SPOKE_NAME}-syncthing|g" "$COMPOSE"
|
||||||
@@ -373,21 +416,7 @@ echo -e " Spoke name: ${GREEN}$SPOKE_NAME${NC}"
|
|||||||
echo -e " Tunnel port: ${GREEN}$TUNNEL_PORT${NC} on $HUB_HOST"
|
echo -e " Tunnel port: ${GREEN}$TUNNEL_PORT${NC} on $HUB_HOST"
|
||||||
echo -e " SSH key: ${GREEN}$KEY_PATH${NC}"
|
echo -e " SSH key: ${GREEN}$KEY_PATH${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}The hub owner needs to do the following on ${HUB_HOST}:${NC}"
|
echo -e "${YELLOW}Next step — on the hub, run as ${HUB_USER}:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 1. Generate a hub->spoke key:"
|
echo " cd tinyboard && ./setup.sh # choose option 2 (onboard spoke)"
|
||||||
echo " ssh-keygen -t ed25519 -f ~/.ssh/${HUB_USER}-${SPOKE_NAME}-$(date +%Y%m)"
|
|
||||||
echo ""
|
|
||||||
echo " 2. Copy it to this spoke through the tunnel:"
|
|
||||||
echo " ssh-copy-id -i ~/.ssh/${HUB_USER}-${SPOKE_NAME}-$(date +%Y%m).pub -p $TUNNEL_PORT ${HUB_USER}@localhost"
|
|
||||||
echo ""
|
|
||||||
echo " 3. Add an rclone remote in ~/.config/rclone/rclone.conf:"
|
|
||||||
echo " [${SPOKE_NAME}-remote]"
|
|
||||||
echo " type = sftp"
|
|
||||||
echo " host = localhost"
|
|
||||||
echo " port = $TUNNEL_PORT"
|
|
||||||
echo " key_file = /home/$HUB_USER/.ssh/${HUB_USER}-${SPOKE_NAME}-$(date +%Y%m)"
|
|
||||||
echo " shell_type = unix"
|
|
||||||
echo " md5sum_command = md5sum"
|
|
||||||
echo " sha1sum_command = sha1sum"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ ST_CONTAINER=""
|
|||||||
get_apikey() {
|
get_apikey() {
|
||||||
ST_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i syncthing | head -1 || true)
|
ST_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i syncthing | head -1 || true)
|
||||||
[ -n "$ST_CONTAINER" ] || die "No running Syncthing container found."
|
[ -n "$ST_CONTAINER" ] || die "No running Syncthing container found."
|
||||||
APIKEY=$(docker exec "$ST_CONTAINER" python3 -c "import xml.etree.ElementTree as ET; print(ET.parse('/var/syncthing/config/config.xml').find('.//apikey').text)" 2>/dev/null || true)
|
APIKEY=$(docker exec "$ST_CONTAINER" cat /var/syncthing/config/config.xml 2>/dev/null | grep -o '<apikey>[^<]*</apikey>' | sed 's/<[^>]*>//g' || true)
|
||||||
[ -n "$APIKEY" ] || die "Could not read Syncthing API key from container '$ST_CONTAINER'."
|
[ -n "$APIKEY" ] || die "Could not read Syncthing API key from container '$ST_CONTAINER'."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user