#!/usr/bin/env bash
set -Eeuo pipefail

STEP_ID="STEP_050M07R14D_VM101_DISK_EXPANSION_1G_R05S_PUBLICATION_SALVAGE_R01"
EXPECTED_COMMIT="0a6998cfead6a1e8b820cce9c00171dd6e81c6a1"
EXPECTED_TREE="b2646e97025823532daa549235e1643f75f11c4e"
EXPECTED_FILE_COUNT="47"
EXPECTED_DISK_SECTORS="2097152"
EXPECTED_PART_SECTORS="2063360"
EXPECTED_MIN_ROOT_KB="980000"
EXPECTED_MIN_AVAIL_KB="850000"

PUBLISHER="/opt/router-ops/bin/router-publish-report"
EXPECTED_PUBLISHER_SHA256="e78430b57afbd0ae27fc575fc4de78bb267c76caf04ec935b01e9b2f89840e32"
PUBLIC_RESOLVE="reports.secret-studio.ru:443:10.71.100.84"
REPOSITORY="https://github.com/DChuvelev/wg-paid-vm101.git"

LOCAL_M07_PLAN_URL="https://reports.secret-studio.ru/latest/20260713-193942_local_architecture_plan_vm101_autonomous_hmn_recovery/"
GLOBAL_PROJECT_PLAN_URL="https://reports.secret-studio.ru/latest/20260711-123348_global_project_plan_wg_paid/"
ACCESS_MAP_URL="https://reports.secret-studio.ru/latest/20260715-210236_access_map_canonical_current/"
VM100_PUBLIC_URL="https://reports.secret-studio.ru/latest/20260715-190653_vm100_git_source_1958200ccc1b/"
VM121_PUBLIC_URL="https://reports.secret-studio.ru/latest/20260713-182700_vm121_git_source_93e0e5a31f09/"
VM130_SOURCE_URL="https://reports.secret-studio.ru/latest/20260716-064558_vm130_router_ops_source_snapshot_r14b2/"
LATEST_ROOT_URL="https://reports.secret-studio.ru/latest/"
OLD_ROOT_URL="https://reports.secret-studio.ru/old/"

BUNDLE_ROOT="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
BUNDLE_ZIP="${BUNDLE_ZIP:?BUNDLE_ZIP required}"
EXPECTED_BUNDLE_SHA256="${EXPECTED_BUNDLE_SHA256:?EXPECTED_BUNDLE_SHA256 required}"
EXPECTED_INSTALLER_SHA256="${EXPECTED_INSTALLER_SHA256:?EXPECTED_INSTALLER_SHA256 required}"

STAMP="$(date -u +%Y%m%d-%H%M%S)"
STEP_DIR="/opt/router-ops/steps/${STAMP}_${STEP_ID}"
mkdir -p "$STEP_DIR"
LOG="$STEP_DIR/main.log"
exec > >(tee -a "$LOG") 2>&1

RESULT="STOP_UNHANDLED_R05S_ERROR"
COPY_VERIFY=PASS
STEP_WAS_NOT_STARTED=false
RUNTIME_IMPACT=false
CORE_RUNTIME_IMPACT=true
CORE_CHANGE_REPEATED=false

STEP_REPORT_URL=
VM101_PUBLIC_URL=
VM101_MODEL_URL=
VM101_METHODS_URL=
FACTS_URL=
VERIFY_URL=
ROLLBACK_URL=
PUBLIC_OBJECT_KIND=
PREVIOUS_MOVED_COUNT=

LIVE_COMMIT=
LIVE_TREE=
LIVE_DISK_SECTORS=
LIVE_PART_SECTORS=
LIVE_ROOT_KB=
LIVE_ROOT_AVAIL_KB=
ENDPOINT_COUNT=0
WATCHER_ENABLED=false
WATCHER_RUNNING=false

progress() {
    printf '>>> [%s] %s\n' "$(date -u +%H:%M:%S)" "$*"
}

step_sha() {
    sha256sum "$STEP_DIR/step-code.sh" | awk '{print $1}'
}

vm101_run() {
    local script="$1"
    printf '%s\n' "$script" |
        ssh pve-mgts \
            "ssh -o BatchMode=yes -o ConnectTimeout=10 -i /root/.ssh/pve_to_openwrt_mgts_ed25519 root@10.71.100.2 'sh -s'"
}

