Repository navigation
Expand file tree
/
Copy pathproxmox-node.sh
More file actions
executable file
·288 lines (264 loc) · 11.7 KB
/
Copy pathproxmox-node.sh
File metadata and controls
executable file
·288 lines (264 loc) · 11.7 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
#!/usr/bin/env bash
#
# Subshell - Proxmox VE NODE helper (spec 2026-09-29), run on the PROXMOX
# HOST as root. Creates an unprivileged Debian container and runs
# YOUR Subshell instance's own node install rail inside it - the exact
# command the app shows under its Nodes screen. Paste that URL once; it
# carries a one-time setup key, so there is nothing to save and nothing to
# reuse. No Docker and no image: a node wants plain paths and tmux.
#
# The agent runs as the CT's `subshell` user and its panes inherit that
# account. Updating the AGENT is the product's own rail; this script's
# `update` verb runs it (the in-app node update is the other door).
set -u
HELPER_SOURCE="${BASH_SOURCE[0]:-}"
APP="Subshell node"
CT_ID="${CT_ID:-}"
CT_HOSTNAME="${CT_HOSTNAME:-subshell-node}"
CT_CORES="${CT_CORES:-1}"
CT_RAM_MB="${CT_RAM_MB:-1024}"
CT_DISK_GB="${CT_DISK_GB:-4}"
CT_BRIDGE="${CT_BRIDGE:-vmbr0}"
SETUP_URL="${SETUP_URL:-}"
CT_STORAGE="${CT_STORAGE:-}"
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-}"
BACKUP_STORAGE="${BACKUP_STORAGE:-}"
msg_ok() { echo -e "\e[32m[OK]\e[0m $*"; }
msg_err() { echo -e "\e[31m[ERROR]\e[0m $*" >&2; exit 1; }
header() { echo -e "\e[32m ==>\e[0m \e[1m$1\e[0m"; }
# Keep the version that actually performed the install, even before a newer
# website deployment. A piped invocation has no source file, so fetch it.
save_helper() {
local destination="/root/proxmox-node.sh"
if [[ -f "$HELPER_SOURCE" ]]; then
if [[ "$HELPER_SOURCE" -ef "$destination" ]]; then
chmod 755 "$destination"
else
install -m 755 "$HELPER_SOURCE" "$destination"
fi
else
curl -fsSL "https://subshell.sh/proxmox-node.sh" -o "$destination" && chmod 755 "$destination"
fi
}
# fn_prompt VAR QUESTION DEFAULT (PVE_NO_PROMPT=1 takes every default, for
# unattended runs, exactly like the community scripts' var_check posture)
fn_prompt() {
local var="$1" question="$2" def="$3" answer=""
if [[ "${PVE_NO_PROMPT:-0}" == "1" ]]; then
eval "$var=\"\$def\""
return
fi
read -rp "${question} [${def}]: " answer < /dev/tty || answer=""
eval "$var=\"${answer:-$def}\""
}
preflight() {
[[ $EUID -eq 0 ]] || msg_err "run this on the Proxmox host as root"
command -v pct >/dev/null 2>&1 || msg_err "pct not found: this is not a Proxmox host"
}
need_ct_id() {
[[ -n "$CT_ID" ]] || read -rp "Container ID: " CT_ID < /dev/tty
[[ -n "$CT_ID" ]] || msg_err "no container id given"
pct status "$CT_ID" >/dev/null 2>&1 || msg_err "no CT $CT_ID on this host"
}
# Only active storages supporting the requested content are candidates.
default_storage() {
local content="${1:-rootdir}" stores
stores=$(pvesm status -content "$content") || return 1
awk 'NR>1 && $3 == "active" {print $1; exit}' <<< "$stores"
}
select_storage() {
local var="$1" content="$2" question="$3" stores chosen
stores=$(pvesm status -content "$content") || msg_err "cannot list $content storage"
chosen="${!var}"
[[ -n "$chosen" ]] || chosen=$(default_storage "$content")
[[ -n "$chosen" ]] || msg_err "no active storage supports $content"
echo "$stores"
fn_prompt "$var" "$question" "$chosen"
chosen="${!var}"
awk -v name="$chosen" 'NR>1 && $1 == name && $3 == "active" {found=1} END {exit !found}' <<< "$stores" \
|| msg_err "storage $chosen is not active or does not support $content"
}
# Use PVE's catalogue and downloader, including its integrity checks.
# PVE 8 supports Debian 12; PVE 9 adds Debian 13 support.
latest_template() {
local arch catalogue file version major debian=12
arch=$(dpkg --print-architecture) || msg_err "cannot determine host architecture"
version=$(pveversion) || msg_err "cannot determine Proxmox version"
major="${version#pve-manager/}"; major="${major%%.*}"
[[ "$major" =~ ^[0-9]+$ && "$major" -ge 8 ]] || msg_err "Proxmox VE 8 or newer is required"
[[ "$major" -lt 9 ]] || debian=13
pveam update >&2 || msg_err "cannot refresh Proxmox template catalogue"
catalogue=$(pveam available --section system) || msg_err "cannot list Proxmox templates"
file=$(awk '{print $2}' <<< "$catalogue" |
grep -E "^debian-${debian}-standard_[0-9.]+-[0-9]+_${arch}\.tar\.(zst|gz|xz)$" | sort -V | tail -1)
[[ -n "$file" ]] || msg_err "no Debian $debian template for $arch in Proxmox's catalogue"
echo "$file"
}
# The exact shape the app renders: http(s) origin, /install.sh, one
# setup_key=nsk_... param. The trailing &server=<origin> clause the app adds
# when you picked a non-canonical address is part of the pasted command and
# passes through to curl untouched, so it is accepted here. Loopback is
# refused at the same point in EITHER position: a CT curl-ing localhost
# reaches the CT, not the instance, and the carried clause bakes the node's
# dial-home URL - loopback there leaves the node dialing its own container.
# Validated before anything is created; the key value is never echoed.
validate_setup_url() {
local url="$1"
[[ "$url" =~ ^https?://[^/[:space:]]+/install\.sh\?setup_key=nsk_[A-Za-z0-9_-]{8,}(&server=[^?#[:space:]]*)?$ ]] || return 1
local origin="${url%%/install.sh*}"
origin="${origin,,}"
[[ "$origin" != *"://localhost"* && "$origin" != *"://127."* && "$origin" != *"://[::1]"* ]] || return 1
if [[ "$url" == *"&server="* ]]; then
local carried="${url#*&server=}"
carried="${carried,,}"
[[ "$carried" != *"://localhost"* && "$carried" != *"://127."* && "$carried" != *"://[::1]"* ]]
fi
}
# ---------- install ----------
install_ct() {
preflight
header "Create the ${APP} container"
local next_id
next_id=$(pvesh get /cluster/nextid) || msg_err "cannot get an unused cluster container ID"
if [[ -n "$CT_ID" ]]; then
msg_ok "using CT_ID=$CT_ID from the environment"
else
fn_prompt CT_ID "Container ID" "$next_id"
fi
fn_prompt CT_HOSTNAME "Hostname (the node names itself after it)" "$CT_HOSTNAME"
fn_prompt CT_CORES "Cores" "$CT_CORES"
fn_prompt CT_RAM_MB "Memory MB" "$CT_RAM_MB"
fn_prompt CT_DISK_GB "Disk GB" "$CT_DISK_GB"
fn_prompt CT_BRIDGE "Bridge" "$CT_BRIDGE"
if [[ -z "$SETUP_URL" ]]; then
echo "Paste the install URL from your Subshell instance (its Nodes screen)."
echo "It carries a one-time key: this run gets one shot at it."
read -rsp "Install URL: " SETUP_URL < /dev/tty || SETUP_URL=""
echo
fi
validate_setup_url "$SETUP_URL" || msg_err "not a usable install URL from the app (expected http(s)://<reachable-host>/install.sh?setup_key=nsk_...[&server=...]); nothing was created"
local tmpl rootpass
select_storage CT_STORAGE rootdir "Container disk storage"
select_storage TEMPLATE_STORAGE vztmpl "Template storage"
tmpl=$(latest_template) || msg_err "template discovery failed; nothing was created"
header "Downloading ${tmpl} (this can take a minute)"
pveam download "$TEMPLATE_STORAGE" "$tmpl" || msg_err "template download failed; nothing was created"
rootpass=$(openssl rand -base64 12)
local args
args=("$CT_ID" "${TEMPLATE_STORAGE}:vztmpl/${tmpl}"
--unprivileged 1
--hostname "$CT_HOSTNAME" --ostype debian
--memory "$CT_RAM_MB" --swap 512 --cores "$CT_CORES"
--rootfs "${CT_STORAGE}:${CT_DISK_GB}"
--net0 "name=eth0,bridge=${CT_BRIDGE},ip=dhcp"
--password "$rootpass")
pct create "${args[@]}" || msg_err "pct create failed"
pct start "$CT_ID" || msg_err "pct start failed"
msg_ok "CT $CT_ID created (CT root password: ${rootpass})"
}
install_node_in_ct() {
header "Install tmux and the node agent inside the CT"
local _
for _ in $(seq 1 60); do pct exec "$CT_ID" -- true 2>/dev/null && break; sleep 1; done
pct exec "$CT_ID" -- bash -ec '
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq ca-certificates curl tmux >/dev/null
useradd --create-home --uid 1000 --shell /bin/bash subshell
loginctl enable-linger subshell
' || msg_err "base setup inside the CT failed"
# The rail itself: the instance's own install script, as the node user,
# non-interactive, naming the node after the CT hostname. NO_SERVICE
# because the service is installed explicitly below (enable AND start).
pct exec "$CT_ID" -- bash -s -- "$SETUP_URL" "$CT_HOSTNAME" <<'REMOTE'
set -eu
url="$1"; name="$2"
uid=$(id -u subshell)
for _ in $(seq 1 30); do [ -d "/run/user/$uid" ] && break; sleep 1; done
[ -d "/run/user/$uid" ] || { echo "logind never came up inside the CT" >&2; exit 1; }
runuser -u subshell -- env HOME=/home/subshell USER=subshell LOGNAME=subshell \
XDG_RUNTIME_DIR="/run/user/$uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
SUBSHELL_NO_SERVICE=1 SUBSHELL_NODE_NAME="$name" \
bash -c 'set -o pipefail; curl -fsSLg "$1" | bash' _ "$url"
runuser -u subshell -- env HOME=/home/subshell USER=subshell LOGNAME=subshell \
PATH="/home/subshell/.local/bin:/usr/local/bin:/usr/bin:/bin" \
XDG_RUNTIME_DIR="/run/user/$uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
subshell service install
echo "agent: $(runuser -u subshell -- env HOME=/home/subshell PATH="/home/subshell/.local/bin:/usr/local/bin:/usr/bin:/bin" subshell version)"
REMOTE
[[ $? -eq 0 ]] || msg_err "the node install failed inside CT $CT_ID (a spent or expired key is NOT retryable: mint a fresh setup link and re-run on a clean CT)"
# A `curl | bash` install leaves no file to re-run; save our own copy
# best-effort (the update verb needs it) so the print below is a promise.
local update_hint=""
if save_helper; then
update_hint="Update the agent later with: bash /root/proxmox-node.sh update (a copy you kept works too)"
else
update_hint="WARN: could not save /root/proxmox-node.sh - keep your copy for the update verb"
fi
msg_ok "${APP} enrolled - watch it come online in your instance's Nodes list"
echo "$update_hint"
}
# ---------- update ----------
update_agent() {
preflight
need_ct_id
header "Run the agent's own updater inside the CT (its service restarts it)"
pct exec "$CT_ID" -- bash -ec '
uid=$(id -u subshell)
runuser -u subshell -- env HOME=/home/subshell USER=subshell LOGNAME=subshell \
PATH="/home/subshell/.local/bin:/usr/local/bin:/usr/bin:/bin" \
XDG_RUNTIME_DIR="/run/user/$uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
subshell update --yes
' || msg_err "update failed inside CT $CT_ID"
msg_ok "${APP} updated"
}
# ---------- remove / backup / restore ----------
remove_ct() {
preflight
need_ct_id
if [[ "${PVE_NO_PROMPT:-0}" != "1" ]]; then
echo "This DESTROYS CT $CT_ID and everything in it."
local a=""
read -rp "Type yes to confirm: " a < /dev/tty
[[ "$a" == "yes" ]] || msg_err "cancelled"
fi
pct shutdown "$CT_ID" 2>/dev/null || true
sleep 3
pct stop "$CT_ID" 2>/dev/null || true
pct destroy "$CT_ID"
msg_ok "CT $CT_ID destroyed"
}
backup_ct() {
preflight
need_ct_id
select_storage BACKUP_STORAGE backup "Backup storage"
vzdump "$CT_ID" --mode snapshot --compress zstd --storage "$BACKUP_STORAGE"
}
restore_help() {
preflight
echo "Restore is Proxmox's own flow: Datacenter -> Backup -> select the vzdump -> Restore."
echo "A ${APP} backup is a full-CT snapshot, so restoring brings back the container and its agent as one."
}
# ---------- menu ----------
case "${1:-}" in
install) install_ct; install_node_in_ct ;;
update) update_agent ;;
remove) remove_ct ;;
backup) backup_ct ;;
restore) restore_help ;;
"")
header "Menu"
echo " 1) install 2) update 3) backup 4) restore help 5) remove"
sel=""
read -rp "Select: " sel < /dev/tty
case "$sel" in
1) install_ct; install_node_in_ct ;;
2) update_agent ;;
3) need_ct_id; backup_ct ;;
4) restore_help ;;
5) need_ct_id; remove_ct ;;
*) msg_err "no such option" ;;
esac
;;
*) msg_err "usage: proxmox-node.sh [install|update|remove|backup|restore]" ;;
esac