fix(reboot): add windows99 vmx backup search package
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m14s
CD Pipeline / build-and-deploy (push) Successful in 4m45s
CD Pipeline / post-deploy-checks (push) Successful in 1m57s

This commit is contained in:
Your Name
2026-07-03 18:06:34 +08:00
parent b0dc42aa6f
commit 796b66c967
14 changed files with 1260 additions and 56 deletions

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env bash
set -euo pipefail
HOSTS="${WINDOWS99_VMX_BACKUP_SEARCH_HOSTS:-110=wooo@192.168.0.110,120=wooo@192.168.0.120,121=wooo@192.168.0.121,188=wooo@192.168.0.188,112=wooo@192.168.0.112}"
SEARCH_ROOTS="${WINDOWS99_VMX_BACKUP_SEARCH_ROOTS:-/home/wooo /mnt /backup /backups /var/backups}"
MAX_DEPTH="${WINDOWS99_VMX_BACKUP_SEARCH_MAX_DEPTH:-7}"
CONNECT_TIMEOUT="${WINDOWS99_VMX_BACKUP_SEARCH_CONNECT_TIMEOUT:-8}"
SSH_TIMEOUT="${WINDOWS99_VMX_BACKUP_SEARCH_TIMEOUT:-35}"
KNOWN_HOSTS_FILE="${WINDOWS99_VMX_BACKUP_SEARCH_KNOWN_HOSTS_FILE:-/tmp/awoooi-windows99-vmx-backup-search-known_hosts}"
usage() {
cat <<'USAGE'
usage: collect-windows99-vmx-source-backup-search.sh [options]
Runs a public-key-only, no-content VMX source/backup filename search across
Linux hosts that may hold recovery artifacts for the missing Windows99 111 VMX.
It emits path fingerprints only. It never reads VMX file contents, writes files,
starts/stops VMs, restarts services, changes registry/task state, or reboots.
Options:
--hosts LIST Comma list like 110=wooo@192.168.0.110,120=wooo@192.168.0.120
--roots LIST Space-separated roots to search. Default: common backup roots
--max-depth N find maxdepth. Default: 7
--timeout SECONDS Per-host SSH timeout. Default: 35
-h, --help Show this help
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--hosts)
shift
HOSTS="${1:?--hosts requires a value}"
;;
--roots)
shift
SEARCH_ROOTS="${1:?--roots requires a value}"
;;
--max-depth)
shift
MAX_DEPTH="${1:?--max-depth requires a value}"
;;
--timeout)
shift
SSH_TIMEOUT="${1:?--timeout requires a value}"
;;
-h|--help)
usage
exit 0
;;
*)
printf 'error=unknown_argument:%s\n' "$1" >&2
usage >&2
exit 64
;;
esac
shift
done
SSH_OPTS=(
-o BatchMode=yes
-o PreferredAuthentications=publickey
-o PubkeyAuthentication=yes
-o PasswordAuthentication=no
-o KbdInteractiveAuthentication=no
-o NumberOfPasswordPrompts=0
-o ConnectTimeout="${CONNECT_TIMEOUT}"
-o ConnectionAttempts=1
-o StrictHostKeyChecking=no
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
-o LogLevel=ERROR
)
printf 'schema_version=windows99_vmx_source_backup_search_collector_v1\n'
printf 'target_host_alias=99\n'
printf 'target_vm_alias=111\n'
printf 'search_mode=filename_path_fingerprint_only\n'
printf 'search_hosts=%s\n' "$HOSTS"
printf 'search_roots_profile=linux_backup_roots\n'
printf 'max_depth=%s\n' "$MAX_DEPTH"
printf 'secret_value_read=false\n'
printf 'password_prompt_allowed=false\n'
printf 'file_content_read=false\n'
printf 'raw_path_output=false\n'
printf 'path_fingerprint_only=true\n'
printf 'remote_write_performed=false\n'
printf 'host_reboot_performed=false\n'
printf 'vm_power_change_performed=false\n'
printf 'windows_service_restart_performed=false\n'
printf 'windows_registry_apply_performed=false\n'
printf 'scheduled_task_write_performed=false\n'
total_candidates=0
searched_hosts=0
blocked_hosts=0
IFS=',' read -r -a host_entries <<<"$HOSTS"
for entry in "${host_entries[@]}"; do
[[ -n "$entry" && "$entry" == *=* ]] || continue
alias_name="${entry%%=*}"
ssh_target="${entry#*=}"
remote_script=$(cat <<REMOTE
set -euo pipefail
{ find ${SEARCH_ROOTS} -maxdepth ${MAX_DEPTH} \\( -iname "*.vmx" -o -iname "*192.168.0.111*" -o -iname "*111*Ubuntu*" \\) 2>/dev/null || true; } \\
| while IFS= read -r candidate_path; do
printf '%s' "\$candidate_path" | sha256sum | awk '{print \$1}'
done \\
| sed -n '1,120p'
REMOTE
)
output=""
status="searched"
if output="$(timeout "${SSH_TIMEOUT}s" ssh "${SSH_OPTS[@]}" "$ssh_target" "bash -s" <<<"$remote_script" 2>/dev/null)"; then
searched_hosts=$((searched_hosts + 1))
else
status="blocked_ssh_batchmode_unavailable"
blocked_hosts=$((blocked_hosts + 1))
fi
candidate_count=0
fingerprints=""
if [[ -n "$output" ]]; then
candidate_count="$(printf '%s\n' "$output" | sed '/^$/d' | wc -l | tr -d ' ')"
fingerprints="$(printf '%s\n' "$output" | sed '/^$/d' | paste -sd, -)"
fi
total_candidates=$((total_candidates + candidate_count))
printf 'HOST_SEARCH alias=%s status=%s candidate_source_count=%s path_fingerprints=%s\n' \
"$alias_name" "$status" "$candidate_count" "$fingerprints"
done
printf 'searched_host_count=%s\n' "$searched_hosts"
printf 'blocked_host_count=%s\n' "$blocked_hosts"
printf 'candidate_source_count=%s\n' "$total_candidates"
if [[ "$total_candidates" -gt 0 ]]; then
printf 'search_status=candidate_fingerprint_found_requires_internal_path_resolution\n'
printf 'safe_next_step=resolve_candidate_fingerprint_to_authorized_internal_path_then_build_scoped_relink_dry_run_no_write\n'
else
printf 'search_status=blocked_no_verified_vmx_source_found\n'
printf 'safe_next_step=continue_verified_backup_inventory_search_or_authorized_console_source_path_then_scoped_dry_run_no_write\n'
fi

