underpost 3.2.21 → 3.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ghpkg.ci.yml +1 -1
- package/.github/workflows/gitlab.ci.yml +1 -1
- package/.github/workflows/npmpkg.ci.yml +1 -1
- package/.github/workflows/publish.ci.yml +2 -2
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/CHANGELOG.md +178 -1
- package/CLI-HELP.md +58 -2
- package/README.md +3 -2
- package/baremetal/commission-workflows.json +1 -0
- package/bin/build.js +13 -1
- package/docker-compose.yml +224 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +27 -15
- package/scripts/kubeadm-node-setup.sh +317 -0
- package/scripts/rocky-kickstart.sh +877 -185
- package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
- package/scripts/test-monitor.sh +242 -78
- package/src/cli/baremetal.js +717 -16
- package/src/cli/cluster.js +3 -1
- package/src/cli/deploy.js +11 -10
- package/src/cli/docker-compose.js +591 -0
- package/src/cli/fs.js +35 -11
- package/src/cli/index.js +83 -0
- package/src/cli/kickstart.js +142 -20
- package/src/cli/monitor.js +114 -29
- package/src/cli/repository.js +75 -11
- package/src/cli/run.js +275 -6
- package/src/cli/ssh.js +190 -0
- package/src/cli/static.js +2 -2
- package/src/client/components/core/PanelForm.js +44 -44
- package/src/{server → client-builder}/client-build-docs.js +15 -5
- package/src/{server → client-builder}/client-build-live.js +3 -3
- package/src/{server → client-builder}/client-build.js +25 -22
- package/src/{server → client-builder}/client-dev-server.js +3 -3
- package/src/{server → client-builder}/client-icons.js +2 -2
- package/src/{server → client-builder}/ssr.js +5 -5
- package/src/client.build.js +1 -1
- package/src/client.dev.js +1 -1
- package/src/index.js +12 -1
- package/src/mailer/EmailRender.js +1 -1
- package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/nginx/Nginx.js +250 -0
- package/src/server/catalog.js +7 -14
- package/src/server/conf.js +61 -12
- package/src/server/start.js +17 -5
- package/src/server.js +1 -1
- package/test/deploy-monitor.test.js +33 -5
- package/typedoc.json +3 -1
- package/src/server/ipfs-client.js +0 -597
- /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
- /package/src/{server → client-builder}/client-formatted.js +0 -0
|
@@ -1,106 +1,254 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Rocky Linux 9 - Anaconda %pre Ephemeral Commissioning Script
|
|
3
|
-
#
|
|
2
|
+
# Rocky Linux 9 - Anaconda %pre Ephemeral Commissioning + Disk Install Script
|
|
3
|
+
#
|
|
4
|
+
# This script runs inside the ephemeral Anaconda live environment (booted via
|
|
5
|
+
# iPXE/HTTP with inst.sshd). It drives the full unattended lifecycle:
|
|
6
|
+
#
|
|
7
|
+
# 1. ephemeral boot up (Anaconda live, this %pre script starts)
|
|
8
|
+
# 2. ssh ready (key-only sshd up and verified listening)
|
|
9
|
+
# 3. metadata posted (async POST stage=ssh-ready to bootstrap server)
|
|
10
|
+
# 4. remote install executed (controller SSHes in and runs the installer,
|
|
11
|
+
# or AUTO_INSTALL fallback self-triggers it)
|
|
12
|
+
# 5. disk installation (POST stage=installing; Rocky written to disk)
|
|
13
|
+
# 6. install completed (POST stage=completed)
|
|
14
|
+
# 7. reboot (boot order points at the freshly written disk)
|
|
15
|
+
# 8. boot into deployed OS
|
|
16
|
+
#
|
|
17
|
+
# Variables injected by the kickstart %pre header (kickstart.js):
|
|
18
|
+
# ROOT_PASS, AUTHORIZED_KEYS, ADMIN_USER,
|
|
19
|
+
# BOOTSTRAP_URL, WORKFLOW_ID, SYSTEM_ID, TARGET_HOSTNAME,
|
|
20
|
+
# SSH_PORT, INSTALL_DISK_HINT, AUTO_INSTALL
|
|
4
21
|
|
|
5
22
|
set +e
|
|
6
23
|
|
|
7
|
-
|
|
24
|
+
SSH_PORT="${SSH_PORT:-22}"
|
|
25
|
+
AUTO_INSTALL="${AUTO_INSTALL:-1}"
|
|
26
|
+
# Fallback: if no remote install trigger arrives within this window, the
|
|
27
|
+
# ephemeral runtime self-installs so a single missed handshake never bricks the
|
|
28
|
+
# flow. The controller normally triggers far sooner over SSH.
|
|
29
|
+
AUTO_INSTALL_FALLBACK_SECONDS="${AUTO_INSTALL_FALLBACK_SECONDS:-600}"
|
|
30
|
+
|
|
31
|
+
KS_LOG=/tmp/ks-pre.log
|
|
32
|
+
READY_FLAG=/tmp/.underpost-ssh-ready-posted
|
|
33
|
+
TRIGGER_FILE=/tmp/.underpost-install-trigger
|
|
34
|
+
INSTALL_LOCK=/tmp/.underpost-install-running
|
|
35
|
+
INSTALL_DONE=/tmp/.underpost-install-done
|
|
36
|
+
INSTALLER=/usr/local/bin/underpost-install.sh
|
|
37
|
+
TARGET_MNT=/mnt/sysimage-underpost
|
|
38
|
+
|
|
39
|
+
log() { echo "$(date): $*" | tee -a "$KS_LOG"; }
|
|
40
|
+
|
|
41
|
+
# Reference feedback on the physical/serial console. Use printf with \r\n so
|
|
42
|
+
# lines render correctly on serial and physical terminals. Best-effort: never
|
|
43
|
+
# fails if /dev/console is unavailable.
|
|
44
|
+
console_log() { printf "[underpost] %s\r\n" "$*" > /dev/console 2>/dev/null || true; }
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# 1. Root password (emergency console fallback only; SSH stays key-only)
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
8
49
|
if [ -n "$ROOT_PASS" ]; then
|
|
9
|
-
|
|
50
|
+
echo "root:$ROOT_PASS" | chpasswd 2>/dev/null || \
|
|
10
51
|
echo "$ROOT_PASS" | passwd --stdin root 2>/dev/null || {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
52
|
+
HASH=$(python3 -c "import crypt; print(crypt.crypt('$ROOT_PASS', crypt.mksalt(crypt.METHOD_SHA512)))" 2>/dev/null || \
|
|
53
|
+
openssl passwd -6 "$ROOT_PASS" 2>/dev/null)
|
|
54
|
+
[ -n "$HASH" ] && sed -i "s|^root:[^:]*:|root:$HASH:|" /etc/shadow 2>/dev/null
|
|
14
55
|
}
|
|
15
56
|
fi
|
|
16
57
|
|
|
17
|
-
# 2. SSH authorized_keys for root
|
|
18
58
|
if [ -n "$AUTHORIZED_KEYS" ]; then
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
59
|
+
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
|
60
|
+
echo "$AUTHORIZED_KEYS" > /root/.ssh/authorized_keys
|
|
61
|
+
chmod 600 /root/.ssh/authorized_keys
|
|
22
62
|
fi
|
|
23
63
|
|
|
24
|
-
#
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# 2. Admin user (parity with previous behavior)
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
25
67
|
if command -v useradd >/dev/null 2>&1; then
|
|
26
|
-
|
|
68
|
+
useradd -m -G wheel "$ADMIN_USER" 2>/dev/null || true
|
|
27
69
|
else
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
70
|
+
NEXT_UID=$(awk -F: 'BEGIN{max=999} $3>max && $3<60000{max=$3} END{print max+1}' /etc/passwd 2>/dev/null || echo 1001)
|
|
71
|
+
if ! grep -q "^$ADMIN_USER:" /etc/passwd 2>/dev/null; then
|
|
72
|
+
echo "$ADMIN_USER:x:$NEXT_UID:$NEXT_UID:$ADMIN_USER:/home/$ADMIN_USER:/bin/bash" >> /etc/passwd
|
|
73
|
+
echo "$ADMIN_USER:x:$NEXT_UID:" >> /etc/group 2>/dev/null
|
|
74
|
+
echo "$ADMIN_USER:!:19000:0:99999:7:::" >> /etc/shadow 2>/dev/null
|
|
75
|
+
mkdir -p /home/$ADMIN_USER
|
|
76
|
+
fi
|
|
35
77
|
fi
|
|
36
78
|
|
|
37
|
-
if [ -n "$
|
|
38
|
-
|
|
39
|
-
echo "$
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
if [ -n "$ADMIN_PASS" ]; then
|
|
80
|
+
echo "$ADMIN_USER:$ADMIN_PASS" | chpasswd 2>/dev/null || \
|
|
81
|
+
echo "$ADMIN_PASS" | passwd --stdin "$ADMIN_USER" 2>/dev/null || {
|
|
82
|
+
HASH=$(python3 -c "import crypt; print(crypt.crypt('$ADMIN_PASS', crypt.mksalt(crypt.METHOD_SHA512)))" 2>/dev/null || \
|
|
83
|
+
openssl passwd -6 "$ADMIN_PASS" 2>/dev/null)
|
|
84
|
+
[ -n "$HASH" ] && sed -i "s|^$ADMIN_USER:[^:]*:|$ADMIN_USER:$HASH:|" /etc/shadow 2>/dev/null
|
|
43
85
|
}
|
|
44
86
|
fi
|
|
45
87
|
|
|
46
88
|
if [ -n "$AUTHORIZED_KEYS" ]; then
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
89
|
+
mkdir -p /home/$ADMIN_USER/.ssh && chmod 700 /home/$ADMIN_USER/.ssh
|
|
90
|
+
echo "$AUTHORIZED_KEYS" > /home/$ADMIN_USER/.ssh/authorized_keys
|
|
91
|
+
chmod 600 /home/$ADMIN_USER/.ssh/authorized_keys
|
|
92
|
+
chown -R $ADMIN_USER:$ADMIN_USER /home/$ADMIN_USER/.ssh 2>/dev/null || true
|
|
51
93
|
fi
|
|
52
94
|
|
|
53
95
|
if [ -d /etc/sudoers.d ]; then
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
elif [ -f /etc/sudoers ]; then
|
|
57
|
-
|
|
96
|
+
echo "$ADMIN_USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$ADMIN_USER
|
|
97
|
+
chmod 0440 /etc/sudoers.d/$ADMIN_USER
|
|
98
|
+
elif [ -f /etc/sudoers ]; then
|
|
99
|
+
echo "$ADMIN_USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# 3. Configure sshd for key-only automation
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
log "[sshd] Starting SSH configuration..."
|
|
106
|
+
|
|
107
|
+
mkdir -p /etc/ssh /root/.ssh /var/lib/anaconda/ssh /run/install/ssh
|
|
108
|
+
chmod 755 /etc/ssh
|
|
109
|
+
chmod 700 /root/.ssh
|
|
110
|
+
chmod 700 /var/lib/anaconda/ssh 2>/dev/null || true
|
|
111
|
+
chmod 700 /run/install/ssh 2>/dev/null || true
|
|
112
|
+
|
|
113
|
+
for KEY_DEST in \
|
|
114
|
+
/root/.ssh/authorized_keys \
|
|
115
|
+
/var/lib/anaconda/ssh/authorized_keys \
|
|
116
|
+
/run/install/ssh/authorized_keys; do
|
|
117
|
+
mkdir -p "$(dirname "$KEY_DEST")"
|
|
118
|
+
chmod 700 "$(dirname "$KEY_DEST")" 2>/dev/null || true
|
|
119
|
+
if [ -n "$AUTHORIZED_KEYS" ]; then
|
|
120
|
+
echo "$AUTHORIZED_KEYS" > "$KEY_DEST"
|
|
121
|
+
chmod 600 "$KEY_DEST"
|
|
122
|
+
chown root:root "$KEY_DEST" 2>/dev/null || true
|
|
123
|
+
log "[sshd] Wrote authorized_keys to $KEY_DEST"
|
|
124
|
+
fi
|
|
125
|
+
done
|
|
126
|
+
|
|
127
|
+
if [ -n "$AUTHORIZED_KEYS" ] && [ -n "$ADMIN_USER" ]; then
|
|
128
|
+
mkdir -p "/home/$ADMIN_USER/.ssh"
|
|
129
|
+
chmod 700 "/home/$ADMIN_USER/.ssh"
|
|
130
|
+
echo "$AUTHORIZED_KEYS" > "/home/$ADMIN_USER/.ssh/authorized_keys"
|
|
131
|
+
chmod 600 "/home/$ADMIN_USER/.ssh/authorized_keys"
|
|
132
|
+
chown -R "$ADMIN_USER:$ADMIN_USER" "/home/$ADMIN_USER/.ssh" 2>/dev/null || true
|
|
133
|
+
log "[sshd] Wrote authorized_keys to /home/$ADMIN_USER/.ssh/authorized_keys"
|
|
58
134
|
fi
|
|
59
135
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
grep -q "^PermitRootLogin" /etc/ssh/sshd_config || echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
|
70
|
-
grep -q "^PubkeyAuthentication" /etc/ssh/sshd_config || echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config
|
|
136
|
+
# Key-only when keys are present; fall back to password auth only if no key was
|
|
137
|
+
# injected (so automation is never fully locked out). Root login is permitted
|
|
138
|
+
# strictly via authorized keys (prohibit-password) for non-interactive batch
|
|
139
|
+
# command execution from the controller.
|
|
140
|
+
if [ -n "$AUTHORIZED_KEYS" ]; then
|
|
141
|
+
SSHD_PASSWORD_AUTH="no"
|
|
142
|
+
SSHD_PERMIT_ROOT="prohibit-password"
|
|
143
|
+
SSHD_KBD_INTERACTIVE="no"
|
|
144
|
+
log "[sshd] authorized_keys present -> key-only mode (PasswordAuthentication no, PermitRootLogin prohibit-password)"
|
|
71
145
|
else
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
146
|
+
SSHD_PASSWORD_AUTH="yes"
|
|
147
|
+
SSHD_PERMIT_ROOT="yes"
|
|
148
|
+
SSHD_KBD_INTERACTIVE="yes"
|
|
149
|
+
log "[sshd] WARNING: no authorized_keys injected -> falling back to password auth"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
cat > /etc/ssh/sshd_config << SSHEOF
|
|
153
|
+
Port ${SSH_PORT}
|
|
154
|
+
Protocol 2
|
|
155
|
+
HostKey /etc/ssh/ssh_host_rsa_key
|
|
156
|
+
HostKey /etc/ssh/ssh_host_ecdsa_key
|
|
157
|
+
HostKey /etc/ssh/ssh_host_ed25519_key
|
|
158
|
+
SyslogFacility AUTH
|
|
159
|
+
LogLevel INFO
|
|
160
|
+
LoginGraceTime 120
|
|
161
|
+
PermitRootLogin ${SSHD_PERMIT_ROOT}
|
|
162
|
+
StrictModes no
|
|
76
163
|
PubkeyAuthentication yes
|
|
77
|
-
AuthorizedKeysFile .ssh/authorized_keys
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
164
|
+
AuthorizedKeysFile /root/.ssh/authorized_keys /var/lib/anaconda/ssh/authorized_keys /run/install/ssh/authorized_keys .ssh/authorized_keys
|
|
165
|
+
IgnoreRhosts yes
|
|
166
|
+
HostbasedAuthentication no
|
|
167
|
+
PermitEmptyPasswords no
|
|
168
|
+
PasswordAuthentication ${SSHD_PASSWORD_AUTH}
|
|
169
|
+
ChallengeResponseAuthentication ${SSHD_KBD_INTERACTIVE}
|
|
170
|
+
KbdInteractiveAuthentication ${SSHD_KBD_INTERACTIVE}
|
|
171
|
+
UsePAM no
|
|
172
|
+
X11Forwarding no
|
|
173
|
+
PrintMotd no
|
|
174
|
+
TCPKeepAlive yes
|
|
175
|
+
AcceptEnv LANG LC_*
|
|
82
176
|
Subsystem sftp /usr/libexec/openssh/sftp-server
|
|
177
|
+
UseDNS no
|
|
178
|
+
AllowTcpForwarding yes
|
|
179
|
+
ClientAliveInterval 30
|
|
180
|
+
ClientAliveCountMax 3
|
|
83
181
|
SSHEOF
|
|
84
|
-
fi
|
|
85
182
|
|
|
86
183
|
if [ -d /etc/ssh/sshd_config.d ]; then
|
|
87
|
-
|
|
88
|
-
PermitRootLogin yes
|
|
89
|
-
PasswordAuthentication yes
|
|
90
|
-
PubkeyAuthentication yes
|
|
91
|
-
KbdInteractiveAuthentication yes
|
|
92
|
-
SSHCONF
|
|
184
|
+
rm -f /etc/ssh/sshd_config.d/*.conf 2>/dev/null || true
|
|
93
185
|
fi
|
|
94
186
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
187
|
+
log "[sshd] Checking/Generating SSH host keys..."
|
|
188
|
+
|
|
189
|
+
_generate_host_key() {
|
|
190
|
+
local TYPE=$1
|
|
191
|
+
local FILE=$2
|
|
192
|
+
local BITS=$3
|
|
193
|
+
|
|
194
|
+
if [ -f "$FILE" ]; then
|
|
195
|
+
log "[sshd] Host key $FILE already exists, skipping."
|
|
196
|
+
return 0
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
log "[sshd] Generating $TYPE host key: $FILE ..."
|
|
200
|
+
|
|
201
|
+
if command -v ssh-keygen >/dev/null 2>&1; then
|
|
202
|
+
ssh-keygen -t "$TYPE" -f "$FILE" -N "" >> "$KS_LOG" 2>&1
|
|
203
|
+
[ -f "$FILE" ] && { log "[sshd] ssh-keygen succeeded for $TYPE"; return 0; }
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
207
|
+
case "$TYPE" in
|
|
208
|
+
rsa) openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:${BITS:-2048} -out "$FILE" >> "$KS_LOG" 2>&1 ;;
|
|
209
|
+
ecdsa) openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out "$FILE" >> "$KS_LOG" 2>&1 ;;
|
|
210
|
+
ed25519) openssl genpkey -algorithm ED25519 -out "$FILE" >> "$KS_LOG" 2>&1 ;;
|
|
211
|
+
esac
|
|
212
|
+
openssl pkey -in "$FILE" -pubout > "${FILE}.pub" 2>> "$KS_LOG"
|
|
213
|
+
[ -f "$FILE" ] && { log "[sshd] openssl succeeded for $TYPE"; return 0; }
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
log "[sshd] WARNING: key generation fallback for $TYPE (placeholder)"
|
|
217
|
+
dd if=/dev/urandom bs=1024 count=4 of="$FILE" 2>/dev/null
|
|
218
|
+
echo "placeholder public key" > "${FILE}.pub"
|
|
219
|
+
[ -f "$FILE" ]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_generate_host_key rsa /etc/ssh/ssh_host_rsa_key 2048
|
|
223
|
+
_generate_host_key ecdsa /etc/ssh/ssh_host_ecdsa_key 256
|
|
224
|
+
_generate_host_key ed25519 /etc/ssh/ssh_host_ed25519_key 256
|
|
225
|
+
|
|
226
|
+
chmod 600 /etc/ssh/ssh_host_*_key 2>/dev/null || true
|
|
227
|
+
chmod 644 /etc/ssh/ssh_host_*_key.pub 2>/dev/null || true
|
|
228
|
+
|
|
229
|
+
log "[sshd] Restarting sshd to apply key-only config..."
|
|
230
|
+
if command -v systemctl >/dev/null 2>&1; then
|
|
231
|
+
systemctl restart sshd 2>/dev/null || systemctl restart sshd.service 2>/dev/null || true
|
|
101
232
|
fi
|
|
233
|
+
if ! pidof sshd >/dev/null 2>&1; then
|
|
234
|
+
SSH_OLD_PID=$(pidof sshd 2>/dev/null | awk '{print $1}')
|
|
235
|
+
[ -n "$SSH_OLD_PID" ] && { kill "$SSH_OLD_PID" 2>/dev/null || true; sleep 1; }
|
|
236
|
+
/usr/sbin/sshd >> "$KS_LOG" 2>&1 || sshd >> "$KS_LOG" 2>&1 || true
|
|
237
|
+
fi
|
|
238
|
+
pidof sshd >/dev/null 2>&1 && log "[sshd] sshd running (pid: $(pidof sshd))" || log "[sshd] WARNING: sshd not running"
|
|
102
239
|
|
|
103
|
-
#
|
|
240
|
+
# Disable firewall so the controller can reach sshd / the install can fetch packages.
|
|
241
|
+
if command -v systemctl >/dev/null 2>&1; then
|
|
242
|
+
systemctl stop firewalld 2>/dev/null || systemctl stop iptables 2>/dev/null || true
|
|
243
|
+
systemctl disable firewalld 2>/dev/null || systemctl disable iptables 2>/dev/null || true
|
|
244
|
+
fi
|
|
245
|
+
firewall-cmd --set-default-zone=trusted 2>/dev/null || true
|
|
246
|
+
iptables -F 2>/dev/null || true
|
|
247
|
+
ip6tables -F 2>/dev/null || true
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
# 4. dnf repos (used by the live environment for the installer's package pull)
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
104
252
|
mkdir -p /var/lib/rpm /var/cache/dnf /var/log/dnf /etc/yum.repos.d /etc/pki/rpm-gpg /etc/dnf/vars
|
|
105
253
|
echo "9" > /etc/dnf/vars/releasever
|
|
106
254
|
|
|
@@ -120,44 +268,10 @@ fi
|
|
|
120
268
|
|
|
121
269
|
[ ! -f /etc/system-release ] && echo "Rocky Linux release 9 (Ephemeral)" > /etc/system-release
|
|
122
270
|
|
|
123
|
-
rpm --initdb 2>/dev/null || rpmdb --initdb 2>/dev/null || {
|
|
124
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
125
|
-
python3 -c "
|
|
126
|
-
import sqlite3, os
|
|
127
|
-
db_path = '/var/lib/rpm/rpmdb.sqlite'
|
|
128
|
-
if not os.path.exists(db_path):
|
|
129
|
-
conn = sqlite3.connect(db_path)
|
|
130
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Packages (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
131
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Name (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
132
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Basenames (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
133
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Installtid (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
134
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Providename (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
135
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Requirename (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
136
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Dirnames (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
137
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Sha1header (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
138
|
-
conn.execute('CREATE TABLE IF NOT EXISTS Sigmd5 (key BLOB NOT NULL, data BLOB NOT NULL)')
|
|
139
|
-
conn.commit()
|
|
140
|
-
conn.close()
|
|
141
|
-
" 2>/dev/null
|
|
142
|
-
fi
|
|
143
|
-
}
|
|
144
|
-
chmod -R 755 /var/lib/rpm 2>/dev/null
|
|
145
|
-
|
|
146
271
|
REPO_ARCH=$(uname -m)
|
|
147
272
|
|
|
148
273
|
rpm --import https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-9 2>/dev/null || \
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
cat > /etc/dnf/dnf.conf << 'DNFCONF'
|
|
152
|
-
[main]
|
|
153
|
-
gpgcheck=1
|
|
154
|
-
installonly_limit=3
|
|
155
|
-
clean_requirements_on_remove=True
|
|
156
|
-
best=True
|
|
157
|
-
skip_if_unavailable=True
|
|
158
|
-
install_weak_deps=False
|
|
159
|
-
tsflags=nodocs
|
|
160
|
-
DNFCONF
|
|
274
|
+
curl -fsSL https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-9 -o /etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 2>/dev/null || true
|
|
161
275
|
|
|
162
276
|
cat > /etc/yum.repos.d/rocky-baseos.repo << REPOEOF
|
|
163
277
|
[baseos]
|
|
@@ -181,114 +295,692 @@ gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9
|
|
|
181
295
|
skip_if_unavailable=1
|
|
182
296
|
REPOEOF2
|
|
183
297
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# 5. Bootstrap status POST helper (async, idempotent per stage, retry+backoff)
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
detect_ip() {
|
|
302
|
+
local ip=""
|
|
303
|
+
ip=$(ip addr show 2>/dev/null | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d/ -f1 | head -1)
|
|
304
|
+
[ -z "$ip" ] && ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
305
|
+
[ -z "$ip" ] && ip=$(grep -oP '32 host \K[0-9.]+' /proc/net/fib_trie 2>/dev/null | grep -v '127.0.0.1' | head -1)
|
|
306
|
+
[ -z "$ip" ] && ip="UNKNOWN"
|
|
307
|
+
echo "$ip"
|
|
308
|
+
}
|
|
194
309
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
enabled=0
|
|
201
|
-
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9
|
|
202
|
-
https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-9
|
|
203
|
-
skip_if_unavailable=1
|
|
204
|
-
REPOEOF4
|
|
310
|
+
detect_mac() {
|
|
311
|
+
local iface
|
|
312
|
+
iface=$(ip -o link show up 2>/dev/null | awk -F': ' '$2!="lo"{print $2; exit}')
|
|
313
|
+
[ -n "$iface" ] && cat "/sys/class/net/$iface/address" 2>/dev/null || echo "UNKNOWN"
|
|
314
|
+
}
|
|
205
315
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
316
|
+
detect_vendor_model() {
|
|
317
|
+
local vendor product
|
|
318
|
+
vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null | tr -d '\n')
|
|
319
|
+
product=$(cat /sys/class/dmi/id/product_name 2>/dev/null | tr -d '\n')
|
|
320
|
+
echo "${vendor} ${product}" | sed 's/^ *//;s/ *$//'
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Pure-bash HTTP POST over /dev/tcp. Last-resort transport when neither curl nor
|
|
324
|
+
# wget is present in the minimal Anaconda %pre environment.
|
|
325
|
+
_raw_http_post() {
|
|
326
|
+
local url="$1" body="$2"
|
|
327
|
+
local rest host port path
|
|
328
|
+
rest="${url#http://}"
|
|
329
|
+
path="/${rest#*/}"
|
|
330
|
+
rest="${rest%%/*}"
|
|
331
|
+
host="${rest%%:*}"
|
|
332
|
+
port="${rest##*:}"
|
|
333
|
+
[ "$host" = "$port" ] && port=80
|
|
334
|
+
exec 3<>"/dev/tcp/${host}/${port}" 2>/dev/null || return 1
|
|
335
|
+
printf 'POST %s HTTP/1.0\r\nHost: %s\r\nContent-Type: application/json\r\nContent-Length: %s\r\nConnection: close\r\n\r\n%s' \
|
|
336
|
+
"$path" "$host" "${#body}" "$body" >&3 2>/dev/null || { exec 3>&- 2>/dev/null; return 1; }
|
|
337
|
+
# Read (and discard) the response so the server flushes; ignore failures.
|
|
338
|
+
timeout 8 cat <&3 >/dev/null 2>&1
|
|
339
|
+
exec 3>&- 2>/dev/null
|
|
340
|
+
return 0
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# post_status <stage> [extra_json_fields]
|
|
344
|
+
# Posts a lifecycle event to ${BOOTSTRAP_URL}/status. Tries curl, then wget, then
|
|
345
|
+
# a raw /dev/tcp HTTP POST so a missing http client never silently drops events.
|
|
346
|
+
# Retries with backoff but never blocks installation forever (capped attempts).
|
|
347
|
+
post_status() {
|
|
348
|
+
local stage="$1"; shift
|
|
349
|
+
local extra="$1"
|
|
350
|
+
if [ -z "$BOOTSTRAP_URL" ]; then
|
|
351
|
+
log "[post] BOOTSTRAP_URL unset, skipping stage=$stage"
|
|
352
|
+
return 0
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
local ip mac model ts payload url
|
|
356
|
+
ip=$(detect_ip)
|
|
357
|
+
mac=$(detect_mac)
|
|
358
|
+
model=$(detect_vendor_model)
|
|
359
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
360
|
+
payload="{\"stage\":\"${stage}\",\"workflowId\":\"${WORKFLOW_ID}\",\"hostname\":\"${TARGET_HOSTNAME}\",\"systemId\":\"${SYSTEM_ID}\",\"ip\":\"${ip}\",\"mac\":\"${mac}\",\"sshPort\":${SSH_PORT},\"hardware\":\"${model}\",\"timestamp\":\"${ts}\"${extra:+,${extra}}}"
|
|
361
|
+
url="${BOOTSTRAP_URL%/}/status"
|
|
362
|
+
|
|
363
|
+
# Reference feedback on the physical console for every lifecycle stage.
|
|
364
|
+
console_log "stage=${stage} ip=${ip}${extra:+ ${extra}}"
|
|
365
|
+
|
|
366
|
+
log "[post] stage=$stage -> $url (curl=$(command -v curl >/dev/null 2>&1 && echo y || echo n) wget=$(command -v wget >/dev/null 2>&1 && echo y || echo n))"
|
|
367
|
+
log "[post] payload=$payload"
|
|
368
|
+
|
|
369
|
+
local attempt=1 max=4 delay=3
|
|
370
|
+
while [ "$attempt" -le "$max" ]; do
|
|
371
|
+
if command -v curl >/dev/null 2>&1 && \
|
|
372
|
+
curl -fsS -m 10 -X POST -H 'Content-Type: application/json' -d "$payload" "$url" >> "$KS_LOG" 2>&1; then
|
|
373
|
+
log "[post] stage=$stage delivered via curl (attempt $attempt)"
|
|
374
|
+
return 0
|
|
375
|
+
fi
|
|
376
|
+
if command -v wget >/dev/null 2>&1 && \
|
|
377
|
+
wget -q -T 10 -O - --header='Content-Type: application/json' --post-data="$payload" "$url" >> "$KS_LOG" 2>&1; then
|
|
378
|
+
log "[post] stage=$stage delivered via wget (attempt $attempt)"
|
|
379
|
+
return 0
|
|
380
|
+
fi
|
|
381
|
+
if _raw_http_post "$url" "$payload"; then
|
|
382
|
+
log "[post] stage=$stage delivered via /dev/tcp (attempt $attempt)"
|
|
383
|
+
return 0
|
|
384
|
+
fi
|
|
385
|
+
log "[post] stage=$stage attempt $attempt/$max failed; retrying in ${delay}s"
|
|
386
|
+
sleep "$delay"
|
|
387
|
+
delay=$((delay * 2))
|
|
388
|
+
attempt=$((attempt + 1))
|
|
389
|
+
done
|
|
390
|
+
log "[post] stage=$stage giving up after $max attempts (non-fatal)"
|
|
391
|
+
return 1
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# Early heartbeat: announce the ephemeral runtime is up and that %pre reached the
|
|
395
|
+
# networking/POST stage. If even this never reaches the controller, the problem
|
|
396
|
+
# is transport/network — not disk detection or sshd.
|
|
397
|
+
post_status "ephemeral-boot" "\"detail\":\"%pre reached network/post stage\""
|
|
398
|
+
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
# 6. Safe install-disk detection for a real physical machine
|
|
401
|
+
# ---------------------------------------------------------------------------
|
|
402
|
+
# Rules (deterministic):
|
|
403
|
+
# - honor INSTALL_DISK_HINT if it is a valid block device
|
|
404
|
+
# - only whole disks (TYPE=disk), writable (RO=0), non-removable (RM=0)
|
|
405
|
+
# - skip USB-attached media (TRAN=usb) and zram/loop/ram devices
|
|
406
|
+
# - skip the device backing the live/ephemeral root, if any is disk-backed
|
|
407
|
+
# - among the remaining, prefer internal bus order (nvme > sata/sas) then
|
|
408
|
+
# largest size, and pick the smallest stable /dev/disk/by-id name for it
|
|
409
|
+
detect_install_disk() {
|
|
410
|
+
if [ -n "$INSTALL_DISK_HINT" ] && [ -b "$INSTALL_DISK_HINT" ]; then
|
|
411
|
+
echo "$INSTALL_DISK_HINT"
|
|
412
|
+
return 0
|
|
413
|
+
fi
|
|
414
|
+
|
|
415
|
+
# Device currently backing the live root filesystem (avoid wiping ourselves).
|
|
416
|
+
local live_src live_disk=""
|
|
417
|
+
live_src=$(findmnt -n -o SOURCE / 2>/dev/null)
|
|
418
|
+
if [ -n "$live_src" ] && [ -b "$live_src" ]; then
|
|
419
|
+
live_disk=$(lsblk -no PKNAME "$live_src" 2>/dev/null | head -1)
|
|
420
|
+
[ -z "$live_disk" ] && live_disk=$(basename "$live_src")
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
# Emit "score name size" per candidate, sort, take the top.
|
|
424
|
+
local best=""
|
|
425
|
+
best=$(lsblk -dn -o NAME,TYPE,SIZE,RM,RO,TRAN -b 2>/dev/null | while read -r name type size rm ro tran; do
|
|
426
|
+
[ "$type" = "disk" ] || continue
|
|
427
|
+
[ "$ro" = "0" ] || continue
|
|
428
|
+
[ "$rm" = "0" ] || continue
|
|
429
|
+
case "$name" in
|
|
430
|
+
loop*|ram*|zram*|sr*|fd*) continue ;;
|
|
431
|
+
esac
|
|
432
|
+
[ "$tran" = "usb" ] && continue
|
|
433
|
+
[ -n "$live_disk" ] && [ "$name" = "$live_disk" ] && continue
|
|
434
|
+
|
|
435
|
+
# Bus preference weight (higher = preferred), then size as tiebreaker.
|
|
436
|
+
local weight=0
|
|
437
|
+
case "$tran" in
|
|
438
|
+
nvme) weight=3 ;;
|
|
439
|
+
sata|sas|ata) weight=2 ;;
|
|
440
|
+
*) weight=1 ;;
|
|
441
|
+
esac
|
|
442
|
+
# score = weight * 1e15 + size ; keeps weight dominant, size as tiebreak
|
|
443
|
+
printf '%d %s\n' "$(( weight * 1000000000000000 + ${size:-0} ))" "/dev/$name"
|
|
444
|
+
done | sort -rn | head -1 | awk '{print $2}')
|
|
445
|
+
|
|
446
|
+
[ -z "$best" ] && return 1
|
|
447
|
+
|
|
448
|
+
# Prefer a stable /dev/disk/by-id symlink for the chosen device.
|
|
449
|
+
local byid
|
|
450
|
+
byid=$(for l in /dev/disk/by-id/*; do
|
|
451
|
+
[ -e "$l" ] || continue
|
|
452
|
+
case "$l" in *-part*) continue ;; esac
|
|
453
|
+
if [ "$(readlink -f "$l")" = "$(readlink -f "$best")" ]; then
|
|
454
|
+
echo "$l"
|
|
455
|
+
fi
|
|
456
|
+
done 2>/dev/null | grep -v '/wwn-' | head -1)
|
|
457
|
+
|
|
458
|
+
[ -n "$byid" ] && echo "$byid" || echo "$best"
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# ---------------------------------------------------------------------------
|
|
462
|
+
# 7. Write the unattended installer (run by the controller over SSH, or by the
|
|
463
|
+
# AUTO_INSTALL fallback). It detects the disk, installs Rocky 9, posts
|
|
464
|
+
# completion, and reboots into the deployed OS.
|
|
465
|
+
# ---------------------------------------------------------------------------
|
|
466
|
+
mkdir -p /usr/local/bin
|
|
467
|
+
cat > "$INSTALLER" << INSTALLEOF
|
|
468
|
+
#!/bin/bash
|
|
469
|
+
# Underpost unattended Rocky Linux 9 installer (runs inside the ephemeral env).
|
|
470
|
+
set +e
|
|
471
|
+
|
|
472
|
+
KS_LOG="$KS_LOG"
|
|
473
|
+
INSTALL_LOCK="$INSTALL_LOCK"
|
|
474
|
+
INSTALL_DONE="$INSTALL_DONE"
|
|
475
|
+
TARGET_MNT="$TARGET_MNT"
|
|
476
|
+
AUTHORIZED_KEYS='$AUTHORIZED_KEYS'
|
|
477
|
+
ADMIN_USER='$ADMIN_USER'
|
|
478
|
+
DEPLOY_USER='$DEPLOY_USER'
|
|
479
|
+
TARGET_HOSTNAME='$TARGET_HOSTNAME'
|
|
480
|
+
NET_IP='$NET_IP'
|
|
481
|
+
NET_PREFIX='$NET_PREFIX'
|
|
482
|
+
NET_GATEWAY='$NET_GATEWAY'
|
|
483
|
+
NET_DNS='$NET_DNS'
|
|
484
|
+
TIMEZONE='$TIMEZONE'
|
|
485
|
+
KEYBOARD_LAYOUT='$KEYBOARD_LAYOUT'
|
|
486
|
+
CHRONY_CONF_PATH='$CHRONY_CONF_PATH'
|
|
487
|
+
# Passwords are carried base64-encoded so any character survives intact. Decoding
|
|
488
|
+
# tries base64 then python3 so a minimal environment never yields empty passwords.
|
|
489
|
+
ks_b64d() { printf %s "\$1" | base64 -d 2>/dev/null || printf %s "\$1" | python3 -c 'import sys,base64;sys.stdout.buffer.write(base64.b64decode(sys.stdin.read().strip()))' 2>/dev/null; }
|
|
490
|
+
ROOT_PASS_B64='$ROOT_PASS_B64'
|
|
491
|
+
ADMIN_PASS_B64='$ADMIN_PASS_B64'
|
|
492
|
+
DEPLOY_PASS_B64='$DEPLOY_PASS_B64'
|
|
493
|
+
ROOT_PASS="\$(ks_b64d "\$ROOT_PASS_B64")"
|
|
494
|
+
ADMIN_PASS="\$(ks_b64d "\$ADMIN_PASS_B64")"
|
|
495
|
+
DEPLOY_PASS="\$(ks_b64d "\$DEPLOY_PASS_B64")"
|
|
496
|
+
[ -z "\$ADMIN_PASS" ] && ADMIN_PASS="\$ROOT_PASS"
|
|
497
|
+
[ -z "\$DEPLOY_PASS" ] && DEPLOY_PASS="\$ROOT_PASS"
|
|
498
|
+
|
|
499
|
+
ilog() {
|
|
500
|
+
echo "\$(date): [install] \$*" | tee -a "\$KS_LOG"
|
|
501
|
+
printf "[underpost-install] %s\r\n" "\$*" > /dev/console 2>/dev/null || true
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
# Prove the installer actually launched (distinguishes "never started" from
|
|
505
|
+
# "started and exited early"). Posted before any guard so it always fires once.
|
|
506
|
+
ilog "installer process started (pid \$\$)"
|
|
507
|
+
post_status "install-start" "\"detail\":\"installer launched\""
|
|
215
508
|
|
|
216
|
-
|
|
217
|
-
|
|
509
|
+
# Single-flight guard.
|
|
510
|
+
if [ -f "\$INSTALL_LOCK" ]; then
|
|
511
|
+
ilog "installer already running (lock present), exiting"
|
|
512
|
+
exit 0
|
|
513
|
+
fi
|
|
514
|
+
if [ -f "\$INSTALL_DONE" ]; then
|
|
515
|
+
ilog "install already completed, exiting"
|
|
516
|
+
exit 0
|
|
517
|
+
fi
|
|
518
|
+
touch "\$INSTALL_LOCK"
|
|
218
519
|
|
|
219
|
-
|
|
520
|
+
post_status "installing" "\"detail\":\"partitioning\""
|
|
220
521
|
|
|
221
|
-
|
|
222
|
-
if !
|
|
223
|
-
|
|
224
|
-
|
|
522
|
+
DISK="\$(detect_install_disk)"
|
|
523
|
+
if [ -z "\$DISK" ] || [ ! -b "\$DISK" ]; then
|
|
524
|
+
ilog "FATAL: no valid install disk detected"
|
|
525
|
+
post_status "failed" "\"detail\":\"no install disk detected\""
|
|
526
|
+
rm -f "\$INSTALL_LOCK"
|
|
527
|
+
exit 1
|
|
225
528
|
fi
|
|
529
|
+
REAL_DISK="\$(readlink -f "\$DISK")"
|
|
530
|
+
ilog "Selected install disk: \$DISK (\$REAL_DISK)"
|
|
531
|
+
post_status "installing" "\"detail\":\"target \$REAL_DISK\",\"disk\":\"\$REAL_DISK\""
|
|
226
532
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
533
|
+
# Detect firmware mode.
|
|
534
|
+
if [ -d /sys/firmware/efi ]; then
|
|
535
|
+
FW="uefi"
|
|
536
|
+
else
|
|
537
|
+
FW="bios"
|
|
231
538
|
fi
|
|
539
|
+
ilog "Firmware mode: \$FW"
|
|
540
|
+
|
|
541
|
+
# Partition naming helper (nvme0n1p1 vs sda1).
|
|
542
|
+
part() {
|
|
543
|
+
case "\$REAL_DISK" in
|
|
544
|
+
*[0-9]) echo "\${REAL_DISK}p\$1" ;;
|
|
545
|
+
*) echo "\${REAL_DISK}\$1" ;;
|
|
546
|
+
esac
|
|
547
|
+
}
|
|
232
548
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
549
|
+
ilog "Wiping and partitioning \$REAL_DISK ..."
|
|
550
|
+
umount -R "\$TARGET_MNT" 2>/dev/null || true
|
|
551
|
+
swapoff -a 2>/dev/null || true
|
|
552
|
+
wipefs -a "\$REAL_DISK" >> "\$KS_LOG" 2>&1
|
|
553
|
+
sgdisk --zap-all "\$REAL_DISK" >> "\$KS_LOG" 2>&1 || dd if=/dev/zero of="\$REAL_DISK" bs=1M count=10 2>/dev/null
|
|
554
|
+
|
|
555
|
+
if [ "\$FW" = "uefi" ]; then
|
|
556
|
+
sgdisk -n 1:0:+600M -t 1:ef00 -c 1:"EFI System" "\$REAL_DISK" >> "\$KS_LOG" 2>&1
|
|
557
|
+
sgdisk -n 2:0:+1024M -t 2:8300 -c 2:"boot" "\$REAL_DISK" >> "\$KS_LOG" 2>&1
|
|
558
|
+
sgdisk -n 3:0:0 -t 3:8300 -c 3:"root" "\$REAL_DISK" >> "\$KS_LOG" 2>&1
|
|
559
|
+
EFI_PART="\$(part 1)"; BOOT_PART="\$(part 2)"; ROOT_PART="\$(part 3)"
|
|
560
|
+
else
|
|
561
|
+
sgdisk -n 1:0:+2M -t 1:ef02 -c 1:"BIOS boot" "\$REAL_DISK" >> "\$KS_LOG" 2>&1
|
|
562
|
+
sgdisk -n 2:0:+1024M -t 2:8300 -c 2:"boot" "\$REAL_DISK" >> "\$KS_LOG" 2>&1
|
|
563
|
+
sgdisk -n 3:0:0 -t 3:8300 -c 3:"root" "\$REAL_DISK" >> "\$KS_LOG" 2>&1
|
|
564
|
+
BOOT_PART="\$(part 2)"; ROOT_PART="\$(part 3)"
|
|
238
565
|
fi
|
|
239
|
-
PROFILEEOF
|
|
240
|
-
chown $ADMIN_USER:$ADMIN_USER /home/$ADMIN_USER/.bash_profile 2>/dev/null || true
|
|
241
566
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
567
|
+
partprobe "\$REAL_DISK" >> "\$KS_LOG" 2>&1 || true
|
|
568
|
+
udevadm settle 2>/dev/null || sleep 3
|
|
569
|
+
|
|
570
|
+
ilog "Creating filesystems ..."
|
|
571
|
+
[ "\$FW" = "uefi" ] && mkfs.vfat -F32 "\$EFI_PART" >> "\$KS_LOG" 2>&1
|
|
572
|
+
mkfs.xfs -f "\$BOOT_PART" >> "\$KS_LOG" 2>&1
|
|
573
|
+
mkfs.xfs -f "\$ROOT_PART" >> "\$KS_LOG" 2>&1
|
|
574
|
+
|
|
575
|
+
ilog "Mounting target ..."
|
|
576
|
+
mkdir -p "\$TARGET_MNT"
|
|
577
|
+
mount "\$ROOT_PART" "\$TARGET_MNT" || { ilog "FATAL: mount root failed"; post_status "failed" "\"detail\":\"mount root failed\""; rm -f "\$INSTALL_LOCK"; exit 1; }
|
|
578
|
+
mkdir -p "\$TARGET_MNT/boot"
|
|
579
|
+
mount "\$BOOT_PART" "\$TARGET_MNT/boot"
|
|
580
|
+
if [ "\$FW" = "uefi" ]; then
|
|
581
|
+
mkdir -p "\$TARGET_MNT/boot/efi"
|
|
582
|
+
mount "\$EFI_PART" "\$TARGET_MNT/boot/efi"
|
|
245
583
|
fi
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
584
|
+
|
|
585
|
+
post_status "installing" "\"detail\":\"installing packages\",\"disk\":\"\$REAL_DISK\""
|
|
586
|
+
|
|
587
|
+
ilog "Installing base packages with dnf --installroot ..."
|
|
588
|
+
PKGS="@core kernel grub2-tools openssh-server NetworkManager chrony dracut-config-generic rocky-release"
|
|
589
|
+
if [ "\$FW" = "uefi" ]; then
|
|
590
|
+
PKGS="\$PKGS grub2-efi-x64 shim-x64 efibootmgr"
|
|
591
|
+
else
|
|
592
|
+
PKGS="\$PKGS grub2-pc"
|
|
593
|
+
fi
|
|
594
|
+
|
|
595
|
+
dnf -y --installroot="\$TARGET_MNT" --releasever=9 \
|
|
596
|
+
--setopt=install_weak_deps=False --nogpgcheck \
|
|
597
|
+
install \$PKGS >> "\$KS_LOG" 2>&1
|
|
598
|
+
DNF_RC=\$?
|
|
599
|
+
if [ "\$DNF_RC" -ne 0 ]; then
|
|
600
|
+
ilog "FATAL: dnf install failed (rc=\$DNF_RC)"
|
|
601
|
+
post_status "failed" "\"detail\":\"dnf install rc=\$DNF_RC\""
|
|
602
|
+
rm -f "\$INSTALL_LOCK"
|
|
603
|
+
exit 1
|
|
604
|
+
fi
|
|
605
|
+
|
|
606
|
+
ilog "Generating fstab ..."
|
|
607
|
+
ROOT_UUID=\$(blkid -s UUID -o value "\$ROOT_PART")
|
|
608
|
+
BOOT_UUID=\$(blkid -s UUID -o value "\$BOOT_PART")
|
|
609
|
+
{
|
|
610
|
+
echo "UUID=\$ROOT_UUID / xfs defaults 0 0"
|
|
611
|
+
echo "UUID=\$BOOT_UUID /boot xfs defaults 0 0"
|
|
612
|
+
if [ "\$FW" = "uefi" ]; then
|
|
613
|
+
EFI_UUID=\$(blkid -s UUID -o value "\$EFI_PART")
|
|
614
|
+
echo "UUID=\$EFI_UUID /boot/efi vfat umask=0077,shortname=winnt 0 2"
|
|
615
|
+
fi
|
|
616
|
+
} > "\$TARGET_MNT/etc/fstab"
|
|
617
|
+
|
|
618
|
+
# Bind mounts for chroot.
|
|
619
|
+
for d in dev proc sys run; do mount --bind /\$d "\$TARGET_MNT/\$d"; done
|
|
620
|
+
|
|
621
|
+
ilog "Configuring installed system (hostname, network, ssh, users) ..."
|
|
622
|
+
echo "\${TARGET_HOSTNAME:-rocky9}" > "\$TARGET_MNT/etc/hostname"
|
|
623
|
+
|
|
624
|
+
chroot "\$TARGET_MNT" /bin/bash -s << CHROOTEOF >> "\$KS_LOG" 2>&1
|
|
625
|
+
set +e
|
|
626
|
+
systemctl enable sshd NetworkManager chronyd 2>/dev/null
|
|
627
|
+
|
|
628
|
+
# SELinux -> permissive in the DEPLOYED OS. Critical: with SELinux enforcing on
|
|
629
|
+
# first boot, files written from this installer chroot (authorized_keys, home
|
|
630
|
+
# dirs) carry the wrong security context, so sshd drops sessions ("Connection
|
|
631
|
+
# reset by peer") and PAM can block console logins. Permissive ignores contexts
|
|
632
|
+
# (no relabel reboot needed) and matches what kubeadm/cluster.js expect.
|
|
633
|
+
sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config 2>/dev/null || true
|
|
634
|
+
|
|
635
|
+
# Ensure sshd host keys exist so sshd actually starts on first boot.
|
|
636
|
+
ssh-keygen -A 2>/dev/null || true
|
|
637
|
+
|
|
638
|
+
# Disable firewalld so the controller can reach sshd on first boot.
|
|
639
|
+
systemctl disable firewalld 2>/dev/null || true
|
|
640
|
+
systemctl mask firewalld 2>/dev/null || true
|
|
641
|
+
|
|
642
|
+
# Static networking bound to the primary wired NIC by MAC so the deployed OS is
|
|
643
|
+
# reachable at the same IP the commission used (NetworkManager keyfile).
|
|
644
|
+
if [ -n "\${NET_IP}" ]; then
|
|
645
|
+
PRIMARY_IF=\\\$(ls /sys/class/net | grep -E '^(en|eth)' | head -1)
|
|
646
|
+
PRIMARY_MAC=\\\$(cat /sys/class/net/\\\$PRIMARY_IF/address 2>/dev/null)
|
|
647
|
+
mkdir -p /etc/NetworkManager/system-connections
|
|
648
|
+
NMFILE=/etc/NetworkManager/system-connections/underpost.nmconnection
|
|
649
|
+
{
|
|
650
|
+
echo "[connection]"
|
|
651
|
+
echo "id=underpost"
|
|
652
|
+
echo "type=ethernet"
|
|
653
|
+
echo "autoconnect=true"
|
|
654
|
+
echo "autoconnect-priority=999"
|
|
655
|
+
echo "[ethernet]"
|
|
656
|
+
[ -n "\\\$PRIMARY_MAC" ] && echo "mac-address=\\\$PRIMARY_MAC"
|
|
657
|
+
echo "[ipv4]"
|
|
658
|
+
echo "method=manual"
|
|
659
|
+
echo "addresses=\${NET_IP}/\${NET_PREFIX}"
|
|
660
|
+
echo "gateway=\${NET_GATEWAY}"
|
|
661
|
+
echo "dns=\${NET_DNS};"
|
|
662
|
+
echo "[ipv6]"
|
|
663
|
+
echo "method=ignore"
|
|
664
|
+
} > "\\\$NMFILE"
|
|
665
|
+
chmod 600 "\\\$NMFILE"
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
# Timezone + NTP (chrony) for the deployed OS. Mirrors the Rocky branch of
|
|
669
|
+
# src/cli/system.js (rocky.timezone): localtime symlink, /etc/timezone, a chrony
|
|
670
|
+
# config with a local + public NTP pool, and chronyd enabled on boot.
|
|
671
|
+
if [ -n "\${TIMEZONE}" ]; then
|
|
672
|
+
ln -sf /usr/share/zoneinfo/\${TIMEZONE} /etc/localtime
|
|
673
|
+
echo "\${TIMEZONE}" > /etc/timezone
|
|
674
|
+
timedatectl set-timezone \${TIMEZONE} 2>/dev/null || true
|
|
675
|
+
{
|
|
676
|
+
echo "# Underpost-managed chrony configuration"
|
|
677
|
+
[ -n "\${NET_GATEWAY}" ] && echo "server \${NET_GATEWAY} iburst prefer"
|
|
678
|
+
echo "server 0.pool.ntp.org iburst"
|
|
679
|
+
echo "server 1.pool.ntp.org iburst"
|
|
680
|
+
echo "server 2.pool.ntp.org iburst"
|
|
681
|
+
echo "server 3.pool.ntp.org iburst"
|
|
682
|
+
echo "driftfile /var/lib/chrony/drift"
|
|
683
|
+
echo "makestep 1.0 3"
|
|
684
|
+
echo "rtcsync"
|
|
685
|
+
echo "logdir /var/log/chrony"
|
|
686
|
+
} > "\${CHRONY_CONF_PATH:-/etc/chrony.conf}"
|
|
687
|
+
systemctl enable chronyd 2>/dev/null || true
|
|
688
|
+
fi
|
|
689
|
+
|
|
690
|
+
# Keyboard layout + locale for the deployed OS. Mirrors the Rocky branch of
|
|
691
|
+
# src/cli/system.js (rocky.keyboard): vconsole.conf, locale.conf, X11 keymap.
|
|
692
|
+
if [ -n "\${KEYBOARD_LAYOUT}" ]; then
|
|
693
|
+
echo "KEYMAP=\${KEYBOARD_LAYOUT}" > /etc/vconsole.conf
|
|
694
|
+
echo "FONT=latarcyrheb-sun16" >> /etc/vconsole.conf
|
|
695
|
+
echo "LANG=en_US.UTF-8" > /etc/locale.conf
|
|
696
|
+
mkdir -p /etc/X11/xorg.conf.d
|
|
697
|
+
{
|
|
698
|
+
echo 'Section "InputClass"'
|
|
699
|
+
echo ' Identifier "system-keyboard"'
|
|
700
|
+
echo ' MatchIsKeyboard "on"'
|
|
701
|
+
echo ' Option "XkbLayout" "\${KEYBOARD_LAYOUT}"'
|
|
702
|
+
echo 'EndSection'
|
|
703
|
+
} > /etc/X11/xorg.conf.d/00-keyboard.conf
|
|
704
|
+
localectl set-keymap \${KEYBOARD_LAYOUT} 2>/dev/null || true
|
|
705
|
+
localectl set-x11-keymap \${KEYBOARD_LAYOUT} 2>/dev/null || true
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
# Key-only SSH on the installed system.
|
|
709
|
+
mkdir -p /etc/ssh/sshd_config.d
|
|
710
|
+
cat > /etc/ssh/sshd_config.d/00-underpost.conf <<SSHDEOF
|
|
711
|
+
PermitRootLogin prohibit-password
|
|
712
|
+
PubkeyAuthentication yes
|
|
713
|
+
PasswordAuthentication no
|
|
714
|
+
SSHDEOF
|
|
715
|
+
|
|
716
|
+
# Create the admin login accounts. create_user <name> writes the authorized key,
|
|
717
|
+
# wheel membership and passwordless sudo. Passwords are applied after the chroot.
|
|
718
|
+
create_user() {
|
|
719
|
+
[ -z "\\\$1" ] && return 0
|
|
720
|
+
id "\\\$1" >/dev/null 2>&1 || useradd -m -G wheel "\\\$1"
|
|
721
|
+
usermod -aG wheel "\\\$1" 2>/dev/null || true
|
|
722
|
+
mkdir -p /home/\\\$1/.ssh && chmod 700 /home/\\\$1/.ssh
|
|
723
|
+
cat > /home/\\\$1/.ssh/authorized_keys <<KEYEOF
|
|
724
|
+
\${AUTHORIZED_KEYS}
|
|
725
|
+
KEYEOF
|
|
726
|
+
chmod 600 /home/\\\$1/.ssh/authorized_keys
|
|
727
|
+
chown -R "\\\$1:\\\$1" /home/\\\$1/.ssh
|
|
728
|
+
echo "\\\$1 ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/\\\$1
|
|
729
|
+
chmod 0440 /etc/sudoers.d/\\\$1
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
|
733
|
+
cat > /root/.ssh/authorized_keys <<KEYEOF3
|
|
734
|
+
\${AUTHORIZED_KEYS}
|
|
735
|
+
KEYEOF3
|
|
736
|
+
chmod 600 /root/.ssh/authorized_keys
|
|
737
|
+
|
|
738
|
+
create_user "\${ADMIN_USER}"
|
|
739
|
+
create_user "\${DEPLOY_USER}"
|
|
740
|
+
|
|
741
|
+
# Build initramfs for the installed kernel.
|
|
742
|
+
KVER=\\\$(ls /lib/modules | head -1)
|
|
743
|
+
[ -n "\\\$KVER" ] && dracut -f /boot/initramfs-\\\$KVER.img "\\\$KVER"
|
|
744
|
+
CHROOTEOF
|
|
745
|
+
|
|
746
|
+
# Set console passwords AFTER the chroot heredoc, from decoded installer vars.
|
|
747
|
+
# Primary path: pipe 'user:password' into 'chroot chpasswd' (keeps password
|
|
748
|
+
# characters out of any heredoc). Fallback: hash with openssl and apply via
|
|
749
|
+
# 'usermod -p' so a chpasswd/PAM quirk in the installroot never leaves an
|
|
750
|
+
# account locked. Verifies the shadow field is a real hash afterwards.
|
|
751
|
+
set_password() {
|
|
752
|
+
local u="\$1" p="\$2"
|
|
753
|
+
[ -z "\$u" ] || [ -z "\$p" ] && return 0
|
|
754
|
+
printf '%s:%s\n' "\$u" "\$p" | chroot "\$TARGET_MNT" chpasswd 2>>"\$KS_LOG"
|
|
755
|
+
local field
|
|
756
|
+
field=\$(chroot "\$TARGET_MNT" getent shadow "\$u" 2>/dev/null | cut -d: -f2)
|
|
757
|
+
case "\$field" in
|
|
758
|
+
'\$'*) ilog "\$u password set (chpasswd)"; return 0 ;;
|
|
759
|
+
esac
|
|
760
|
+
local hash
|
|
761
|
+
hash=\$(printf %s "\$p" | openssl passwd -6 -stdin 2>/dev/null)
|
|
762
|
+
if [ -n "\$hash" ]; then
|
|
763
|
+
chroot "\$TARGET_MNT" usermod -p "\$hash" "\$u" 2>>"\$KS_LOG" && ilog "\$u password set (openssl hash fallback)"
|
|
764
|
+
else
|
|
765
|
+
ilog "WARNING: failed to set password for \$u"
|
|
766
|
+
fi
|
|
767
|
+
}
|
|
768
|
+
set_password root "\$ROOT_PASS"
|
|
769
|
+
set_password "\$ADMIN_USER" "\$ADMIN_PASS"
|
|
770
|
+
set_password "\$DEPLOY_USER" "\$DEPLOY_PASS"
|
|
771
|
+
|
|
772
|
+
post_status "installing" "\"detail\":\"installing bootloader\",\"disk\":\"\$REAL_DISK\""
|
|
773
|
+
|
|
774
|
+
ilog "Installing bootloader (\$FW) on \$REAL_DISK ..."
|
|
775
|
+
if [ "\$FW" = "uefi" ]; then
|
|
776
|
+
chroot "\$TARGET_MNT" grub2-mkconfig -o /boot/grub2/grub.cfg >> "\$KS_LOG" 2>&1
|
|
777
|
+
chroot "\$TARGET_MNT" mkdir -p /boot/efi/EFI/rocky
|
|
778
|
+
chroot "\$TARGET_MNT" grub2-mkconfig -o /boot/efi/EFI/rocky/grub.cfg >> "\$KS_LOG" 2>&1
|
|
779
|
+
# Drop any stale Rocky Linux UEFI entries to avoid duplicates.
|
|
780
|
+
for b in \$(chroot "\$TARGET_MNT" efibootmgr 2>/dev/null | awk '/Rocky Linux/{print substr(\$1,5,4)}'); do
|
|
781
|
+
chroot "\$TARGET_MNT" efibootmgr -b "\$b" -B >> "\$KS_LOG" 2>&1
|
|
782
|
+
done
|
|
783
|
+
# Create the boot entry (also prepends it to BootOrder).
|
|
784
|
+
chroot "\$TARGET_MNT" efibootmgr -c -d "\$REAL_DISK" -p 1 -L "Rocky Linux" -l "\\\\EFI\\\\rocky\\\\shimx64.efi" >> "\$KS_LOG" 2>&1
|
|
785
|
+
BOOT_RC=\$?
|
|
786
|
+
# Set BootNext so the immediate post-install reboot boots the DISK, not the
|
|
787
|
+
# USB iPXE installer — this overrides a USB-first firmware order for one boot,
|
|
788
|
+
# so no manual BIOS change is needed to land in the freshly installed OS.
|
|
789
|
+
NEW_BOOT=\$(chroot "\$TARGET_MNT" efibootmgr 2>/dev/null | awk '/Rocky Linux/{print substr(\$1,5,4); exit}')
|
|
790
|
+
if [ -n "\$NEW_BOOT" ]; then
|
|
791
|
+
chroot "\$TARGET_MNT" efibootmgr -n "\$NEW_BOOT" >> "\$KS_LOG" 2>&1
|
|
792
|
+
ilog "Set UEFI BootNext=\$NEW_BOOT (Rocky Linux on \$REAL_DISK); remove USB after this boot to stay on disk"
|
|
793
|
+
fi
|
|
794
|
+
else
|
|
795
|
+
chroot "\$TARGET_MNT" /bin/bash -c "grub2-install --target=i386-pc \$REAL_DISK && \
|
|
796
|
+
grub2-mkconfig -o /boot/grub2/grub.cfg" >> "\$KS_LOG" 2>&1
|
|
797
|
+
BOOT_RC=\$?
|
|
250
798
|
fi
|
|
251
799
|
|
|
252
|
-
#
|
|
253
|
-
|
|
800
|
+
# Teardown mounts.
|
|
801
|
+
for d in dev proc sys run; do umount -l "\$TARGET_MNT/\$d" 2>/dev/null; done
|
|
802
|
+
umount -R "\$TARGET_MNT" 2>/dev/null || true
|
|
803
|
+
|
|
804
|
+
if [ "\$BOOT_RC" -ne 0 ]; then
|
|
805
|
+
ilog "FATAL: bootloader install failed (rc=\$BOOT_RC)"
|
|
806
|
+
post_status "failed" "\"detail\":\"bootloader rc=\$BOOT_RC\""
|
|
807
|
+
rm -f "\$INSTALL_LOCK"
|
|
808
|
+
exit 1
|
|
809
|
+
fi
|
|
810
|
+
|
|
811
|
+
touch "\$INSTALL_DONE"
|
|
812
|
+
ilog "Install completed on \$REAL_DISK. Rebooting into deployed OS (BootNext set to disk)."
|
|
813
|
+
ilog "If it boots the USB again, remove the USB stick now — the OS is on \$REAL_DISK."
|
|
814
|
+
ilog "Console login: 'root', '\${ADMIN_USER}'\$([ -n "\$DEPLOY_USER" ] && echo " or '\$DEPLOY_USER'") (password \$([ -n "\$ROOT_PASS" ] && echo set || echo NOT-set)). SSH is key-only at \${NET_IP:-dhcp}."
|
|
815
|
+
post_status "completed" "\"detail\":\"install ok, rebooting into disk\",\"disk\":\"\$REAL_DISK\",\"loginUser\":\"\${ADMIN_USER}\",\"deployUser\":\"\${DEPLOY_USER}\",\"passwordSet\":\$([ -n "\$ROOT_PASS" ] && echo true || echo false),\"staticIp\":\"\${NET_IP}\",\"bootNext\":\"\${NEW_BOOT:-}\""
|
|
816
|
+
rm -f "\$INSTALL_LOCK"
|
|
817
|
+
sync
|
|
818
|
+
sleep 3
|
|
819
|
+
reboot -f || systemctl reboot || echo b > /proc/sysrq-trigger
|
|
820
|
+
INSTALLEOF
|
|
821
|
+
chmod +x "$INSTALLER"
|
|
822
|
+
log "[install] Wrote unattended installer to $INSTALLER"
|
|
823
|
+
|
|
824
|
+
# Export helpers used by the installer (it sources nothing; functions are
|
|
825
|
+
# re-declared above). The installer relies on post_status/detect_install_disk
|
|
826
|
+
# being defined in its own process, so embed minimal copies inline.
|
|
827
|
+
# To keep one source of truth, re-export them via a sourced fragment.
|
|
828
|
+
cat > /usr/local/bin/underpost-install-helpers.sh << HELPEREOF
|
|
829
|
+
$(declare -f log)
|
|
830
|
+
$(declare -f console_log)
|
|
831
|
+
$(declare -f detect_ip)
|
|
832
|
+
$(declare -f detect_mac)
|
|
833
|
+
$(declare -f detect_vendor_model)
|
|
834
|
+
$(declare -f post_status)
|
|
835
|
+
$(declare -f detect_install_disk)
|
|
836
|
+
BOOTSTRAP_URL='$BOOTSTRAP_URL'
|
|
837
|
+
WORKFLOW_ID='$WORKFLOW_ID'
|
|
838
|
+
SYSTEM_ID='$SYSTEM_ID'
|
|
839
|
+
TARGET_HOSTNAME='$TARGET_HOSTNAME'
|
|
840
|
+
SSH_PORT='$SSH_PORT'
|
|
841
|
+
INSTALL_DISK_HINT='$INSTALL_DISK_HINT'
|
|
842
|
+
HELPEREOF
|
|
843
|
+
# Prepend a source of the helpers into the installer so its post_status /
|
|
844
|
+
# detect_install_disk calls resolve.
|
|
845
|
+
sed -i "2i source /usr/local/bin/underpost-install-helpers.sh" "$INSTALLER"
|
|
846
|
+
|
|
847
|
+
# ---------------------------------------------------------------------------
|
|
848
|
+
# 8. Physical console banner + SSH-ready handshake
|
|
849
|
+
# ---------------------------------------------------------------------------
|
|
850
|
+
DETECTED_IP=$(detect_ip)
|
|
254
851
|
|
|
255
852
|
echo ""
|
|
256
853
|
echo "=============================================="
|
|
257
854
|
echo "Underpost: Ephemeral SSHD Setup Complete"
|
|
258
855
|
echo "=============================================="
|
|
259
|
-
echo "Root login: root
|
|
260
|
-
echo "Admin user: $ADMIN_USER
|
|
856
|
+
echo "Root login: root (key-only: $([ -n "$AUTHORIZED_KEYS" ] && echo yes || echo no))"
|
|
857
|
+
echo "Admin user: $ADMIN_USER"
|
|
261
858
|
echo "SSH keys: $([ -n "$AUTHORIZED_KEYS" ] && echo 'configured' || echo 'NOT configured')"
|
|
262
859
|
echo "sshd status: $(pidof sshd >/dev/null 2>&1 && echo "running (pid $(pidof sshd))" || echo 'NOT running')"
|
|
263
|
-
echo "sudo: $(command -v sudo >/dev/null 2>&1 && echo 'installed' || echo 'NOT available (use su -)')"
|
|
264
860
|
echo "IP address: $DETECTED_IP"
|
|
861
|
+
echo "Installer: $INSTALLER"
|
|
265
862
|
echo "=============================================="
|
|
266
863
|
|
|
267
|
-
# Physical console display (printf with \r\n for proper line breaks on serial/physical consoles)
|
|
268
864
|
{
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
printf "\r\n"
|
|
865
|
+
printf "\r\n"
|
|
866
|
+
printf "██╗░░░██╗███╗░░██╗██████╗░███████╗██████╗░██████╗░░█████╗░░██████╗████████╗\r\n"
|
|
867
|
+
printf "██║░░░██║████╗░██║██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔══██╗██╔════╝╚══██╔══╝\r\n"
|
|
868
|
+
printf "██║░░░██║██╔██╗██║██║░░██║█████╗░░██████╔╝██████╔╝██║░░██║╚█████╗░░░░██║░░░\r\n"
|
|
869
|
+
printf "██║░░░██║██║╚████║██║░░██║██╔══╝░░██╔══██╗██╔═══╝░██║░░██║░╚═══██╗░░░██║░░░\r\n"
|
|
870
|
+
printf "╚██████╔╝██║░╚███║██████╔╝███████╗██║░░██║██║░░░░░╚█████╔╝██████╔╝░░░██║░░░\r\n"
|
|
871
|
+
printf "░╚═════╝░╚═╝░░╚══╝╚═════╝░╚══════╝╚═╝░░╚═╝╚═╝░░░░░░╚════╝░╚═════╝░░░░╚═╝░░░\r\n"
|
|
872
|
+
printf "==============================================\r\n"
|
|
873
|
+
printf " Underpost Network Ephemeral Commissioning Active\r\n"
|
|
874
|
+
printf "==============================================\r\n"
|
|
875
|
+
printf " SSH as: root@%s -p %s (key-only)\r\n" "$DETECTED_IP" "$SSH_PORT"
|
|
876
|
+
printf " Stage: awaiting remote install trigger\r\n"
|
|
877
|
+
printf " Disk: auto-detect (hint: %s)\r\n" "${INSTALL_DISK_HINT:-none}"
|
|
878
|
+
printf "==============================================\r\n"
|
|
879
|
+
printf "\r\n"
|
|
285
880
|
} > /dev/console 2>/dev/null || true
|
|
286
881
|
|
|
287
|
-
#
|
|
882
|
+
# Best-effort check that sshd is accepting connections on the configured port.
|
|
883
|
+
# Reported as metadata in the ssh-ready event but does NOT gate the POST: the
|
|
884
|
+
# controller needs the announcement regardless, and inst.sshd may name/track the
|
|
885
|
+
# daemon in ways pidof misses.
|
|
886
|
+
ssh_is_ready() {
|
|
887
|
+
if command -v ss >/dev/null 2>&1; then
|
|
888
|
+
ss -lnt 2>/dev/null | grep -q ":${SSH_PORT} " && return 0
|
|
889
|
+
fi
|
|
890
|
+
if command -v netstat >/dev/null 2>&1; then
|
|
891
|
+
netstat -lnt 2>/dev/null | grep -q ":${SSH_PORT} " && return 0
|
|
892
|
+
fi
|
|
893
|
+
timeout 3 bash -c "</dev/tcp/127.0.0.1/${SSH_PORT}" >/dev/null 2>&1
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
# Give sshd a brief window to come up, but never block the announcement.
|
|
897
|
+
for _ in $(seq 1 15); do
|
|
898
|
+
ssh_is_ready && break
|
|
899
|
+
sleep 2
|
|
900
|
+
done
|
|
901
|
+
|
|
902
|
+
SSHD_LISTENING=$(ssh_is_ready && echo true || echo false)
|
|
903
|
+
SSHD_PID=$(pidof sshd 2>/dev/null | awk '{print $1}')
|
|
904
|
+
log "[ready] sshd_listening=${SSHD_LISTENING} sshd_pid=${SSHD_PID:-none} port=${SSH_PORT}"
|
|
905
|
+
|
|
906
|
+
# Idempotent: only POST ssh-ready once per boot/session. Posted unconditionally
|
|
907
|
+
# (best effort) so a strict readiness probe never swallows the handshake.
|
|
908
|
+
if [ ! -f "$READY_FLAG" ]; then
|
|
909
|
+
if post_status "ssh-ready" "\"detail\":\"ephemeral runtime ready\",\"sshdListening\":${SSHD_LISTENING},\"sshdPid\":\"${SSHD_PID:-}\",\"installer\":\"${INSTALLER}\""; then
|
|
910
|
+
touch "$READY_FLAG"
|
|
911
|
+
log "[ready] ssh-ready handshake posted"
|
|
912
|
+
else
|
|
913
|
+
log "[ready] ssh-ready POST failed; will retry in lifecycle loop"
|
|
914
|
+
fi
|
|
915
|
+
fi
|
|
916
|
+
|
|
917
|
+
# ---------------------------------------------------------------------------
|
|
918
|
+
# 9. Lifecycle wait loop
|
|
919
|
+
# - keep sshd alive (inst.sshd watchdog should also handle this)
|
|
920
|
+
# - run the installer when the controller drops a trigger file over SSH,
|
|
921
|
+
# e.g. ssh root@host 'touch /tmp/.underpost-install-trigger'
|
|
922
|
+
# or directly: ssh root@host '/usr/local/bin/underpost-install.sh'
|
|
923
|
+
# - AUTO_INSTALL fallback self-triggers after AUTO_INSTALL_FALLBACK_SECONDS
|
|
924
|
+
# ---------------------------------------------------------------------------
|
|
925
|
+
INSTALL_LOG=/tmp/underpost-install.log
|
|
926
|
+
LAUNCHED_FLAG=/tmp/.underpost-install-launched
|
|
927
|
+
|
|
928
|
+
# Launch the installer fully detached from this loop process via setsid + closed
|
|
929
|
+
# stdio, so it keeps running independently of any sshd session or signal. Guarded
|
|
930
|
+
# by LAUNCHED_FLAG so trigger + fallback never double-launch.
|
|
931
|
+
launch_installer() {
|
|
932
|
+
[ -f "$LAUNCHED_FLAG" ] && return 0
|
|
933
|
+
touch "$LAUNCHED_FLAG"
|
|
934
|
+
log "[loop] launching installer ($INSTALLER), log -> $INSTALL_LOG"
|
|
935
|
+
console_log "remote command received: starting disk install ($INSTALLER)"
|
|
936
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
937
|
+
setsid bash "$INSTALLER" >> "$INSTALL_LOG" 2>&1 < /dev/null &
|
|
938
|
+
else
|
|
939
|
+
nohup bash "$INSTALLER" >> "$INSTALL_LOG" 2>&1 < /dev/null &
|
|
940
|
+
fi
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
START_TS=$(date +%s)
|
|
944
|
+
LOOP_N=0
|
|
288
945
|
while true; do
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
946
|
+
[ -f "$INSTALL_DONE" ] && { log "[loop] install done; idle"; sleep 60; continue; }
|
|
947
|
+
|
|
948
|
+
LOOP_N=$((LOOP_N + 1))
|
|
949
|
+
|
|
950
|
+
# Keep retrying the ssh-ready handshake until it is acknowledged so a flaky
|
|
951
|
+
# first POST never strands the controller.
|
|
952
|
+
if [ ! -f "$READY_FLAG" ]; then
|
|
953
|
+
if post_status "ssh-ready" "\"detail\":\"retry from lifecycle loop\",\"installer\":\"${INSTALLER}\""; then
|
|
954
|
+
touch "$READY_FLAG"
|
|
955
|
+
log "[loop] ssh-ready handshake posted (retry)"
|
|
956
|
+
fi
|
|
957
|
+
fi
|
|
958
|
+
|
|
959
|
+
# Periodic heartbeat so the controller log shows the runtime is alive while
|
|
960
|
+
# it waits for a trigger.
|
|
961
|
+
if [ "$((LOOP_N % 4))" -eq 0 ]; then
|
|
962
|
+
post_status "heartbeat" "\"detail\":\"awaiting install trigger\",\"uptimeSec\":$(( $(date +%s) - START_TS ))" >/dev/null 2>&1 || true
|
|
963
|
+
fi
|
|
964
|
+
|
|
965
|
+
if ! pidof sshd >/dev/null 2>&1; then
|
|
966
|
+
log "[loop] sshd not running, restarting..."
|
|
967
|
+
/usr/sbin/sshd 2>/dev/null || sshd 2>/dev/null
|
|
968
|
+
fi
|
|
969
|
+
|
|
970
|
+
if [ -f "$TRIGGER_FILE" ] && [ ! -f "$INSTALL_DONE" ]; then
|
|
971
|
+
log "[loop] install trigger detected"
|
|
972
|
+
console_log "ssh install trigger detected"
|
|
973
|
+
rm -f "$TRIGGER_FILE"
|
|
974
|
+
launch_installer
|
|
975
|
+
fi
|
|
976
|
+
|
|
977
|
+
if [ "$AUTO_INSTALL" = "1" ] && [ ! -f "$LAUNCHED_FLAG" ] && [ ! -f "$INSTALL_DONE" ]; then
|
|
978
|
+
NOW=$(date +%s)
|
|
979
|
+
if [ "$((NOW - START_TS))" -ge "$AUTO_INSTALL_FALLBACK_SECONDS" ]; then
|
|
980
|
+
log "[loop] AUTO_INSTALL fallback timeout reached; self-triggering installer"
|
|
981
|
+
launch_installer
|
|
982
|
+
fi
|
|
983
|
+
fi
|
|
984
|
+
|
|
985
|
+
sleep 15
|
|
294
986
|
done
|