Update pve RustDesk GA sync scripts

This commit is contained in:
alexveley
2026-06-24 02:08:51 -04:00
parent 7151e11d34
commit f47915dadf
3 changed files with 68 additions and 94 deletions
+45 -67
View File
@@ -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