-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsetup-openclaw-pi.sh
More file actions
315 lines (276 loc) · 9.6 KB
/
setup-openclaw-pi.sh
File metadata and controls
315 lines (276 loc) · 9.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# setup-openclaw-pi.sh (run on macOS)
#
# Automates:
# - SSD wipe + flash Raspberry Pi OS (via Raspberry Pi Imager CLI)
# - Secure provisioning over SSH
# - Node.js 22 install + OpenClaw install (non-Docker)
# - systemd service + basic host hardening
#
# DANGEROUS: This script can ERASE a disk.
# Safety features:
# - Lists disks and forces explicit disk selection (e.g. disk4)
# - Shows disk details before erasing
# - Refuses to erase internal disks
# - Requires a "type-to-confirm" phrase: ERASE diskX
###############################################################################
# -----------------------------
# Config (override via env vars)
# -----------------------------
PI_HOST="${PI_HOST:-onepi.local}"
PI_USER="${PI_USER:-openclaw}"
PI_SSH_PORT="${PI_SSH_PORT:-22}"
SSH_PUBKEY="${SSH_PUBKEY:-$HOME/.ssh/id_ed25519.pub}"
# Optional: if you want to skip the flash step and do only provisioning, set:
SKIP_FLASH="${SKIP_FLASH:-0}"
# Pi Imager CLI (optional). If missing, script will tell you to flash via GUI.
RPI_IMAGER_BIN="${RPI_IMAGER_BIN:-rpi-imager}"
RPI_OS_PRESET="${RPI_OS_PRESET:-raspios_arm64}" # preset name varies by imager version
RPI_OS_IMAGE_PATH="${RPI_OS_IMAGE_PATH:-}" # optional local .img/.zip
OPENCLAW_NPM_PKG="${OPENCLAW_NPM_PKG:-openclaw@latest}"
OPENCLAW_SERVICE_NAME="${OPENCLAW_SERVICE_NAME:-openclaw}"
# -----------------------------
# Helpers
# -----------------------------
die() { echo "ERROR: $*" >&2; exit 1; }
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"; }
confirm_phrase() {
local phrase="$1"
echo ""
echo "Type exactly to confirm: ${phrase}"
read -r -p "> " ans
[[ "$ans" == "$phrase" ]] || die "Confirmation did not match. Aborting."
}
wait_for_ssh() {
local host="$1" port="$2"
echo "Waiting for SSH on ${host}:${port} ..."
for _ in $(seq 1 180); do
if nc -z "$host" "$port" >/dev/null 2>&1; then
echo "SSH is reachable."
return 0
fi
sleep 2
done
die "Timed out waiting for SSH on ${host}:${port}"
}
run_ssh() {
local cmd="$1"
ssh -p "$PI_SSH_PORT" \
-o StrictHostKeyChecking=accept-new \
-o UserKnownHostsFile="$HOME/.ssh/known_hosts" \
"${PI_USER}@${PI_HOST}" "$cmd"
}
run_ssh_sudo() {
local cmd="$1"
run_ssh "sudo bash -lc $(printf '%q' "$cmd")"
}
# -----------------------------
# Preflight
# -----------------------------
need_cmd diskutil
need_cmd ssh
need_cmd nc
[[ -f "$SSH_PUBKEY" ]] || die "SSH public key not found at: $SSH_PUBKEY (set SSH_PUBKEY=...)"
echo "=================================================="
echo "OpenClaw Pi Bootstrap (macOS) — DESTRUCTIVE ACTIONS"
echo "=================================================="
echo "This script can ERASE a disk. Read carefully."
echo ""
# -----------------------------
# Step 1: Choose target disk
# -----------------------------
echo "Available disks:"
diskutil list
echo ""
read -r -p "Enter target SSD disk identifier (e.g. disk4): " TARGET_DISK
[[ "$TARGET_DISK" =~ ^disk[0-9]+$ ]] || die "Invalid disk identifier: $TARGET_DISK"
# Show disk details and enforce "external" (not internal/system)
echo ""
echo "Disk details for /dev/${TARGET_DISK}:"
diskutil info "/dev/${TARGET_DISK}" || die "Could not read disk info."
# Refuse internal disks (extra safety)
INTERNAL="$(diskutil info "/dev/${TARGET_DISK}" | awk -F': ' '/Device Location|Internal/ {print tolower($2)}' | head -n 1)"
# Many macs show "Device Location: Internal" or "Internal: Yes"
if diskutil info "/dev/${TARGET_DISK}" | grep -qiE 'Device Location:\s*Internal|Internal:\s*Yes'; then
die "Refusing to erase an INTERNAL disk (/dev/${TARGET_DISK}). Choose the external SSD."
fi
echo ""
echo "WARNING: Next step may ERASE /dev/${TARGET_DISK} بالكامل."
confirm_phrase "ERASE ${TARGET_DISK}"
# -----------------------------
# Step 2: Erase disk
# -----------------------------
echo "Erasing /dev/${TARGET_DISK} ..."
diskutil eraseDisk FAT32 RPI "/dev/${TARGET_DISK}"
# -----------------------------
# Step 3: Flash Raspberry Pi OS (optional)
# -----------------------------
if [[ "$SKIP_FLASH" == "1" ]]; then
echo ""
echo "SKIP_FLASH=1 set — skipping OS flash step."
else
echo ""
echo "======================================"
echo "Flashing Raspberry Pi OS to /dev/${TARGET_DISK}"
echo "======================================"
if command -v "$RPI_IMAGER_BIN" >/dev/null 2>&1; then
if [[ -n "$RPI_OS_IMAGE_PATH" ]]; then
[[ -f "$RPI_OS_IMAGE_PATH" ]] || die "RPI_OS_IMAGE_PATH not found: $RPI_OS_IMAGE_PATH"
echo "Using local OS image: $RPI_OS_IMAGE_PATH"
"$RPI_IMAGER_BIN" --cli --image "$RPI_OS_IMAGE_PATH" --storage "/dev/${TARGET_DISK}" || {
echo "Pi Imager CLI failed. Flash via GUI and re-run with SKIP_FLASH=1."
exit 1
}
else
echo "Using OS preset: $RPI_OS_PRESET"
echo "If this fails, flash via Raspberry Pi Imager GUI, then re-run with SKIP_FLASH=1."
"$RPI_IMAGER_BIN" --cli --os "$RPI_OS_PRESET" --storage "/dev/${TARGET_DISK}" || {
echo "Pi Imager CLI failed. Flash via GUI and re-run with SKIP_FLASH=1."
exit 1
}
fi
else
echo "Pi Imager CLI not found (${RPI_IMAGER_BIN})."
echo "Please flash the SSD using Raspberry Pi Imager GUI, then re-run with:"
echo " SKIP_FLASH=1 ./setup-openclaw-pi.sh"
exit 1
fi
fi
# -----------------------------
# Step 4: Boot Pi and provision via SSH
# -----------------------------
echo ""
echo "=================================================="
echo "Now move the SSD to the Pi and boot it."
echo "Ensure it's on the same network as this Mac."
echo "Then press Enter to continue provisioning over SSH."
echo "=================================================="
read -r -p "Press Enter when Pi is booted... " _
wait_for_ssh "$PI_HOST" "$PI_SSH_PORT"
echo ""
echo "Pushing SSH key (idempotent) ..."
PUBKEY_CONTENT="$(cat "$SSH_PUBKEY")"
run_ssh "mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && grep -qF $(printf '%q' "$PUBKEY_CONTENT") ~/.ssh/authorized_keys || echo $(printf '%q' "$PUBKEY_CONTENT") >> ~/.ssh/authorized_keys"
echo ""
echo "Provisioning OS packages + security hardening ..."
run_ssh_sudo "
set -euo pipefail
apt update
DEBIAN_FRONTEND=noninteractive apt full-upgrade -y
DEBIAN_FRONTEND=noninteractive apt install -y git curl wget unzip ca-certificates ufw fail2ban openssl net-tools perl
"
echo ""
echo "Adding 2GB swapfile if missing ..."
run_ssh_sudo "
set -euo pipefail
if ! swapon --show | grep -q '/swapfile'; then
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
"
echo ""
echo "Hardening SSH (keys-only, no root login) ..."
echo "NOTE: This will disable SSH password login. Ensure your key works."
run_ssh_sudo "
set -euo pipefail
SSHD=/etc/ssh/sshd_config
cp -n \$SSHD \${SSHD}.bak || true
perl -0777 -i -pe 's/^#?\\s*PermitRootLogin\\s+.*/PermitRootLogin no/mg;
s/^#?\\s*PasswordAuthentication\\s+.*/PasswordAuthentication no/mg;
s/^#?\\s*PubkeyAuthentication\\s+.*/PubkeyAuthentication yes/mg' \$SSHD
systemctl restart ssh
"
echo ""
echo "Enabling firewall (UFW) ..."
run_ssh_sudo "
set -euo pipefail
ufw allow OpenSSH
ufw --force enable
ufw status verbose
"
echo ""
echo "Enabling fail2ban ..."
run_ssh_sudo "
set -euo pipefail
systemctl enable fail2ban
systemctl restart fail2ban
systemctl status fail2ban --no-pager
"
echo ""
echo "Installing Node.js 22+ ..."
run_ssh_sudo "
set -euo pipefail
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
DEBIAN_FRONTEND=noninteractive apt install -y nodejs
node -v
npm -v
"
echo ""
echo "Installing OpenClaw ..."
run_ssh_sudo "
set -euo pipefail
npm install -g ${OPENCLAW_NPM_PKG}
openclaw --version
"
echo ""
echo "Creating systemd service (${OPENCLAW_SERVICE_NAME}) ..."
run_ssh_sudo "
set -euo pipefail
OPENCLAW_BIN=\$(command -v openclaw)
if [[ -z \"\$OPENCLAW_BIN\" ]]; then
echo 'openclaw not found in PATH' >&2
exit 1
fi
# Best-effort command detection
GATEWAY_CMD=''
if \$OPENCLAW_BIN --help 2>/dev/null | grep -qE '\\bgateway\\b'; then
GATEWAY_CMD=\"\$OPENCLAW_BIN gateway\"
elif \$OPENCLAW_BIN --help 2>/dev/null | grep -qE '\\bstart\\b'; then
GATEWAY_CMD=\"\$OPENCLAW_BIN start\"
else
GATEWAY_CMD=\"\$OPENCLAW_BIN\"
fi
cat >/etc/systemd/system/${OPENCLAW_SERVICE_NAME}.service <<'UNIT'
[Unit]
Description=OpenClaw Gateway
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${PI_USER}
WorkingDirectory=/home/${PI_USER}
ExecStart=/usr/bin/env bash -lc '__GATEWAY_CMD__'
Restart=always
RestartSec=3
Environment=NODE_ENV=production
# Basic hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/home/${PI_USER}
[Install]
WantedBy=multi-user.target
UNIT
sed -i \"s|__GATEWAY_CMD__|\${GATEWAY_CMD//|/\\\\|}|g\" /etc/systemd/system/${OPENCLAW_SERVICE_NAME}.service
systemctl daemon-reload
systemctl enable ${OPENCLAW_SERVICE_NAME}
systemctl restart ${OPENCLAW_SERVICE_NAME}
systemctl status ${OPENCLAW_SERVICE_NAME} --no-pager
"
echo ""
echo "DONE."
echo ""
echo "NEXT (manual, important):"
echo " 1) SSH into the Pi: ssh ${PI_USER}@${PI_HOST}"
echo " 2) Run OpenClaw wizard (command varies):"
echo " openclaw --help"
echo " openclaw onboard # or: openclaw configure"
echo " 3) In config, bind gateway to 127.0.0.1 (NOT 0.0.0.0)."
echo " 4) Restart service: sudo systemctl restart ${OPENCLAW_SERVICE_NAME}"
echo " 5) Logs: journalctl -u ${OPENCLAW_SERVICE_NAME} -n 200 --no-pager"