#!/usr/bin/env bash set -Eeuo pipefail umask 077 MODE="${1:---check}" BASE="/opt/router-ops" REPOSITORY_URL="https://github.com/DChuvelev/wg-paid-vm101.git" REPO_DIR="$BASE/state/source-repositories/wg-paid-vm101" STATE_FILE="$BASE/state/current-vm101-git-source.env" TUNNEL_ENV="$BASE/state/current-quick-tunnel.env" TOKEN_FILE="$BASE/public/.router-public-token" PUBLISH_TOOL="$BASE/bin/router-publish-report" LOCK_DIR="$BASE/state/locks" LOCK_FILE="$LOCK_DIR/vm101-git-source-publish.lock" TMPDIR_RUN="$(mktemp -d /tmp/vm101-git-source-publish.XXXXXX)" VM101_OUTPUT="$TMPDIR_RUN/vm101-publish.txt" cleanup() { rm -rf "$TMPDIR_RUN" } trap cleanup EXIT INT TERM log() { echo ">>> [vm101-source] $*" } stop() { echo "STOP=$*" exit 1 } case "$MODE" in --check|--publish) ;; *) echo "Usage: $0 --check" echo " $0 --publish" exit 2 ;; esac mkdir -p \ "$(dirname "$REPO_DIR")" \ "$LOCK_DIR" exec 9>"$LOCK_FILE" if ! flock -n 9; then stop "ANOTHER_VM101_SOURCE_PUBLISH_IS_RUNNING" fi log "precheck" command -v git >/dev/null 2>&1 command -v tar >/dev/null 2>&1 command -v gzip >/dev/null 2>&1 command -v sha256sum >/dev/null 2>&1 command -v python3 >/dev/null 2>&1 command -v flock >/dev/null 2>&1 test -x "$PUBLISH_TOOL" test -f "$TOKEN_FILE" test -f "$TUNNEL_ENV" echo "MODE=$MODE" echo "REPOSITORY_URL=$REPOSITORY_URL" echo "REPO_DIR=$REPO_DIR" log "run VM101 git publisher through approved nested SSH" if [ "$MODE" = "--publish" ]; then VM101_MODE="--publish" else VM101_MODE="--check" fi set +e ssh \ -o BatchMode=yes \ -o ConnectTimeout=15 \ pve-mgts \ "ssh \ -n \ -o BatchMode=yes \ -o ConnectTimeout=15 \ -o StrictHostKeyChecking=accept-new \ -i /root/.ssh/pve_to_openwrt_mgts_ed25519 \ root@10.71.100.2 \ /usr/local/sbin/router-vm101-git-publish.sh \ $VM101_MODE" \ > "$VM101_OUTPUT" 2>&1 VM101_RC=$? set -e cat "$VM101_OUTPUT" echo echo "VM101_PUBLISHER_RC=$VM101_RC" if [ "$VM101_RC" -ne 0 ]; then stop "VM101_GIT_PUBLISHER_FAILED_RC_$VM101_RC" fi VM101_RESULT="$( awk -F= ' /^RESULT=/ { value=$2 } END { print value } ' "$VM101_OUTPUT" )" VM101_COMMIT="$( awk -F= ' /^GIT_COMMIT=/ { value=$2 } END { print value } ' "$VM101_OUTPUT" )" if [ -z "$VM101_COMMIT" ]; then VM101_COMMIT="$( awk -F= ' /^LOCAL_HEAD=/ { value=$2 } END { print value } ' "$VM101_OUTPUT" )" fi VM101_REMOTE_COMMIT="$( awk -F= ' /^REMOTE_COMMIT=/ { value=$2 } /^REMOTE_HEAD=/ { if (value == "") value=$2 } END { print value } ' "$VM101_OUTPUT" )" case "$VM101_COMMIT" in [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) ;; *) stop "INVALID_VM101_COMMIT_$VM101_COMMIT" ;; esac echo echo "VM101_RESULT=$VM101_RESULT" echo "VM101_COMMIT=$VM101_COMMIT" echo "VM101_REMOTE_COMMIT=$VM101_REMOTE_COMMIT" if [ -n "$VM101_REMOTE_COMMIT" ] && [ "$VM101_REMOTE_COMMIT" != "$VM101_COMMIT" ] then stop "VM101_LOCAL_REMOTE_COMMIT_MISMATCH" fi log "clone or refresh repository on VM130" if [ ! -d "$REPO_DIR/.git" ]; then rm -rf "$REPO_DIR" git clone \ --branch main \ "$REPOSITORY_URL" \ "$REPO_DIR" else CURRENT_ORIGIN="$( git -C "$REPO_DIR" remote get-url origin )" echo "CURRENT_ORIGIN=$CURRENT_ORIGIN" if [ "$CURRENT_ORIGIN" != "$REPOSITORY_URL" ]; then stop "UNEXPECTED_REPOSITORY_ORIGIN_$CURRENT_ORIGIN" fi git -C "$REPO_DIR" fetch \ --prune \ origin \ main git -C "$REPO_DIR" checkout \ -B main \ origin/main git -C "$REPO_DIR" reset \ --hard \ origin/main git -C "$REPO_DIR" clean \ -fdx fi VM130_COMMIT="$( git -C "$REPO_DIR" rev-parse HEAD )" VM130_TREE="$( git -C "$REPO_DIR" show -s --format=%T HEAD )" VM130_FILE_COUNT="$( git -C "$REPO_DIR" \ ls-tree \ -r \ --full-tree \ --name-only \ HEAD | awk 'END {print NR + 0}' )" VM130_STATUS="$( git -C "$REPO_DIR" status --porcelain )" echo echo "VM130_COMMIT=$VM130_COMMIT" echo "VM130_TREE=$VM130_TREE" echo "VM130_FILE_COUNT=$VM130_FILE_COUNT" echo "VM130_STATUS=${VM130_STATUS:-CLEAN}" test "$VM130_COMMIT" = "$VM101_COMMIT" test -z "$VM130_STATUS" log "verify required managed paths" for REQUIRED_PATH in \ etc/crontabs/root \ usr/local/sbin/router-vm101-git-publish.sh do if [ ! -f "$REPO_DIR/$REQUIRED_PATH" ]; then stop "REQUIRED_PATH_MISSING_$REQUIRED_PATH" fi echo "REQUIRED_PATH_OK=$REQUIRED_PATH" done if [ "$MODE" = "--check" ]; then echo echo "RESULT=PASS_VM130_VM101_GIT_SOURCE_CHECK" echo "VM101_COMMIT=$VM101_COMMIT" echo "VM130_COMMIT=$VM130_COMMIT" echo "VM130_TREE=$VM130_TREE" echo "VM130_FILE_COUNT=$VM130_FILE_COUNT" echo "VM101_PUSH_PERFORMED=false" echo "ARCHIVE_CREATED=false" echo "CLOUDFLARE_PUBLISHED=false" exit 0 fi log "create archive and metadata from exact Git commit" SHORT_COMMIT="${VM130_COMMIT:0:12}" GENERATED_AT_UTC="$(date -u +%Y%m%d-%H%M%S)" ARCHIVE_NAME="wg-paid-vm101-${SHORT_COMMIT}.tar.gz" ARCHIVE="$TMPDIR_RUN/$ARCHIVE_NAME" ARCHIVE_SHA_FILE="$TMPDIR_RUN/${ARCHIVE_NAME}.sha256" ARCHIVE_B64_NAME="${ARCHIVE_NAME}.b64.txt" ARCHIVE_B64="$TMPDIR_RUN/$ARCHIVE_B64_NAME" MANIFEST="$TMPDIR_RUN/git-tree-manifest.txt" PATHS="$TMPDIR_RUN/managed-paths.txt" COMMIT_INFO="$TMPDIR_RUN/git-commit.txt" FACTS="$TMPDIR_RUN/facts.json" git -C "$REPO_DIR" archive \ --format=tar.gz \ --prefix="wg-paid-vm101-${SHORT_COMMIT}/" \ "$VM130_COMMIT" \ > "$ARCHIVE" sha256sum "$ARCHIVE" \ > "$ARCHIVE_SHA_FILE" python3 - "$ARCHIVE" "$ARCHIVE_B64" <<'PY' import base64 import pathlib import sys import textwrap source = pathlib.Path(sys.argv[1]) output = pathlib.Path(sys.argv[2]) encoded = base64.b64encode(source.read_bytes()).decode("ascii") with output.open("w", encoding="ascii", newline="\n") as stream: for line in textwrap.wrap(encoded, 76): stream.write(line) stream.write("\n") PY git -C "$REPO_DIR" \ ls-tree \ -r \ --full-tree \ "$VM130_COMMIT" \ > "$MANIFEST" git -C "$REPO_DIR" \ ls-tree \ -r \ --full-tree \ --name-only \ "$VM130_COMMIT" \ > "$PATHS" git -C "$REPO_DIR" \ show \ -s \ --format=fuller \ "$VM130_COMMIT" \ > "$COMMIT_INFO" ARCHIVE_SHA256="$( awk '{print $1}' "$ARCHIVE_SHA_FILE" )" python3 - \ "$FACTS" \ "$GENERATED_AT_UTC" \ "$REPOSITORY_URL" \ "$VM130_COMMIT" \ "$VM130_TREE" \ "$VM130_FILE_COUNT" \ "$ARCHIVE_NAME" \ "$ARCHIVE_SHA256" <<'PY' import json import sys ( output, generated_at, repository, commit, tree, file_count, archive, archive_sha256, ) = sys.argv[1:] facts = { "result": "PASS_VM101_GIT_SOURCE_ARCHIVE", "generated_at_utc": generated_at, "repository": repository, "branch": "main", "commit": commit, "tree": tree, "file_count": int(file_count), "archive": archive, "archive_sha256": archive_sha256, "source_contract": "VM101 live worktree -> Git commit -> GitHub main -> VM130 checkout -> Git archive", } with open(output, "w", encoding="utf-8") as stream: json.dump(facts, stream, ensure_ascii=False, indent=2) stream.write("\n") PY python3 - "$FACTS" "$ARCHIVE_B64_NAME" <<'PY' import json import pathlib import sys facts_path = pathlib.Path(sys.argv[1]) archive_b64_name = sys.argv[2] facts = json.loads(facts_path.read_text(encoding="utf-8")) facts["archive_base64_transport"] = archive_b64_name facts["archive_base64_encoding"] = "base64" facts["archive_base64_decodes_to"] = facts["archive"] facts_path.write_text( json.dumps(facts, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) PY echo "ARCHIVE=$ARCHIVE" echo "ARCHIVE_SHA256=$ARCHIVE_SHA256" echo "ARCHIVE_BASE64=$ARCHIVE_B64" echo "MANIFEST=$MANIFEST" echo "PATHS=$PATHS" echo "FACTS=$FACTS" log "publish atomically into latest/ and archive prior version into old/" LABEL="vm101_git_source_${SHORT_COMMIT}" PUBLISH_OUTPUT="$( "$PUBLISH_TOOL" \ "$LABEL" \ "$ARCHIVE" \ "$ARCHIVE_SHA_FILE" \ "$ARCHIVE_B64" \ "$MANIFEST" \ "$PATHS" \ "$COMMIT_INFO" \ "$FACTS" \ "$VM101_OUTPUT" )" printf '%s\n' "$PUBLISH_OUTPUT" PUBLISHED_DIR="$( printf '%s\n' "$PUBLISH_OUTPUT" | awk -F= ' /^PUBLISHED_DIR=/ { print $2 } ' )" test -n "$PUBLISHED_DIR" test -d "$PUBLISHED_DIR" PUBLISHED_RELATIVE_PATH="$( printf '%s\n' "$PUBLISH_OUTPUT" | awk -F= ' /^PUBLISHED_RELATIVE_PATH=/ { print substr($0, index($0, "=") + 1) } ' )" PUBLIC_TOKEN="$(cat "$TOKEN_FILE")" QUICK_TUNNEL_URL="$( awk -F= ' $1 == "QUICK_TUNNEL_URL" { print substr($0, index($0, "=") + 1) } ' "$TUNNEL_ENV" )" test -n "$PUBLISHED_RELATIVE_PATH" case "$PUBLISHED_RELATIVE_PATH" in latest/*) ;; *) stop "INVALID_PUBLISHED_RELATIVE_PATH_$PUBLISHED_RELATIVE_PATH" ;; esac test -n "$QUICK_TUNNEL_URL" SOURCE_URL="${QUICK_TUNNEL_URL%/}/r/${PUBLIC_TOKEN}/${PUBLISHED_RELATIVE_PATH%/}/" ARCHIVE_URL="${SOURCE_URL}${ARCHIVE_NAME}" ARCHIVE_B64_URL="${SOURCE_URL}${ARCHIVE_B64_NAME}" FACTS_URL="${SOURCE_URL}facts.json" MANIFEST_URL="${SOURCE_URL}git-tree-manifest.txt" log "save current source archive state" STATE_TMP="${STATE_FILE}.tmp.$$" { echo "VM101_GIT_SOURCE_UPDATED_AT_UTC=$GENERATED_AT_UTC" echo "VM101_GIT_SOURCE_REPOSITORY=$REPOSITORY_URL" echo "VM101_GIT_SOURCE_BRANCH=main" echo "VM101_GIT_SOURCE_COMMIT=$VM130_COMMIT" echo "VM101_GIT_SOURCE_TREE=$VM130_TREE" echo "VM101_GIT_SOURCE_FILE_COUNT=$VM130_FILE_COUNT" echo "VM101_GIT_SOURCE_PUBLIC_DIR=$PUBLISHED_DIR" echo "VM101_GIT_SOURCE_URL=$SOURCE_URL" echo "VM101_GIT_SOURCE_ARCHIVE_URL=$ARCHIVE_URL" echo "VM101_GIT_SOURCE_ARCHIVE_B64_URL=$ARCHIVE_B64_URL" echo "VM101_GIT_SOURCE_FACTS_URL=$FACTS_URL" echo "VM101_GIT_SOURCE_MANIFEST_URL=$MANIFEST_URL" echo "VM101_GIT_SOURCE_ARCHIVE_SHA256=$ARCHIVE_SHA256" } > "$STATE_TMP" chmod 600 "$STATE_TMP" mv "$STATE_TMP" "$STATE_FILE" echo echo "RESULT=PASS_VM130_VM101_GIT_SOURCE_PUBLISH" echo "VM101_GIT_SOURCE_COMMIT=$VM130_COMMIT" echo "VM101_GIT_SOURCE_TREE=$VM130_TREE" echo "VM101_GIT_SOURCE_FILE_COUNT=$VM130_FILE_COUNT" echo "VM101_GIT_SOURCE_URL=$SOURCE_URL" echo "VM101_GIT_SOURCE_ARCHIVE_URL=$ARCHIVE_URL" echo "VM101_GIT_SOURCE_ARCHIVE_B64_URL=$ARCHIVE_B64_URL" echo "VM101_GIT_SOURCE_FACTS_URL=$FACTS_URL" echo "VM101_GIT_SOURCE_MANIFEST_URL=$MANIFEST_URL" echo "VM101_GIT_SOURCE_ARCHIVE_SHA256=$ARCHIVE_SHA256" echo "VM101_PUSH_PERFORMED=true" echo "ARCHIVE_CREATED=true" echo "CLOUDFLARE_PUBLISHED=true"