#!/usr/bin/env bash set -euo pipefail DEFAULT_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" HOST_DIRECTORY="${WINDOWS99_VMX_BACKUP_SEARCH_HOST_DIRECTORY:-$DEFAULT_HOSTS}" HOSTS="${WINDOWS99_VMX_BACKUP_SEARCH_HOSTS:-$DEFAULT_HOSTS}" 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 entry="${entry//[[:space:]]/}" [[ -n "$entry" ]] || continue alias_name="${entry%%=*}" ssh_target="" if [[ "$entry" == *=* ]]; then ssh_target="${entry#*=}" else alias_name="$entry" IFS=',' read -r -a directory_entries <<<"$HOST_DIRECTORY" for directory_entry in "${directory_entries[@]}"; do directory_entry="${directory_entry//[[:space:]]/}" [[ -n "$directory_entry" && "$directory_entry" == *=* ]] || continue if [[ "${directory_entry%%=*}" == "$alias_name" ]]; then ssh_target="${directory_entry#*=}" break fi done fi if [[ -z "$ssh_target" || "$ssh_target" == "$alias_name" ]]; then blocked_hosts=$((blocked_hosts + 1)) printf 'HOST_SEARCH alias=%s status=blocked_host_alias_unresolved candidate_source_count=0 path_fingerprints=\n' \ "$alias_name" continue fi remote_script=$(cat </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