View File

@@ -0,0 +1,118 @@
from __future__ import annotations
import os
import subprocess
import textwrap
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = (
ROOT
/ "scripts"
/ "reboot-recovery"
/ "collect-windows99-vmx-source-backup-search.sh"
)
def _write_executable(path: Path, text: str) -> None:
path.write_text(textwrap.dedent(text).lstrip())
path.chmod(0o755)
def _key_values(stdout: str) -> dict[str, str]:
values: dict[str, str] = {}
for line in stdout.splitlines():
if "=" not in line or line.startswith("HOST_SEARCH "):
continue
key, value = line.split("=", 1)
values[key] = value
return values
def test_backup_search_collector_is_no_secret_no_content_check_mode() -> None:
text = SCRIPT.read_text(encoding="utf-8")
assert "windows99_vmx_source_backup_search_collector_v1" in text
assert "BatchMode=yes" in text
assert "PreferredAuthentications=publickey" in text
assert "PasswordAuthentication=no" in text
assert "KbdInteractiveAuthentication=no" in text
assert "NumberOfPasswordPrompts=0" in text
assert "path_fingerprint_only=true" in text
assert "file_content_read=false" in text
assert "remote_write_performed=false" in text
for forbidden in [
"sshpass",
"PasswordAuthentication=yes",
"KbdInteractiveAuthentication=yes",
"scp ",
"sudo ",
"cat \"$candidate_path\"",
"Get-Content",
"Start-VM",
"vmrun start",
"shutdown",
"Restart-Computer",
"Set-Content",
"Copy-Item",
"Move-Item",
"Remove-Item",
]:
assert forbidden not in text
def test_backup_search_collector_emits_fingerprints_not_paths(tmp_path: Path) -> None:
fake_bin = tmp_path / "bin"
fake_bin.mkdir()
_write_executable(
fake_bin / "ssh",
"""
#!/usr/bin/env bash
target="${@: -2:1}"
if [[ "$target" == *"blocked"* ]]; then
exit 255
fi
printf '%s\n' 'fp-one'
printf '%s\n' 'fp-two'
""",
)
env = os.environ.copy()
env["PATH"] = f"{fake_bin}:{env['PATH']}"
env["WINDOWS99_VMX_BACKUP_SEARCH_KNOWN_HOSTS_FILE"] = str(
fake_bin / "known_hosts"
)
result = subprocess.run(
[
"bash",
str(SCRIPT),
"--hosts",
"ok=wooo@ok,blocked=wooo@blocked",
"--timeout",
"3",
],
cwd=ROOT,
env=env,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0
values = _key_values(result.stdout)
assert values["schema_version"] == "windows99_vmx_source_backup_search_collector_v1"
assert values["target_vm_alias"] == "111"
assert values["candidate_source_count"] == "2"
assert values["searched_host_count"] == "1"
assert values["blocked_host_count"] == "1"
assert "HOST_SEARCH alias=ok status=searched candidate_source_count=2" in result.stdout
assert "HOST_SEARCH alias=blocked status=blocked_ssh_batchmode_unavailable" in (
result.stdout
)
assert "path_fingerprints=fp-one,fp-two" in result.stdout
assert "192.168.0." not in result.stdout
assert "/home/wooo" not in result.stdout
assert "file_content_read=false" in result.stdout
assert "remote_write_performed=false" in result.stdout
assert "vm_power_change_performed=false" in result.stdout