@stackframe/stack-cli 2.8.86 → 2.8.88
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/dist/emulator/cloud-init/emulator/user-data +200 -16
- package/dist/emulator/common.sh +139 -0
- package/dist/emulator/run-emulator.sh +704 -60
- package/dist/index.js +848 -284
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
|
@@ -75,12 +75,24 @@ write_files:
|
|
|
75
75
|
# ssk/sak: required by the emulator's own dashboard (StackServerApp
|
|
76
76
|
# construction throws without them). Not used by user-app flows; the
|
|
77
77
|
# /local-emulator/project route mints separate per-project credentials.
|
|
78
|
+
#
|
|
79
|
+
# Snapshot-build mode (STACK_EMULATOR_BUILD_SNAPSHOT=1 in /etc/stack-build.env):
|
|
80
|
+
# use deterministic placeholder hex strings instead of random values. The
|
|
81
|
+
# built image then contains these placeholders; at every `emulator start`
|
|
82
|
+
# resume the host generates fresh per-install secrets and
|
|
83
|
+
# /usr/local/bin/rotate-secrets (inside the stack container) swaps them in.
|
|
78
84
|
umask 077
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
if [ -f /etc/stack-build.env ] && grep -q '^STACK_EMULATOR_BUILD_SNAPSHOT=1' /etc/stack-build.env 2>/dev/null; then
|
|
86
|
+
printf '%s' '00000000000000000000000000000000ffffffffffffffffffffffffffffffff' > /var/lib/stack-auth/internal-pck
|
|
87
|
+
printf '%s' '00000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' > /var/lib/stack-auth/internal-ssk
|
|
88
|
+
printf '%s' '00000000000000000000000000000000dddddddddddddddddddddddddddddddd' > /var/lib/stack-auth/internal-sak
|
|
89
|
+
else
|
|
90
|
+
for key in internal-pck internal-ssk internal-sak; do
|
|
91
|
+
if [ ! -s "/var/lib/stack-auth/$key" ]; then
|
|
92
|
+
openssl rand -hex 32 > "/var/lib/stack-auth/$key"
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
fi
|
|
84
96
|
INTERNAL_PCK="$(cat /var/lib/stack-auth/internal-pck)"
|
|
85
97
|
INTERNAL_SSK="$(cat /var/lib/stack-auth/internal-ssk)"
|
|
86
98
|
INTERNAL_SAK="$(cat /var/lib/stack-auth/internal-sak)"
|
|
@@ -92,13 +104,25 @@ write_files:
|
|
|
92
104
|
HOST_SERVICES_HOST=10.0.2.2
|
|
93
105
|
P="$STACK_EMULATOR_PORT_PREFIX"
|
|
94
106
|
|
|
107
|
+
# Snapshot-build mode: ship a deterministic placeholder CRON_SECRET so the
|
|
108
|
+
# baked VM contains a known-public value that rotate-secrets swaps out on
|
|
109
|
+
# every resume. Outside snapshot-build mode, leave CRON_SECRET unset so
|
|
110
|
+
# docker/local-emulator/entrypoint.sh generates a fresh random one.
|
|
111
|
+
EMULATOR_CRON_SECRET=""
|
|
112
|
+
if [ -f /etc/stack-build.env ] && grep -q '^STACK_EMULATOR_BUILD_SNAPSHOT=1' /etc/stack-build.env 2>/dev/null; then
|
|
113
|
+
EMULATOR_CRON_SECRET="00000000000000000000000000000000cccccccccccccccccccccccccccccccc"
|
|
114
|
+
fi
|
|
115
|
+
|
|
95
116
|
{
|
|
96
117
|
# Static vars from base config and runtime (e.g. API keys, feature flags)
|
|
97
118
|
cat /mnt/stack-runtime/base.env
|
|
98
119
|
cat /mnt/stack-runtime/runtime.env
|
|
99
|
-
printf '
|
|
100
|
-
printf '
|
|
120
|
+
printf 'STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=%s\n' "$INTERNAL_PCK"
|
|
121
|
+
printf 'STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$INTERNAL_SSK"
|
|
101
122
|
printf 'STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=%s\n' "$INTERNAL_SAK"
|
|
123
|
+
if [ -n "$EMULATOR_CRON_SECRET" ]; then
|
|
124
|
+
printf 'CRON_SECRET=%s\n' "$EMULATOR_CRON_SECRET"
|
|
125
|
+
fi
|
|
102
126
|
|
|
103
127
|
# Computed vars — depend on port prefix or deps host
|
|
104
128
|
# Host-side ports (for browser URLs — browser runs on host, not in VM)
|
|
@@ -106,6 +130,11 @@ write_files:
|
|
|
106
130
|
HP_DASHBOARD="$STACK_EMULATOR_DASHBOARD_HOST_PORT"
|
|
107
131
|
HP_MINIO="$STACK_EMULATOR_MINIO_HOST_PORT"
|
|
108
132
|
HP_INBUCKET="$STACK_EMULATOR_INBUCKET_HOST_PORT"
|
|
133
|
+
# Mock OAuth binds to this port inside the VM and the host forwards the
|
|
134
|
+
# same port through, so the OIDC issuer URL is reachable identically
|
|
135
|
+
# from the browser and from the backend. Falls back to ${P}14 for
|
|
136
|
+
# older ISOs that don't set it.
|
|
137
|
+
HP_MOCK_OAUTH="${STACK_EMULATOR_MOCK_OAUTH_HOST_PORT:-${P}14}"
|
|
109
138
|
|
|
110
139
|
cat <<COMPUTED
|
|
111
140
|
STACK_SKIP_MIGRATIONS=true
|
|
@@ -129,7 +158,8 @@ write_files:
|
|
|
129
158
|
STACK_CLICKHOUSE_URL=http://${DEPS_HOST}:8123
|
|
130
159
|
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:${HP_DASHBOARD}/handler/email-verification
|
|
131
160
|
STACK_EMAIL_MONITOR_INBUCKET_API_URL=http://${DEPS_HOST}:9001
|
|
132
|
-
STACK_OAUTH_MOCK_URL=http://localhost:${
|
|
161
|
+
STACK_OAUTH_MOCK_URL=http://localhost:${HP_MOCK_OAUTH}
|
|
162
|
+
STACK_OAUTH_MOCK_PORT=${HP_MOCK_OAUTH}
|
|
133
163
|
STACK_FREESTYLE_API_ENDPOINT=http://${DEPS_HOST}:8180
|
|
134
164
|
STACK_STRIPE_MOCK_PORT=12111
|
|
135
165
|
NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=pk_test_mock_publishable_key_for_local_emulator
|
|
@@ -142,15 +172,46 @@ write_files:
|
|
|
142
172
|
permissions: '0755'
|
|
143
173
|
content: |
|
|
144
174
|
#!/bin/bash
|
|
145
|
-
|
|
175
|
+
# Mount the host filesystem at /host. Two modes:
|
|
176
|
+
# (no args) — cold-boot: bind /host on itself, make it a shared
|
|
177
|
+
# mount point, then mount virtio-9p on top. The
|
|
178
|
+
# bind+shared step is what lets the docker bind
|
|
179
|
+
# mount (-v /host:/host:rshared) receive later
|
|
180
|
+
# propagation events.
|
|
181
|
+
# --post-resume — snapshot-resume: /host is already shared (set up
|
|
182
|
+
# at build time and preserved across the snapshot,
|
|
183
|
+
# plus the docker bind mount has rshared
|
|
184
|
+
# propagation). The host has just hot-plugged
|
|
185
|
+
# virtio-9p; mount it on /host and the new mount
|
|
186
|
+
# propagates into the running container.
|
|
187
|
+
set -uo pipefail
|
|
146
188
|
mkdir -p /host
|
|
189
|
+
|
|
190
|
+
# Idempotent: bind /host on itself once so it becomes a mount point
|
|
191
|
+
# with its own propagation, then make it shared. mount --make-shared
|
|
192
|
+
# requires a mount point, hence the bind first.
|
|
147
193
|
if ! mountpoint -q /host; then
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
mount --bind /host /host
|
|
195
|
+
fi
|
|
196
|
+
mount --make-shared /host
|
|
197
|
+
|
|
198
|
+
if [ "${1:-}" = "--post-resume" ]; then
|
|
199
|
+
if mount -t 9p -o trans=virtio,version=9p2000.L hostfs /host; then
|
|
200
|
+
exit 0
|
|
151
201
|
fi
|
|
202
|
+
echo "post-resume 9p mount failed" >&2
|
|
203
|
+
exit 1
|
|
152
204
|
fi
|
|
153
205
|
|
|
206
|
+
# Cold boot. In snapshot-build mode the host detaches virtfs (QEMU
|
|
207
|
+
# disallows migration while it's mounted), so the 9p mount may not be
|
|
208
|
+
# available — tolerate that and fall through to an empty /host.
|
|
209
|
+
if mount -t 9p -o trans=virtio,version=9p2000.L hostfs /host 2>/dev/null; then
|
|
210
|
+
exit 0
|
|
211
|
+
fi
|
|
212
|
+
echo "host filesystem unavailable; continuing with empty /host" >&2
|
|
213
|
+
exit 0
|
|
214
|
+
|
|
154
215
|
- path: /usr/local/bin/run-stack-container
|
|
155
216
|
permissions: '0755'
|
|
156
217
|
content: |
|
|
@@ -190,7 +251,7 @@ write_files:
|
|
|
190
251
|
-v stack-clickhouse-data:/data/clickhouse \
|
|
191
252
|
-v stack-minio-data:/data/minio \
|
|
192
253
|
-v stack-inbucket-data:/data/inbucket \
|
|
193
|
-
-v /host:/host \
|
|
254
|
+
-v /host:/host:rshared \
|
|
194
255
|
stack-local-emulator 2>&1 | tee -a "$host_log"
|
|
195
256
|
else
|
|
196
257
|
exec docker run \
|
|
@@ -204,7 +265,7 @@ write_files:
|
|
|
204
265
|
-v stack-clickhouse-data:/data/clickhouse \
|
|
205
266
|
-v stack-minio-data:/data/minio \
|
|
206
267
|
-v stack-inbucket-data:/data/inbucket \
|
|
207
|
-
-v /host:/host \
|
|
268
|
+
-v /host:/host:rshared \
|
|
208
269
|
stack-local-emulator
|
|
209
270
|
fi
|
|
210
271
|
|
|
@@ -441,8 +502,8 @@ write_files:
|
|
|
441
502
|
--network host \
|
|
442
503
|
--env-file /etc/stack-build.env \
|
|
443
504
|
--env-file /etc/stack-build-computed.env \
|
|
444
|
-
-e
|
|
445
|
-
-e
|
|
505
|
+
-e STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY="$SMOKE_PCK" \
|
|
506
|
+
-e STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY="$SMOKE_SSK" \
|
|
446
507
|
-e STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY="$SMOKE_SAK" \
|
|
447
508
|
-e STACK_SKIP_MIGRATIONS=true \
|
|
448
509
|
-e STACK_SKIP_SEED_SCRIPT=true \
|
|
@@ -522,6 +583,74 @@ write_files:
|
|
|
522
583
|
fstrim -av 2>/dev/null || true
|
|
523
584
|
log "slim-docker-image done."
|
|
524
585
|
|
|
586
|
+
- path: /usr/local/bin/wait-for-stack-ready
|
|
587
|
+
permissions: '0755'
|
|
588
|
+
content: |
|
|
589
|
+
#!/bin/bash
|
|
590
|
+
# Poll the stack container's backend + dashboard on the guest's own
|
|
591
|
+
# localhost until both respond healthy. Used at snapshot-build time to
|
|
592
|
+
# gate "emit STACK_SERVICES_READY" on the app actually being warm.
|
|
593
|
+
set -uo pipefail
|
|
594
|
+
|
|
595
|
+
TIMEOUT="${STACK_READY_TIMEOUT:-600}"
|
|
596
|
+
BACKEND_PORT="${STACK_READY_BACKEND_PORT:-8102}"
|
|
597
|
+
DASHBOARD_PORT="${STACK_READY_DASHBOARD_PORT:-8101}"
|
|
598
|
+
|
|
599
|
+
log() { /usr/local/bin/log-provision "wait-for-stack-ready: $*"; }
|
|
600
|
+
|
|
601
|
+
start=$SECONDS
|
|
602
|
+
next_heartbeat=$((start + 30))
|
|
603
|
+
log "waiting for backend:$BACKEND_PORT and dashboard:$DASHBOARD_PORT (timeout=${TIMEOUT}s)"
|
|
604
|
+
while true; do
|
|
605
|
+
backend_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "http://127.0.0.1:${BACKEND_PORT}/health?db=1" 2>/dev/null || true)
|
|
606
|
+
dashboard_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "http://127.0.0.1:${DASHBOARD_PORT}/handler/sign-in" 2>/dev/null || true)
|
|
607
|
+
if [ "$backend_code" = "200" ] && [ "$dashboard_code" = "200" ]; then
|
|
608
|
+
log "ready ($((SECONDS - start))s)"
|
|
609
|
+
exit 0
|
|
610
|
+
fi
|
|
611
|
+
if [ "$SECONDS" -ge "$next_heartbeat" ]; then
|
|
612
|
+
log "still waiting (backend=$backend_code dashboard=$dashboard_code, $((SECONDS - start))s elapsed)"
|
|
613
|
+
next_heartbeat=$((SECONDS + 30))
|
|
614
|
+
fi
|
|
615
|
+
if [ "$((SECONDS - start))" -ge "$TIMEOUT" ]; then
|
|
616
|
+
log "TIMEOUT after $((SECONDS - start))s (backend=$backend_code dashboard=$dashboard_code)"
|
|
617
|
+
docker ps -a 2>&1 | /usr/local/bin/log-provision-stream "wait-for-stack-ready: ps" || true
|
|
618
|
+
docker logs --tail 200 stack 2>&1 | /usr/local/bin/log-provision-stream "wait-for-stack-ready: stack" || true
|
|
619
|
+
systemctl status stack.service --no-pager -l 2>&1 | /usr/local/bin/log-provision-stream "wait-for-stack-ready: svc" || true
|
|
620
|
+
journalctl -u stack.service --no-pager -n 100 2>&1 | /usr/local/bin/log-provision-stream "wait-for-stack-ready: jrnl" || true
|
|
621
|
+
docker image ls 2>&1 | /usr/local/bin/log-provision-stream "wait-for-stack-ready: img" || true
|
|
622
|
+
exit 1
|
|
623
|
+
fi
|
|
624
|
+
sleep 2
|
|
625
|
+
done
|
|
626
|
+
|
|
627
|
+
- path: /usr/local/bin/trigger-fast-rotate
|
|
628
|
+
permissions: '0755'
|
|
629
|
+
content: |
|
|
630
|
+
#!/bin/bash
|
|
631
|
+
# Called via qemu-guest-agent on every snapshot resume. Reads fresh
|
|
632
|
+
# secrets from stdin (key=value lines, written by the host via QGA's
|
|
633
|
+
# guest-exec input-data) and execs rotate-secrets inside the stack
|
|
634
|
+
# container with those values exported.
|
|
635
|
+
set -euo pipefail
|
|
636
|
+
|
|
637
|
+
tmp="$(mktemp /var/run/stack-fresh-XXXXXX.env)"
|
|
638
|
+
cat > "$tmp"
|
|
639
|
+
chmod 0600 "$tmp"
|
|
640
|
+
|
|
641
|
+
# shellcheck disable=SC1090
|
|
642
|
+
set -a
|
|
643
|
+
source "$tmp"
|
|
644
|
+
set +a
|
|
645
|
+
rm -f "$tmp"
|
|
646
|
+
|
|
647
|
+
exec docker exec \
|
|
648
|
+
-e STACK_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY \
|
|
649
|
+
-e STACK_INTERNAL_PROJECT_SECRET_SERVER_KEY \
|
|
650
|
+
-e STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY \
|
|
651
|
+
-e CRON_SECRET \
|
|
652
|
+
stack /usr/local/bin/rotate-secrets
|
|
653
|
+
|
|
525
654
|
- path: /etc/systemd/system/stack.service
|
|
526
655
|
content: |
|
|
527
656
|
[Unit]
|
|
@@ -591,6 +720,14 @@ write_files:
|
|
|
591
720
|
systemctl disable --now ssh || true
|
|
592
721
|
systemctl mask ssh || true
|
|
593
722
|
|
|
723
|
+
# qemu-guest-agent: used by the host to inject fresh secrets + trigger
|
|
724
|
+
# rotate-secrets after a snapshot resume. Must be running INSIDE the VM
|
|
725
|
+
# at snapshot capture time — the virtio-serial port's "open" state is
|
|
726
|
+
# part of the migrated device state. If QGA wasn't connected at capture,
|
|
727
|
+
# the resumed VM's port stays closed and the host can't reach it.
|
|
728
|
+
systemctl enable qemu-guest-agent || true
|
|
729
|
+
systemctl start qemu-guest-agent || true
|
|
730
|
+
|
|
594
731
|
log_provision "installing emulator containers"
|
|
595
732
|
bash /usr/local/bin/install-emulator-containers
|
|
596
733
|
|
|
@@ -603,6 +740,53 @@ write_files:
|
|
|
603
740
|
log_provision "starting slim-docker-image"
|
|
604
741
|
bash /usr/local/bin/slim-docker-image
|
|
605
742
|
|
|
743
|
+
# Capture mode: bring the stack container up, wait for full
|
|
744
|
+
# readiness, emit STACK_SERVICES_READY, then wait indefinitely for the
|
|
745
|
+
# host build script to capture VM state over QMP (stop + migrate + quit).
|
|
746
|
+
# The VM never shuts itself down in this path — the host tears it down
|
|
747
|
+
# once the savevm file has been written.
|
|
748
|
+
#
|
|
749
|
+
# CI never sets STACK_EMULATOR_CAPTURE_SAVEVM=1 (snapshots aren't
|
|
750
|
+
# portable across accelerators, so they're captured locally on first
|
|
751
|
+
# `stack emulator pull`). This branch only fires for opt-in local
|
|
752
|
+
# builds run with EMULATOR_CAPTURE_SAVEVM=1.
|
|
753
|
+
if [ -f /etc/stack-build.env ] && grep -q '^STACK_EMULATOR_CAPTURE_SAVEVM=1' /etc/stack-build.env 2>/dev/null; then
|
|
754
|
+
log_provision "capture mode: starting stack.service"
|
|
755
|
+
systemctl start stack.service || true
|
|
756
|
+
|
|
757
|
+
log_provision "waiting for backend + dashboard to be ready"
|
|
758
|
+
if ! /usr/local/bin/wait-for-stack-ready; then
|
|
759
|
+
log_provision "ERROR: stack services did not become ready"
|
|
760
|
+
exit 1
|
|
761
|
+
fi
|
|
762
|
+
|
|
763
|
+
# Ensure qemu-guest-agent is running so its virtio-serial port stays
|
|
764
|
+
# "open" in the snapshot — the host needs that port at runtime to
|
|
765
|
+
# trigger rotate-secrets.
|
|
766
|
+
log_provision "ensuring qemu-guest-agent is up"
|
|
767
|
+
systemctl restart qemu-guest-agent || true
|
|
768
|
+
sleep 2
|
|
769
|
+
if ! systemctl is-active --quiet qemu-guest-agent; then
|
|
770
|
+
log_provision "ERROR: qemu-guest-agent failed to start"
|
|
771
|
+
systemctl status qemu-guest-agent --no-pager -l 2>&1 | /usr/local/bin/log-provision-stream "qga"
|
|
772
|
+
exit 1
|
|
773
|
+
fi
|
|
774
|
+
log_provision "qemu-guest-agent active"
|
|
775
|
+
|
|
776
|
+
log_provision "services ready; signalling STACK_SERVICES_READY"
|
|
777
|
+
if [ -n "${STACK_PROVISION_LOG_FILE:-}" ]; then
|
|
778
|
+
printf '%s\n' "STACK_SERVICES_READY" >> "$STACK_PROVISION_LOG_FILE"
|
|
779
|
+
fi
|
|
780
|
+
write_marker_to_consoles "STACK_SERVICES_READY"
|
|
781
|
+
sync || true
|
|
782
|
+
|
|
783
|
+
# Clear the EXIT trap so the cleanup path doesn't mark this as failed
|
|
784
|
+
# when the host powers us off via QMP quit.
|
|
785
|
+
trap - EXIT
|
|
786
|
+
# Block forever; host will issue qmp quit after migrate completes.
|
|
787
|
+
while true; do sleep 3600; done
|
|
788
|
+
fi
|
|
789
|
+
|
|
606
790
|
log_provision "build pipeline complete"
|
|
607
791
|
if [ -n "${STACK_PROVISION_LOG_FILE:-}" ]; then
|
|
608
792
|
printf '%s\n' "STACK_CLOUD_INIT_DONE" >> "$STACK_PROVISION_LOG_FILE"
|
package/dist/emulator/common.sh
CHANGED
|
@@ -68,3 +68,142 @@ make_iso_from_dir() {
|
|
|
68
68
|
exit 1
|
|
69
69
|
fi
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
# Send one or more QMP commands over the monitor socket. Stdin is a stream of
|
|
73
|
+
# JSON objects; qmp_capabilities is always sent first to exit negotiation mode.
|
|
74
|
+
# Keep stdin open briefly after writing so socat doesn't close before QEMU
|
|
75
|
+
# responds — QMP replies in milliseconds so 0.5s is plenty.
|
|
76
|
+
#
|
|
77
|
+
# Callers: build-image.sh capture flow, run-emulator.sh cmd_capture.
|
|
78
|
+
qmp_session() {
|
|
79
|
+
local sock="$1"
|
|
80
|
+
local payload
|
|
81
|
+
payload="$(cat)"
|
|
82
|
+
( printf '%s\n' "$payload"; sleep 0.5 ) | socat -t30 - "UNIX-CONNECT:${sock}"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Drive the snapshot capture over QMP:
|
|
86
|
+
# 1. qmp_capabilities — exit negotiation mode.
|
|
87
|
+
# 2. stop — pause the VM so no more disk writes happen.
|
|
88
|
+
# 3. migrate-set-capabilities — enable mapped-ram + multifd for fast resume.
|
|
89
|
+
# 4. migrate to file:<path> — streams RAM/device state out.
|
|
90
|
+
# 5. Poll query-migrate until status=completed (or failed).
|
|
91
|
+
# 6. quit — terminate QEMU cleanly.
|
|
92
|
+
#
|
|
93
|
+
# Depends on log/err/warn being defined by the sourcing script.
|
|
94
|
+
capture_vm_state() {
|
|
95
|
+
local sock="$1"
|
|
96
|
+
local guest_path="$2"
|
|
97
|
+
|
|
98
|
+
if [ ! -S "$sock" ]; then
|
|
99
|
+
err "QMP monitor socket missing: $sock"
|
|
100
|
+
return 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
log " QMP: stopping VM..."
|
|
104
|
+
{
|
|
105
|
+
printf '%s\n' '{"execute":"qmp_capabilities"}'
|
|
106
|
+
printf '%s\n' '{"execute":"stop"}'
|
|
107
|
+
} | qmp_session "$sock" >/dev/null || {
|
|
108
|
+
err "QMP stop failed"
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
log " QMP: enabling mapped-ram + multifd for fast resume..."
|
|
113
|
+
# mapped-ram: writes each RAM page to a fixed offset in the output file
|
|
114
|
+
# (vs the legacy streamed format). This lets the target QEMU mmap the file
|
|
115
|
+
# and fault pages lazily — and combined with multifd, load RAM in parallel.
|
|
116
|
+
# multifd-channels=4 matches our pinned SMP so the channels don't starve
|
|
117
|
+
# each other on the target's 4 vCPUs.
|
|
118
|
+
local caps_cmd params_cmd
|
|
119
|
+
caps_cmd='{"execute":"migrate-set-capabilities","arguments":{"capabilities":[{"capability":"mapped-ram","state":true},{"capability":"multifd","state":true}]}}'
|
|
120
|
+
params_cmd='{"execute":"migrate-set-parameters","arguments":{"multifd-channels":4}}'
|
|
121
|
+
local setup_resp
|
|
122
|
+
setup_resp=$({
|
|
123
|
+
printf '%s\n' '{"execute":"qmp_capabilities"}'
|
|
124
|
+
printf '%s\n' "$caps_cmd"
|
|
125
|
+
printf '%s\n' "$params_cmd"
|
|
126
|
+
} | qmp_session "$sock") || {
|
|
127
|
+
err "QMP capabilities setup failed"
|
|
128
|
+
return 1
|
|
129
|
+
}
|
|
130
|
+
if printf '%s' "$setup_resp" | grep -q '"error"[[:space:]]*:'; then
|
|
131
|
+
err "QMP capabilities returned error: $setup_resp"
|
|
132
|
+
return 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
log " QMP: migrating RAM state to ${guest_path}..."
|
|
136
|
+
# Use file: migration (native QEMU) instead of exec: to avoid relying on a
|
|
137
|
+
# spawned shell finding zstd in PATH. Compressed as a separate host step
|
|
138
|
+
# after migrate completes.
|
|
139
|
+
local migrate_cmd
|
|
140
|
+
migrate_cmd=$(printf '{"execute":"migrate","arguments":{"uri":"file:%s"}}' "$guest_path")
|
|
141
|
+
local migrate_resp
|
|
142
|
+
migrate_resp=$({
|
|
143
|
+
printf '%s\n' '{"execute":"qmp_capabilities"}'
|
|
144
|
+
printf '%s\n' "$migrate_cmd"
|
|
145
|
+
} | qmp_session "$sock") || {
|
|
146
|
+
err "QMP migrate failed"
|
|
147
|
+
return 1
|
|
148
|
+
}
|
|
149
|
+
if printf '%s' "$migrate_resp" | grep -q '"error"[[:space:]]*:'; then
|
|
150
|
+
err "QMP migrate returned error: $migrate_resp"
|
|
151
|
+
return 1
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# Poll migration status. Migration runs in the background after the
|
|
155
|
+
# migrate command returns; we watch for "completed" or "failed".
|
|
156
|
+
local migrate_timeout=600
|
|
157
|
+
local waited=0
|
|
158
|
+
local last_heartbeat=0
|
|
159
|
+
while [ "$waited" -lt "$migrate_timeout" ]; do
|
|
160
|
+
local status_line status
|
|
161
|
+
status_line=$({
|
|
162
|
+
printf '%s\n' '{"execute":"qmp_capabilities"}'
|
|
163
|
+
printf '%s\n' '{"execute":"query-migrate"}'
|
|
164
|
+
} | qmp_session "$sock" 2>/dev/null || true)
|
|
165
|
+
status="$(printf '%s\n' "$status_line" | grep -o '"status"[[:space:]]*:[[:space:]]*"[a-z-]*"' | head -1 | sed -E 's/.*"([a-z-]+)".*/\1/')"
|
|
166
|
+
case "$status" in
|
|
167
|
+
completed)
|
|
168
|
+
log " QMP: migrate completed (${waited}s)"
|
|
169
|
+
break
|
|
170
|
+
;;
|
|
171
|
+
failed|cancelled)
|
|
172
|
+
err " QMP: migrate ended with status=$status"
|
|
173
|
+
err " QMP response: $status_line"
|
|
174
|
+
return 1
|
|
175
|
+
;;
|
|
176
|
+
active|setup|device|"")
|
|
177
|
+
# still running
|
|
178
|
+
if [ "$((waited - last_heartbeat))" -ge 30 ]; then
|
|
179
|
+
local transferred
|
|
180
|
+
transferred=$(printf '%s' "$status_line" | grep -o '"transferred"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed -E 's/.*:[[:space:]]*([0-9]+).*/\1/')
|
|
181
|
+
log " QMP: migrate in progress (${waited}s, status=${status:-init}, transferred=${transferred:-0})"
|
|
182
|
+
last_heartbeat=$waited
|
|
183
|
+
fi
|
|
184
|
+
;;
|
|
185
|
+
*)
|
|
186
|
+
log " QMP: migrate status=$status (${waited}s)"
|
|
187
|
+
;;
|
|
188
|
+
esac
|
|
189
|
+
sleep 2
|
|
190
|
+
waited=$((waited + 2))
|
|
191
|
+
done
|
|
192
|
+
|
|
193
|
+
if [ "$waited" -ge "$migrate_timeout" ]; then
|
|
194
|
+
err "QMP migrate timed out after ${migrate_timeout}s"
|
|
195
|
+
err "Last query-migrate response: $({
|
|
196
|
+
printf '%s\n' '{"execute":"qmp_capabilities"}'
|
|
197
|
+
printf '%s\n' '{"execute":"query-migrate"}'
|
|
198
|
+
} | qmp_session "$sock" 2>/dev/null || true)"
|
|
199
|
+
return 1
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
log " QMP: quitting VM..."
|
|
203
|
+
{
|
|
204
|
+
printf '%s\n' '{"execute":"qmp_capabilities"}'
|
|
205
|
+
printf '%s\n' '{"execute":"quit"}'
|
|
206
|
+
} | qmp_session "$sock" >/dev/null || true
|
|
207
|
+
|
|
208
|
+
return 0
|
|
209
|
+
}
|