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
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:
139
scripts/reboot-recovery/collect-windows99-vmx-source-backup-search.sh
Executable file
139
scripts/reboot-recovery/collect-windows99-vmx-source-backup-search.sh
Executable 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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user