From f47915dadfa116de194782f2e64efe0224320f78 Mon Sep 17 00:00:00 2001 From: alexveley Date: Wed, 24 Jun 2026 02:08:51 -0400 Subject: [PATCH] Update pve RustDesk GA sync scripts --- README.md | 31 +++++----- blair-sync-rustdesk-via-ga.sh | 112 ++++++++++++++-------------------- install.sh | 19 +++--- 3 files changed, 68 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 3ecd2b3..a6837ca 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,27 @@ -# Temporary public Gitea repo for curling pve install scripts (delete when fleet rollout is done). -# Raw URLs: -# https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/install.sh -# https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/blair-sync-rustdesk-via-ga.sh +# Temporary public Gitea repo — pve curl install for Blair RustDesk GA sync. +# Delete or make private after fleet rollout. -## pve install (one command) +## Architecture (canonical) + +- **pve only:** `/usr/local/sbin/blair-sync-rustdesk-via-ga.sh` every **2 minutes** +- **Per seat:** QEMU guest agent only (no scheduled reporter task, no share JSON) +- Reads `RustDesk2.toml` via GA file-read; PowerShell exec fallback + +## pve install / upgrade ```bash curl -fsSL https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/install.sh | bash ``` -Prerequisite: VM 230 running, `qm guest cmd 230 ping` OK. - ## Manual test ```bash -/usr/local/sbin/blair-sync-rustdesk-via-ga.sh --dry-run -BLAIR_VERBOSE=1 BLAIR_VMIDS=230 /usr/local/sbin/blair-sync-rustdesk-via-ga.sh --dry-run +BLAIR_VERBOSE=1 /usr/local/sbin/blair-sync-rustdesk-via-ga.sh --dry-run BLAIR_VMIDS=230 /usr/local/sbin/blair-sync-rustdesk-via-ga.sh +tail -20 /var/log/blair-rustdesk-sync.log ``` -Re-install after updates: +## Deprecates -```bash -curl -fsSL https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/install.sh | bash -``` - -## Delete this repo when done - -Make private or delete from Gitea after fleet rollout. +- `sync-rustdesk-proxmox-notes.sh` (share report reader) +- Per-VM `Blair\Register-RustDeskId` scheduled task (optional to remove) diff --git a/blair-sync-rustdesk-via-ga.sh b/blair-sync-rustdesk-via-ga.sh index 864e3f8..ddb4dc3 100644 --- a/blair-sync-rustdesk-via-ga.sh +++ b/blair-sync-rustdesk-via-ga.sh @@ -1,7 +1,8 @@ #!/bin/bash # Poll RustDesk IDs via QEMU guest agent; update Proxmox VM notes + registry.json. -# Install to /usr/local/sbin on pve. Cron every 5m. No guest reporter or share reports needed. -# Usage: blair-sync-rustdesk-via-ga.sh [--dry-run] +# Canonical Blair fleet sync — no per-VM scheduled tasks or share reports required. +# Install: /usr/local/sbin/blair-sync-rustdesk-via-ga.sh Cron: every 2 minutes. +# Usage: blair-sync-rustdesk-via-ga.sh [--dry-run] BLAIR_VERBOSE=1 for detail. set -euo pipefail export BLAIR_SHARE="${BLAIR_SHARE:-/tank/blair-share}" @@ -29,23 +30,24 @@ DRY_RUN = os.environ.get('BLAIR_DRY_RUN', '') in ('1', 'true', 'yes') VERBOSE = os.environ.get('BLAIR_VERBOSE', '') in ('1', 'true', 'yes') VMIDS_ENV = os.environ.get('BLAIR_VMIDS', '').strip() +# RustDesk2.toml (LocalService) is the source of truth on Windows seats. GUEST_READ_PATHS = [ - r'C:\ProgramData\Blair\rustdesk-report.json', r'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml', r'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk.toml', r'C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config\RustDesk2.toml', r'C:\ProgramData\RustDesk\config\RustDesk2.toml', + r'C:\ProgramData\Blair\rustdesk-report.json', ] PS_READ_ID = r''' $ProgressPreference = 'SilentlyContinue' $ErrorActionPreference = 'SilentlyContinue' $paths = @( - 'C:\ProgramData\Blair\rustdesk-report.json', 'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml', 'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk.toml', 'C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config\RustDesk2.toml', - 'C:\ProgramData\RustDesk\config\RustDesk2.toml' + 'C:\ProgramData\RustDesk\config\RustDesk2.toml', + 'C:\ProgramData\Blair\rustdesk-report.json' ) foreach ($p in $paths) { if (-not (Test-Path -LiteralPath $p)) { continue } @@ -74,11 +76,13 @@ exit 1 ''' -def log(msg: str) -> None: - print(msg, flush=True) +def log(msg: str, *, force: bool = False) -> None: + if VERBOSE or force: + print(msg, flush=True) def update_desc(old: str, new_id: str) -> str: + old = old.replace('Registry%3A S%3A', 'Registry%3A Z%3A').replace('Registry: S:', 'Registry: Z:') if re.search(r'RustDesk\s+\d+', old): return re.sub(r'RustDesk\s+\d+', f'RustDesk {new_id}', old) if not old: @@ -141,17 +145,10 @@ def decode_ga_payload(raw) -> str: return text -def decode_exec_payload(st: dict) -> tuple[str, str]: - out = decode_ga_payload(st.get('out-data')) - err = decode_ga_payload(st.get('err-data')) - return out, err - - def clean_exec_text(text: str) -> str: if not text: return '' - text = re.sub(r'#< CLIXML.*', '', text, flags=re.DOTALL) - return text.strip() + return re.sub(r'#< CLIXML.*', '', text, flags=re.DOTALL).strip() def extract_id_from_exec(out: str, err: str) -> str | None: @@ -167,15 +164,14 @@ def extract_id_from_exec(out: str, err: str) -> str | None: def parse_exec_result(st: dict) -> tuple[str | None, str | None]: try: exitcode = int(st.get('exitcode', 1)) - out, err = decode_exec_payload(st) + out = decode_ga_payload(st.get('out-data')) + err = decode_ga_payload(st.get('err-data')) rid = extract_id_from_exec(out, err) if rid: return rid, None if exitcode != 0: brief = clean_exec_text(err) or clean_exec_text(out) or f'guest exit {exitcode}' - if len(brief) > 120: - brief = brief[:120] + '...' - return None, brief + return None, brief[:120] + ('...' if len(brief) > 120 else '') return None, f'no id in exec output: {out!r}' except Exception as exc: # noqa: BLE001 return None, f'exec decode error: {exc}' @@ -209,21 +205,16 @@ def guest_exec_text(vmid: int, command: list[str], timeout: int = 90) -> tuple[s try: st = json.loads(st_proc.stdout) except json.JSONDecodeError: - time.sleep(2) + time.sleep(1) continue if st.get('exited') != 1: - time.sleep(2) + time.sleep(1) continue return parse_exec_result(st) return None, 'guest exec timeout' -def decode_agent_file_content(raw) -> str | None: - text = decode_ga_payload(raw) - return text or None - - def parse_id_from_content(text: str, path: str) -> str | None: if path.lower().endswith('.json'): try: @@ -264,7 +255,7 @@ def guest_file_read(vmid: int, guest_path: str) -> tuple[str | None, str | None] raw = data.get('content') if raw is None: continue - text = decode_agent_file_content(raw) + text = decode_ga_payload(raw) if text: return text, None except (json.JSONDecodeError, ValueError, UnicodeError): @@ -272,45 +263,31 @@ def guest_file_read(vmid: int, guest_path: str) -> tuple[str | None, str | None] return None, f'file-read failed for {guest_path}' -def powershell_encoded(script: str) -> str: - return base64.b64encode(script.strip().encode('utf-16-le')).decode('ascii') - - -def guest_exec_powershell(vmid: int) -> tuple[str | None, str | None]: - enc = powershell_encoded(PS_READ_ID) - return guest_exec_text( - vmid, - ['powershell.exe', '-NoProfile', '-NonInteractive', '-EncodedCommand', enc], - timeout=120, - ) - - def read_rustdesk_id(vmid: int) -> tuple[str | None, str | None]: - errors: list[str] = [] for path in GUEST_READ_PATHS: try: text, err = guest_file_read(vmid, path) if not text: - if VERBOSE: - errors.append(f'{path}: {err}') + log(f'VM {vmid}: file-read miss {path}: {err}') continue rid = parse_id_from_content(text, path) if rid: - if VERBOSE: - log(f'VM {vmid}: ID {rid} from file-read {path}') + log(f'VM {vmid}: ID {rid} from {path}') return rid, None - errors.append(f'{path}: no id in file') - except Exception as exc: # noqa: BLE001 — keep sync loop alive - errors.append(f'{path}: {exc}') - continue + log(f'VM {vmid}: no id in {path}') + except Exception as exc: # noqa: BLE001 + log(f'VM {vmid}: {path}: {exc}') - rid, err = guest_exec_powershell(vmid) + enc = base64.b64encode(PS_READ_ID.strip().encode('utf-16-le')).decode('ascii') + rid, err = guest_exec_text( + vmid, + ['powershell.exe', '-NoProfile', '-NonInteractive', '-EncodedCommand', enc], + timeout=120, + ) if rid: - if VERBOSE: - log(f'VM {vmid}: ID {rid} from guest exec') + log(f'VM {vmid}: ID {rid} from guest exec') return rid, None - errors.append(f'exec: {err}') - return None, '; '.join(errors[-4:]) if errors else 'unknown' + return None, err or 'no RustDesk ID found' def qm_description(vmid: int) -> str: @@ -336,31 +313,29 @@ def save_registry(registry: dict) -> None: def main() -> int: if not os.path.isfile(REGISTRY): - log(f'Missing registry: {REGISTRY}') + log(f'Missing registry: {REGISTRY}', force=True) return 1 if DRY_RUN: - log('DRY RUN - no qm set or registry writes') + log('DRY RUN - no qm set or registry writes', force=True) registry = load_registry() vmids = fleet_vmids(registry) if not vmids: - log('No fleet VMIDs found') return 0 - registry_dirty = False + updated: list[int] = [] for vmid in vmids: if not qm_running(vmid): log(f'VM {vmid}: skip (not running)') continue if not guest_ping(vmid): - log(f'VM {vmid}: skip (guest agent not responding)') + log(f'VM {vmid}: skip (guest agent not responding)', force=True) continue new_id, err = read_rustdesk_id(vmid) if not new_id: - hint = ' (try BLAIR_VERBOSE=1)' if not VERBOSE else '' - log(f'VM {vmid}: skip (could not read RustDesk ID: {err}){hint}') + log(f'VM {vmid}: skip ({err})', force=True) continue old_registry_id = registry_old_id(registry, vmid) @@ -372,11 +347,12 @@ def main() -> int: continue newdesc = update_desc(current_desc, new_id) - log(f'VM {vmid}: {old_registry_id or old_desc_id or "(none)"} -> {new_id}') + log(f'VM {vmid}: {old_registry_id or old_desc_id or "(none)"} -> {new_id}', force=True) if DRY_RUN: preview = newdesc if len(newdesc) <= 120 else newdesc[:120] + '...' - log(f' would set description: {preview}') + log(f' would set: {preview}', force=True) + updated.append(vmid) continue subprocess.run(['qm', 'set', str(vmid), '-description', newdesc], check=True) @@ -384,11 +360,13 @@ def main() -> int: for seat in registry.get('windows_gpu_seats', []): if seat.get('vmid') == vmid: seat['rustdesk_id'] = new_id - registry_dirty = True - - if registry_dirty and not DRY_RUN: save_registry(registry) - log('Updated registry.json') + updated.append(vmid) + + if updated and not DRY_RUN: + log(f'Updated VM(s): {", ".join(str(v) for v in updated)}', force=True) + elif DRY_RUN and updated: + log(f'Would update VM(s): {", ".join(str(v) for v in updated)}', force=True) return 0 diff --git a/install.sh b/install.sh index 0e69262..b718232 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Install blair-sync-rustdesk-via-ga.sh from Gitea raw URL. Run on pve as root. +# Install or upgrade blair-sync-rustdesk-via-ga.sh from Gitea. Run on pve as root. set -euo pipefail BASE_URL="${BLAIR_SYNC_BASE_URL:-https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main}" @@ -7,6 +7,7 @@ TARGET='/usr/local/sbin/blair-sync-rustdesk-via-ga.sh' SHARE_COPY='/tank/blair-share/tools/golden/host/sync-rustdesk-via-guest-agent.sh' CRON='/etc/cron.d/blair-rustdesk-sync' LOG='/var/log/blair-rustdesk-sync.log' +CRON_LINE="*/2 * * * * root $TARGET >> $LOG 2>&1" echo "=== Download sync script ===" curl -fsSL "$BASE_URL/blair-sync-rustdesk-via-ga.sh" -o "$TARGET" @@ -17,22 +18,20 @@ mkdir -p "$(dirname "$SHARE_COPY")" cp -f "$TARGET" "$SHARE_COPY" chmod +x "$SHARE_COPY" -echo "=== shebang check ===" -head -1 "$TARGET" | od -An -tx1 -file "$TARGET" - -echo "=== cron ===" -printf '%s\n' "*/5 * * * * root $TARGET >> $LOG 2>&1" > "$CRON" +echo "=== cron (every 2 min) ===" +printf '%s\n' "$CRON_LINE" > "$CRON" chmod 644 "$CRON" cat "$CRON" -echo "=== dry run ===" +echo "=== dry run (verbose) ===" BLAIR_VERBOSE=1 "$TARGET" --dry-run echo "=== live run ===" "$TARGET" -echo "=== VM 230 description ===" -qm config 230 | grep ^description || true +echo "=== fleet descriptions ===" +for v in 230 231 232 233 234 235; do + qm config "$v" 2>/dev/null | grep ^description || true +done echo "=== done ==="