fix: SAC pushes RDP bundle from git via WinRM, no client git (0.20.14)

This commit is contained in:
PTah
2026-06-20 19:49:20 +10:00
parent 191fb4e2cf
commit ca90f5c296
4 changed files with 283 additions and 76 deletions
+209 -52
View File
@@ -2,11 +2,15 @@
from __future__ import annotations
import base64
import re
import socket
from dataclasses import dataclass
from pathlib import Path
from app.config import get_settings
from app.models import Host
from app.services.agent_git_release import _cache_dir, clone_or_update_repo
_CLIXML_PROGRESS_NOISE = frozenset(
{
@@ -16,6 +20,26 @@ _CLIXML_PROGRESS_NOISE = frozenset(
)
_CLIXML_MARKER = "#< CLIXML"
RDP_REMOTE_STAGING = r"C:\ProgramData\RDP-login-monitor\_sac_staging"
RDP_BUNDLE_FILES = (
"Login_Monitor.ps1",
"Sac-Client.ps1",
"RdpMonitor-TaskQuery.ps1",
"version.txt",
"Deploy-LoginMonitor.ps1",
"Restart-RdpLoginMonitor.ps1",
"login_monitor.settings.example.ps1",
)
RDP_BUNDLE_REQUIRED = frozenset(
{
"Login_Monitor.ps1",
"Sac-Client.ps1",
"version.txt",
"Deploy-LoginMonitor.ps1",
}
)
_WINRM_FILE_CHUNK_BYTES = 20_000
class WinAdminNotConfiguredError(Exception):
pass
@@ -289,55 +313,103 @@ def _wrap_powershell_body(body: str) -> str:
)
def _rdp_update_powershell_script(
script_path: str,
*,
repo_url: str = "",
git_branch: str = "main",
) -> str:
script = script_path.strip()
if script:
literal = _powershell_literal_path(script)
body = (
f"$p = '{literal}'\n"
"if (-not (Test-Path -LiteralPath $p)) { throw \"Deploy script not found: $p\" }\n"
"Write-Output \"Running: $p\"\n"
"& $p"
)
return _wrap_powershell_body(body)
def _custom_deploy_body(script_path: str) -> str:
literal = _powershell_literal_path(script_path.strip())
return (
f"$p = '{literal}'\n"
"if (-not (Test-Path -LiteralPath $p)) { throw \"Deploy script not found: $p\" }\n"
"Write-Output \"Running: $p\"\n"
"& $p"
)
safe_repo = _powershell_literal_path(
(repo_url or "https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git").strip()
def _prepare_staging_body(staging_path: str) -> str:
literal = _powershell_literal_path(staging_path)
return (
f"$staging = '{literal}'\n"
"if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force }\n"
"New-Item -ItemType Directory -Path $staging -Force | Out-Null\n"
"Write-Output \"Staging ready: $staging\"\n"
)
safe_branch = _powershell_literal_path((git_branch or "main").strip() or "main")
body = (
f"$gitUrl = '{safe_repo}'\n"
f"$branch = '{safe_branch}'\n"
"$repoDir = Join-Path $env:ProgramData 'RDP-login-monitor\\_sac_git'\n"
"$gitCmd = Get-Command git.exe -ErrorAction SilentlyContinue\n"
"if (-not $gitCmd) {\n"
" throw 'git.exe not found on client PC — install Git for Windows or set win_agent_update_script in SAC'\n"
"}\n"
"Write-Output \"git: $gitUrl (branch $branch) -> $repoDir\"\n"
"if (-not (Test-Path -LiteralPath (Join-Path $repoDir '.git'))) {\n"
" if (Test-Path -LiteralPath $repoDir) { Remove-Item -LiteralPath $repoDir -Recurse -Force }\n"
" New-Item -ItemType Directory -Path (Split-Path $repoDir -Parent) -Force | Out-Null\n"
" & git.exe clone -b $branch $gitUrl $repoDir\n"
" if ($LASTEXITCODE -ne 0) { throw \"git clone failed (exit $LASTEXITCODE)\" }\n"
"} else {\n"
" Push-Location $repoDir\n"
" & git.exe fetch origin\n"
" & git.exe reset --hard \"origin/$branch\"\n"
" $code = $LASTEXITCODE\n"
" Pop-Location\n"
" if ($code -ne 0) { throw \"git reset failed (exit $code)\" }\n"
"}\n"
"$deploy = Join-Path $repoDir 'Deploy-LoginMonitor.ps1'\n"
"if (-not (Test-Path -LiteralPath $deploy)) { throw \"Deploy-LoginMonitor.ps1 missing in git checkout: $repoDir\" }\n"
"Write-Output \"Running: $deploy -SourceShareRoot $repoDir\"\n"
"& $deploy -SourceShareRoot $repoDir\n"
def _deploy_from_staging_body(staging_path: str) -> str:
literal = _powershell_literal_path(staging_path)
return (
f"$staging = '{literal}'\n"
"$deploy = Join-Path $staging 'Deploy-LoginMonitor.ps1'\n"
"if (-not (Test-Path -LiteralPath $deploy)) { throw \"Deploy-LoginMonitor.ps1 missing in staging\" }\n"
"Write-Output \"Running: $deploy -SourceShareRoot $staging\"\n"
"& $deploy -SourceShareRoot $staging\n"
)
def _write_file_chunk_body(remote_path: str, b64_chunk: str, *, append: bool) -> str:
literal = _powershell_literal_path(remote_path)
if append:
return (
f"$path = '{literal}'\n"
f"$bytes = [Convert]::FromBase64String('{b64_chunk}')\n"
"$stream = [System.IO.File]::Open($path, [System.IO.FileMode]::Append)\n"
"try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Close() }\n"
)
return (
f"$path = '{literal}'\n"
f"$bytes = [Convert]::FromBase64String('{b64_chunk}')\n"
"$dir = Split-Path -LiteralPath $path -Parent\n"
"if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }\n"
"[System.IO.File]::WriteAllBytes($path, $bytes)\n"
)
def _winrm_push_file(
*,
target: str,
user: str,
password: str,
remote_path: str,
data: bytes,
) -> WinRmCmdResult:
last: WinRmCmdResult | None = None
if not data:
body = (
f"$path = '{_powershell_literal_path(remote_path)}'\n"
"$dir = Split-Path -LiteralPath $path -Parent\n"
"if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }\n"
"[System.IO.File]::WriteAllBytes($path, [byte[]]@())\n"
)
return run_winrm_ps(
target=target,
user=user,
password=password,
script=_wrap_powershell_body(body),
timeout_sec=120,
)
for offset in range(0, len(data), _WINRM_FILE_CHUNK_BYTES):
chunk = data[offset : offset + _WINRM_FILE_CHUNK_BYTES]
b64_chunk = base64.b64encode(chunk).decode("ascii")
body = _write_file_chunk_body(remote_path, b64_chunk, append=offset > 0)
last = run_winrm_ps(
target=target,
user=user,
password=password,
script=_wrap_powershell_body(body),
timeout_sec=120,
)
if not last.ok:
return last
assert last is not None
return last
def _fetch_rdp_bundle_dir(repo_url: str, git_branch: str) -> Path:
settings = get_settings()
return clone_or_update_repo(
repo_url,
git_branch,
_cache_dir(),
token=(settings.sac_agent_git_token or "").strip(),
)
return _wrap_powershell_body(body)
def run_winrm_rdp_monitor_update(
@@ -349,17 +421,102 @@ def run_winrm_rdp_monitor_update(
repo_url: str = "",
git_branch: str = "main",
) -> WinRmCmdResult:
return run_winrm_ps(
target = target.strip()
if script_path.strip():
return run_winrm_ps(
target=target,
user=user,
password=password,
script=_wrap_powershell_body(_custom_deploy_body(script_path)),
timeout_sec=900,
)
effective_repo = (
repo_url or "https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git"
).strip()
effective_branch = (git_branch or "main").strip() or "main"
if not effective_repo:
return WinRmCmdResult(
ok=False,
message="rdp_git_repo_url is not configured in SAC",
target=target,
)
try:
repo_dir = _fetch_rdp_bundle_dir(effective_repo, effective_branch)
except Exception as exc:
return WinRmCmdResult(
ok=False,
message=f"SAC failed to fetch RDP bundle from git: {exc}",
target=target,
)
missing = [name for name in RDP_BUNDLE_REQUIRED if not (repo_dir / name).is_file()]
if missing:
return WinRmCmdResult(
ok=False,
message=f"RDP bundle incomplete on SAC: missing {', '.join(missing)}",
target=target,
)
staging = RDP_REMOTE_STAGING
prep = run_winrm_ps(
target=target,
user=user,
password=password,
script=_rdp_update_powershell_script(
script_path,
repo_url=repo_url,
git_branch=git_branch,
),
script=_wrap_powershell_body(_prepare_staging_body(staging)),
timeout_sec=120,
)
if not prep.ok:
return prep
pushed: list[str] = []
log_parts = [prep.stdout]
for filename in RDP_BUNDLE_FILES:
local_file = repo_dir / filename
if not local_file.is_file():
continue
remote_file = f"{staging}\\{filename}"
push_result = _winrm_push_file(
target=target,
user=user,
password=password,
remote_path=remote_file,
data=local_file.read_bytes(),
)
log_parts.append(push_result.stdout)
if not push_result.ok:
return WinRmCmdResult(
ok=False,
message=f"Failed to push {filename} to client: {push_result.message}",
target=target,
stdout="\n".join(part.strip() for part in log_parts if part.strip()),
stderr=push_result.stderr,
exit_code=push_result.exit_code,
)
pushed.append(filename)
deploy = run_winrm_ps(
target=target,
user=user,
password=password,
script=_wrap_powershell_body(_deploy_from_staging_body(staging)),
timeout_sec=900,
)
header = f"SAC pushed {len(pushed)} file(s) from git to {staging}"
stdout = "\n\n".join(
part.strip()
for part in [header, "\n".join(part.strip() for part in log_parts if part.strip()), deploy.stdout]
if part.strip()
)
return WinRmCmdResult(
ok=deploy.ok,
message=deploy.message,
target=target,
stdout=stdout,
stderr=deploy.stderr,
exit_code=deploy.exit_code,
)
def run_winrm_on_host_targets(