value_from_log() {
    local key="$1" log="$2"
    awk -F= -v k="$key" '$1==k {v=substr($0,index($0,"=")+1)} END{print v}' "$log"
}

url_from_log() {
    local preferred_key="$1" log="$2" url dir folder
    url="$(value_from_log "$preferred_key" "$log")"
    if [[ -n "$url" ]]; then
        printf '%s\n' "$url"
        return 0
    fi
    dir="$(value_from_log PUBLISHED_DIR "$log")"
    [[ -n "$dir" ]] || return 1
    folder="$(basename "$dir")"
    printf 'https://reports.secret-studio.ru/latest/%s/\n' "$folder"
}

publish_files() {
    local label="$1" log="$2"
    shift 2
    local neutral="/tmp/router-publish-${STAMP}-${label}"
    rm -rf "$neutral"
    mkdir -p "$neutral"
    local src base
    for src in "$@"; do
        base="$(basename "$src")"
        cp -a "$src" "$neutral/$base"
    done
    (
        cd "$neutral"
        find . -type f ! -name manifest.sha256 -print0 | sort -z | xargs -0 sha256sum >manifest.sha256
    )
    "$PUBLISHER" "$label" "$neutral"/* >"$log" 2>&1
}

http_get() {
    local url="$1" out="$2"
    curl --fail --silent --show-error --location \
        --connect-timeout 5 --max-time 30 \
        --resolve "$PUBLIC_RESOLVE" \
        "$url" -o "$out"
}

write_reports() {
    cat >"$STEP_DIR/report.txt" <<EOF
STEP_ID=$STEP_ID
RESULT=$RESULT
PURPOSE=Salvage and formally publish the already successful R14D R05 VM101 1G offline ext4 expansion.
CORE_CHANGE_REPEATED=false
CORE_RESULT=PASS_VM101_DISK_EXPANDED_TO_1G_OFFLINE_EXT4
CORE_RUNTIME_IMPACT=true
SALVAGE_RUNTIME_IMPACT=false
LIVE_COMMIT=$LIVE_COMMIT
LIVE_TREE=$LIVE_TREE
LIVE_DISK_SECTORS=$LIVE_DISK_SECTORS
LIVE_PART_SECTORS=$LIVE_PART_SECTORS
LIVE_ROOT_KB=$LIVE_ROOT_KB
LIVE_ROOT_AVAIL_KB=$LIVE_ROOT_AVAIL_KB
ENDPOINT_COUNT=$ENDPOINT_COUNT
WATCHER_ENABLED=$WATCHER_ENABLED
WATCHER_RUNNING=$WATCHER_RUNNING
ROLLBACK_POLICY=Manual snapshot rollback only; no automatic rollback after successful storage core.
PUBLICATION_FAILURE_CAUSE=Previous R05 called router-publish-report with a directory only; approved interface requires label plus files.
COPY_VERIFY=$COPY_VERIFY
STEP_SHA256=$(step_sha)
RUNTIME_IMPACT=$RUNTIME_IMPACT
EOF

    cat >"$STEP_DIR/verify.txt" <<EOF
RESULT=$RESULT
COPY_VERIFY=$COPY_VERIFY
STEP_SHA256=$(step_sha)
PUBLISHER_SHA256=$(sha256sum "$PUBLISHER" | awk '{print $1}')
EXPECTED_PUBLISHER_SHA256=$EXPECTED_PUBLISHER_SHA256
EXPECTED_COMMIT=$EXPECTED_COMMIT
ACTUAL_COMMIT=$LIVE_COMMIT
EXPECTED_TREE=$EXPECTED_TREE
ACTUAL_TREE=$LIVE_TREE
EXPECTED_DISK_SECTORS=$EXPECTED_DISK_SECTORS
ACTUAL_DISK_SECTORS=$LIVE_DISK_SECTORS
EXPECTED_PART_SECTORS=$EXPECTED_PART_SECTORS
ACTUAL_PART_SECTORS=$LIVE_PART_SECTORS
MIN_ROOT_KB=$EXPECTED_MIN_ROOT_KB
ACTUAL_ROOT_KB=$LIVE_ROOT_KB
MIN_ROOT_AVAIL_KB=$EXPECTED_MIN_AVAIL_KB
ACTUAL_ROOT_AVAIL_KB=$LIVE_ROOT_AVAIL_KB
ENDPOINT_COUNT=$ENDPOINT_COUNT
WATCHER_ENABLED=$WATCHER_ENABLED
WATCHER_RUNNING=$WATCHER_RUNNING
CORE_CHANGE_REPEATED=false
RUNTIME_IMPACT=false
EOF

    python3 - "$STEP_DIR/facts.json" <<PY
import json, sys
facts = {
  "step_id": "$STEP_ID",
  "result": "$RESULT",
  "core_change_repeated": False,
  "core_result": "PASS_VM101_DISK_EXPANDED_TO_1G_OFFLINE_EXT4",
  "core_runtime_impact": True,
  "salvage_runtime_impact": False,
  "vm101": {
    "commit": "$LIVE_COMMIT",
    "tree": "$LIVE_TREE",
    "disk_sectors": int("$LIVE_DISK_SECTORS"),
    "partition_sectors": int("$LIVE_PART_SECTORS"),
    "root_kb": int("$LIVE_ROOT_KB"),
    "root_available_kb": int("$LIVE_ROOT_AVAIL_KB"),
    "endpoint_count": int("$ENDPOINT_COUNT"),
    "watcher_enabled": "$WATCHER_ENABLED" == "true",
    "watcher_running": "$WATCHER_RUNNING" == "true"
  },
  "publication_failure_cause": "Previous R05 passed a directory without the required publisher label.",
  "rollback_policy": "manual snapshot rollback only; no automatic late rollback"
}
with open(sys.argv[1], "w") as f:
    json.dump(facts, f, indent=2, sort_keys=True)
    f.write("\n")
PY

    cat >"$STEP_DIR/rollback.sh" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
echo 'Manual rollback only. Do not run unless explicitly approved.'
echo 'Current rollback snapshot: R14D5_20260716-125155'
echo 'Rollback would restore the old 124M disk and interrupt all VM101-routed traffic.'
echo 'RESULT=STOP_MANUAL_APPROVAL_REQUIRED'
exit 1
EOF
    chmod 700 "$STEP_DIR/rollback.sh"

    cat >"$STEP_DIR/system-model.txt" <<EOF
VM101 current storage model:
- Proxmox VMID 101, sata1 1 GiB
- /dev/sda sectors: $LIVE_DISK_SECTORS
- /dev/sda2 sectors: $LIVE_PART_SECTORS
- ext4 root: $LIVE_ROOT_KB KiB
- root available: $LIVE_ROOT_AVAIL_KB KiB
- Machine Git: $LIVE_COMMIT / $LIVE_TREE
- VPN endpoints present: $ENDPOINT_COUNT/5
- watcher enabled/running: $WATCHER_ENABLED/$WATCHER_RUNNING
EOF

    cat >"$STEP_DIR/methods-and-limitations.txt" <<'EOF'
Verified VM101 disk expansion method:
1. Stop VM101 and create a <=40-character Proxmox snapshot.
2. qm resize sata1 to 1G.
3. Attach the stopped raw LV with qemu-nbd.
4. Extend partition 2.
5. Run e2fsck, offline resize2fs, then e2fsck.
6. Disconnect NBD before VM start.
7. Mark storage core complete before asynchronous VPN runtime checks.
8. Never automatically rollback a proven storage expansion because endpoints are still settling.
Publisher interface: router-publish-report <label> <file1> [file2 ...].
EOF

    cat >"$STEP_DIR/access-routes.txt" <<'EOF'
PVE_ROUTE=ops@router-ops -> ssh pve-mgts
VM101_ROUTE=ops@router-ops -> pve-mgts -> root@10.71.100.2
VM101_DIRECT_FROM_ROUTER_OPS=unsupported
ACCESS_MAP_CHANGED=false
EOF
}

finish() {
    printf '%s\n' \
        "RESULT=$RESULT" \
        "STEP_REPORT_URL=$STEP_REPORT_URL" \
        "LOCAL_M07_PLAN_URL=$LOCAL_M07_PLAN_URL" \
        "GLOBAL_PROJECT_PLAN_URL=$GLOBAL_PROJECT_PLAN_URL" \
        "ACCESS_MAP_URL=$ACCESS_MAP_URL" \
        "VM100_PUBLIC_URL=$VM100_PUBLIC_URL" \
        "VM101_PUBLIC_URL=$VM101_PUBLIC_URL" \
        "VM121_PUBLIC_URL=$VM121_PUBLIC_URL" \
        "VM101_MODEL_URL=$VM101_MODEL_URL" \
        "VM101_METHODS_URL=$VM101_METHODS_URL" \
        "VM130_SOURCE_URL=$VM130_SOURCE_URL" \
        "LATEST_ROOT_URL=$LATEST_ROOT_URL" \
        "OLD_ROOT_URL=$OLD_ROOT_URL" \
        "FACTS_URL=$FACTS_URL" \
        "VERIFY_URL=$VERIFY_URL" \
        "ROLLBACK_URL=$ROLLBACK_URL" \
        "PUBLIC_OBJECT_KIND=$PUBLIC_OBJECT_KIND" \
        "PREVIOUS_MOVED_COUNT=$PREVIOUS_MOVED_COUNT" \
        "COPY_VERIFY=$COPY_VERIFY" \
        "STEP_SHA256=$(step_sha)" \
        "RUNTIME_IMPACT=$RUNTIME_IMPACT"
}

fail() {
    RESULT="$1"
    write_reports
    publish_files \
        "step050m07r14d_vm101_disk_expansion_1g_r05s_publication_salvage" \
        "$STEP_DIR/publish-step.log" \
        "$STEP_DIR/step-code.sh" "$STEP_DIR/report.txt" "$STEP_DIR/facts.json" \
        "$STEP_DIR/verify.txt" "$STEP_DIR/rollback.sh" "$STEP_DIR/system-model.txt" \
        "$STEP_DIR/methods-and-limitations.txt" "$STEP_DIR/access-routes.txt" \
        "$STEP_DIR/main.log" "$STEP_DIR/transfer-verify.txt" 2>/dev/null || true
    STEP_REPORT_URL="$(url_from_log STEP_REPORT_URL "$STEP_DIR/publish-step.log" 2>/dev/null || true)"
    finish
    exit 1
}

progress "verify exact transferred salvage code"
cp "$0" "$STEP_DIR/step-code.sh"
cp "$BUNDLE_ZIP" "$STEP_DIR/"
cp "$BUNDLE_ROOT/manifest.sha256" "$STEP_DIR/bundle-manifest.sha256"

ACTUAL_BUNDLE_SHA="$(sha256sum "$BUNDLE_ZIP" | awk '{print $1}')"
ACTUAL_INSTALLER_SHA="$(sha256sum "$BUNDLE_ROOT/install.sh" | awk '{print $1}')"
{
    echo "EXPECTED_BUNDLE_SHA256=$EXPECTED_BUNDLE_SHA256"
    echo "ACTUAL_BUNDLE_SHA256=$ACTUAL_BUNDLE_SHA"
    echo "EXPECTED_INSTALLER_SHA256=$EXPECTED_INSTALLER_SHA256"
    echo "ACTUAL_INSTALLER_SHA256=$ACTUAL_INSTALLER_SHA"
} >"$STEP_DIR/transfer-verify.txt"

[[ "$ACTUAL_BUNDLE_SHA" == "$EXPECTED_BUNDLE_SHA256" ]] || {
    COPY_VERIFY=FAIL; STEP_WAS_NOT_STARTED=true
    fail STOP_COPY_OR_TRANSFER_ERROR
}
[[ "$ACTUAL_INSTALLER_SHA" == "$EXPECTED_INSTALLER_SHA256" ]] || {
    COPY_VERIFY=FAIL; STEP_WAS_NOT_STARTED=true
    fail STOP_COPY_OR_TRANSFER_ERROR
}
(
    cd "$BUNDLE_ROOT"
    sha256sum -c manifest.sha256
) >>"$STEP_DIR/transfer-verify.txt" 2>&1 || {
    COPY_VERIFY=FAIL; STEP_WAS_NOT_STARTED=true
    fail STOP_COPY_OR_TRANSFER_ERROR
}
bash -n "$STEP_DIR/step-code.sh"
echo "COPY_VERIFY=PASS" >>"$STEP_DIR/transfer-verify.txt"
echo "STEP_SHA256=$(step_sha)" >>"$STEP_DIR/transfer-verify.txt"

[[ "$(sha256sum "$PUBLISHER" | awk '{print $1}')" == "$EXPECTED_PUBLISHER_SHA256" ]] ||
    fail STOP_PUBLISHER_SHA256_MISMATCH

progress "independently verify current VM101 disk, source and runtime"
vm101_run '
set +e
echo "COMMIT=$(git --git-dir=/root/.vm101-source.git --work-tree=/ rev-parse HEAD 2>/dev/null)"
echo "TREE=$(git --git-dir=/root/.vm101-source.git --work-tree=/ rev-parse "HEAD^{tree}" 2>/dev/null)"
echo STATUS_BEGIN
git --git-dir=/root/.vm101-source.git --work-tree=/ status --short 2>&1
echo STATUS_END
echo "DISK_SECTORS=$(cat /sys/class/block/sda/size)"
echo "PART_SECTORS=$(cat /sys/class/block/sda2/size)"
df -Pk /
echo ENDPOINTS_BEGIN
for iface in vpn1 vpn2 vpn3 vpn4 vpn5; do
    endpoint="$(amneziawg show "$iface" endpoints 2>/dev/null | awk "NR==1 {print \$2}")"
    printf "%s\t%s\n" "$iface" "$endpoint"
done
echo ENDPOINTS_END
/etc/init.d/router-egress-health-repair enabled >/dev/null 2>&1
echo "WATCHER_ENABLED_RC=$?"
/etc/init.d/router-egress-health-repair running >/dev/null 2>&1
echo "WATCHER_RUNNING_RC=$?"
' >"$STEP_DIR/vm101-live-state.txt"

LIVE_COMMIT="$(awk -F= '$1=="COMMIT"{print $2}' "$STEP_DIR/vm101-live-state.txt")"
LIVE_TREE="$(awk -F= '$1=="TREE"{print $2}' "$STEP_DIR/vm101-live-state.txt")"
LIVE_DISK_SECTORS="$(awk -F= '$1=="DISK_SECTORS"{print $2}' "$STEP_DIR/vm101-live-state.txt")"
LIVE_PART_SECTORS="$(awk -F= '$1=="PART_SECTORS"{print $2}' "$STEP_DIR/vm101-live-state.txt")"
LIVE_ROOT_KB="$(awk '$6=="/"{print $2}' "$STEP_DIR/vm101-live-state.txt")"
LIVE_ROOT_AVAIL_KB="$(awk '$6=="/"{print $4}' "$STEP_DIR/vm101-live-state.txt")"
ENDPOINT_COUNT="$(awk '/ENDPOINTS_BEGIN/{f=1;next}/ENDPOINTS_END/{f=0}f && $2!=""{n++} END{print n+0}' "$STEP_DIR/vm101-live-state.txt")"
grep -q '^WATCHER_ENABLED_RC=0$' "$STEP_DIR/vm101-live-state.txt" && WATCHER_ENABLED=true
grep -q '^WATCHER_RUNNING_RC=0$' "$STEP_DIR/vm101-live-state.txt" && WATCHER_RUNNING=true

[[ "$LIVE_COMMIT" == "$EXPECTED_COMMIT" ]] || fail STOP_VM101_SOURCE_MISMATCH
[[ "$LIVE_TREE" == "$EXPECTED_TREE" ]] || fail STOP_VM101_SOURCE_MISMATCH
awk '/STATUS_BEGIN/{f=1;next}/STATUS_END/{f=0}f && NF{bad=1} END{exit bad?1:0}' "$STEP_DIR/vm101-live-state.txt" ||
    fail STOP_VM101_WORKTREE_NOT_CLEAN
[[ "$LIVE_DISK_SECTORS" == "$EXPECTED_DISK_SECTORS" ]] || fail STOP_DISK_POSTCHECK_FAILED
[[ "$LIVE_PART_SECTORS" == "$EXPECTED_PART_SECTORS" ]] || fail STOP_PARTITION_POSTCHECK_FAILED
[[ "$LIVE_ROOT_KB" -ge "$EXPECTED_MIN_ROOT_KB" ]] || fail STOP_FILESYSTEM_POSTCHECK_FAILED
[[ "$LIVE_ROOT_AVAIL_KB" -ge "$EXPECTED_MIN_AVAIL_KB" ]] || fail STOP_FILESYSTEM_POSTCHECK_FAILED
[[ "$ENDPOINT_COUNT" -eq 5 ]] || fail STOP_RUNTIME_ENDPOINTS_NOT_READY
[[ "$WATCHER_ENABLED" == true && "$WATCHER_RUNNING" == true ]] || fail STOP_WATCHER_NOT_HEALTHY

ssh pve-mgts 'qm status 101; qm config 101 | grep -E "^(sata1|parent):"; qm listsnapshot 101 | tail -n 8' \
    >"$STEP_DIR/pve-live-state.txt"
grep -q '^status: running$' "$STEP_DIR/pve-live-state.txt" || fail STOP_PVE_STATUS_FAILED
grep -Eq '^sata1: .*size=1G([,[:space:]]|$)' "$STEP_DIR/pve-live-state.txt" || fail STOP_PVE_DISK_FAILED
grep -Fq 'R14D5_20260716-125155' "$STEP_DIR/pve-live-state.txt" || fail STOP_PVE_SNAPSHOT_PROOF_FAILED

progress "verify exact GitHub commit/tree/file count"
CLONE="$STEP_DIR/github-clone"
git clone --quiet "$REPOSITORY" "$CLONE" >"$STEP_DIR/git-clone.log" 2>&1
git -C "$CLONE" checkout --quiet --detach "$EXPECTED_COMMIT"
[[ "$(git -C "$CLONE" rev-parse HEAD)" == "$EXPECTED_COMMIT" ]] || fail STOP_GITHUB_CLONE_FAILED
[[ "$(git -C "$CLONE" rev-parse 'HEAD^{tree}')" == "$EXPECTED_TREE" ]] || fail STOP_GITHUB_CLONE_FAILED
[[ "$(git -C "$CLONE" ls-tree -r --name-only HEAD | wc -l | tr -d ' ')" == "$EXPECTED_FILE_COUNT" ]] ||
    fail STOP_GITHUB_FILE_COUNT_FAILED

progress "publish fresh VM101 source through machine-close workflow"
router-machine-close vm101 \
    "$STEP_ID: salvage publish after proven 1G offline ext4 expansion" \
    >"$STEP_DIR/router-machine-close.log" 2>&1 || fail STOP_MACHINE_CLOSE_FAILED
VM101_PUBLIC_URL="$(value_from_log VM101_PUBLIC_URL "$STEP_DIR/router-machine-close.log")"
[[ -n "$VM101_PUBLIC_URL" ]] || VM101_PUBLIC_URL="$(url_from_log VM101_PUBLIC_URL "$STEP_DIR/router-machine-close.log" || true)"
[[ -n "$VM101_PUBLIC_URL" ]] || fail STOP_VM101_SOURCE_URL_MISSING

progress "publish fresh VM101 model"
cat >"$STEP_DIR/vm101-model.md" <<EOF
# VM101 current model after R14D R05

- Disk: 1 GiB, $LIVE_DISK_SECTORS sectors
- Root partition: $LIVE_PART_SECTORS sectors
- ext4 root: $LIVE_ROOT_KB KiB
- available: $LIVE_ROOT_AVAIL_KB KiB
- source: $LIVE_COMMIT / $LIVE_TREE
- VPN endpoints: $ENDPOINT_COUNT/5
- watcher enabled/running: $WATCHER_ENABLED/$WATCHER_RUNNING
- rollback snapshot retained: R14D5_20260716-125155
EOF
publish_files \
    "vm101_model_disk_expansion_1g_r14d_r05_salvage" \
    "$STEP_DIR/publish-model.log" \
    "$STEP_DIR/vm101-model.md" "$STEP_DIR/vm101-live-state.txt" \
    "$STEP_DIR/pve-live-state.txt" "$STEP_DIR/system-model.txt" 2>/dev/null || true
VM101_MODEL_URL="$(url_from_log VM101_MODEL_URL "$STEP_DIR/publish-model.log" || true)"
[[ -n "$VM101_MODEL_URL" ]] || fail STOP_VM101_MODEL_PUBLICATION_FAILED

progress "publish fresh VM101 methods"
cat >"$STEP_DIR/vm101-methods.md" <<'EOF'
# VM101 verified offline ext4 expansion method

The successful method is:
VM stop -> short snapshot name -> qm resize -> qemu-nbd -> parted ->
e2fsck -> offline resize2fs -> e2fsck -> NBD disconnect -> VM start.

Online resize of the mounted OpenWrt root returned EINVAL and is not supported.
After the offline filesystem operation and NBD disconnect succeed, late runtime checks
must never trigger automatic storage rollback.
EOF
publish_files \
    "vm101_methods_disk_expansion_1g_r14d_r05_salvage" \
    "$STEP_DIR/publish-methods.log" \
    "$STEP_DIR/vm101-methods.md" "$STEP_DIR/methods-and-limitations.txt" \
    "$STEP_DIR/access-routes.txt" "$STEP_DIR/rollback.sh" 2>/dev/null || true
VM101_METHODS_URL="$(url_from_log VM101_METHODS_URL "$STEP_DIR/publish-methods.log" || true)"
[[ -n "$VM101_METHODS_URL" ]] || fail STOP_VM101_METHODS_PUBLICATION_FAILED

progress "publish formal salvage STEP report with correct publisher interface"
RESULT="PASS_VM101_DISK_EXPANSION_R05_SALVAGED_AND_PUBLISHED"
write_reports
publish_files \
    "step050m07r14d_vm101_disk_expansion_1g_r05s_publication_salvage" \
    "$STEP_DIR/publish-step.log" \
    "$STEP_DIR/step-code.sh" "$STEP_DIR/report.txt" "$STEP_DIR/facts.json" \
    "$STEP_DIR/verify.txt" "$STEP_DIR/rollback.sh" "$STEP_DIR/system-model.txt" \
    "$STEP_DIR/methods-and-limitations.txt" "$STEP_DIR/access-routes.txt" \
    "$STEP_DIR/main.log" "$STEP_DIR/transfer-verify.txt" \
    "$STEP_DIR/vm101-live-state.txt" "$STEP_DIR/pve-live-state.txt" \
    "$STEP_DIR/router-machine-close.log" "$STEP_DIR/publish-model.log" \
    "$STEP_DIR/publish-methods.log"

STEP_REPORT_URL="$(url_from_log STEP_REPORT_URL "$STEP_DIR/publish-step.log" || true)"
PUBLIC_OBJECT_KIND="$(value_from_log PUBLIC_OBJECT_KIND "$STEP_DIR/publish-step.log")"
PREVIOUS_MOVED_COUNT="$(value_from_log PREVIOUS_MOVED_COUNT "$STEP_DIR/publish-step.log")"
[[ -n "$STEP_REPORT_URL" ]] || fail STOP_STEP_REPORT_PUBLICATION_FAILED
FACTS_URL="${STEP_REPORT_URL}facts.json"
VERIFY_URL="${STEP_REPORT_URL}verify.txt"
ROLLBACK_URL="${STEP_REPORT_URL}rollback.sh"

progress "externally verify current HTTPS artifacts"
http_get "$STEP_REPORT_URL" "$STEP_DIR/http-step-index.html"
http_get "$FACTS_URL" "$STEP_DIR/http-facts.json"
http_get "$VERIFY_URL" "$STEP_DIR/http-verify.txt"
http_get "$ROLLBACK_URL" "$STEP_DIR/http-rollback.sh"
http_get "$VM101_PUBLIC_URL" "$STEP_DIR/http-vm101-source-index.html"
http_get "$VM101_MODEL_URL" "$STEP_DIR/http-vm101-model-index.html"
http_get "$VM101_METHODS_URL" "$STEP_DIR/http-vm101-methods-index.html"
http_get "$LATEST_ROOT_URL" "$STEP_DIR/http-latest-index.html"
http_get "$OLD_ROOT_URL" "$STEP_DIR/http-old-index.html"

write_reports
finish
