fix: WinRM RDP update via run_ps, strip CLIXML noise (0.20.11)
This commit is contained in:
@@ -2,12 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import re
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.models import Host
|
||||
|
||||
_CLIXML_PROGRESS_NOISE = frozenset(
|
||||
{
|
||||
"Preparing modules for first use.",
|
||||
"Preparing modules fo",
|
||||
}
|
||||
)
|
||||
_CLIXML_MARKER = "#< CLIXML"
|
||||
|
||||
|
||||
class WinAdminNotConfiguredError(Exception):
|
||||
pass
|
||||
@@ -71,6 +79,129 @@ def _winrm_session(
|
||||
return session, winrm
|
||||
|
||||
|
||||
def _clixml_to_plain(text: str) -> str:
|
||||
"""Turn WinRM/PowerShell CLIXML blobs into readable text; drop progress noise."""
|
||||
raw = text or ""
|
||||
if _CLIXML_MARKER not in raw:
|
||||
return raw.strip()
|
||||
|
||||
messages = re.findall(r'<S N="Message">([^<]*)</S>', raw, flags=re.IGNORECASE)
|
||||
messages = [
|
||||
msg.strip()
|
||||
for msg in messages
|
||||
if msg.strip() and msg.strip() not in _CLIXML_PROGRESS_NOISE
|
||||
]
|
||||
to_strings = re.findall(r"<ToString>([^<]*)</ToString>", raw, flags=re.IGNORECASE)
|
||||
to_strings = [
|
||||
text.strip()
|
||||
for text in to_strings
|
||||
if text.strip() and "Preparing modules" not in text
|
||||
]
|
||||
parts = [*messages, *to_strings]
|
||||
if parts:
|
||||
return "\n".join(parts)
|
||||
|
||||
without = re.sub(r"#< CLIXML.*", "", raw, flags=re.DOTALL).strip()
|
||||
return without
|
||||
|
||||
|
||||
def _winrm_failure_detail(stdout: str, stderr: str, exit_code: int) -> str:
|
||||
stdout_plain = _clixml_to_plain(stdout)
|
||||
stderr_plain = _clixml_to_plain(stderr)
|
||||
|
||||
for candidate in (stderr_plain, stdout_plain):
|
||||
if candidate and _CLIXML_MARKER not in candidate:
|
||||
line = candidate.strip().splitlines()[0][:2000]
|
||||
if line.startswith("ERROR:"):
|
||||
return line[6:].strip() or line
|
||||
return line
|
||||
|
||||
if stdout_plain:
|
||||
return stdout_plain[:2000]
|
||||
if stderr_plain:
|
||||
return stderr_plain[:2000]
|
||||
return (
|
||||
f"PowerShell exit code {exit_code} "
|
||||
"(CLIXML без текста ошибки — проверьте Deploy-LoginMonitor.ps1 на ПК)"
|
||||
)
|
||||
|
||||
|
||||
def _sanitize_winrm_streams(stdout: str, stderr: str) -> tuple[str, str]:
|
||||
return _clixml_to_plain(stdout), _clixml_to_plain(stderr)
|
||||
|
||||
|
||||
def _finalize_winrm_run(
|
||||
*,
|
||||
target: str,
|
||||
stdout: str,
|
||||
stderr: str,
|
||||
exit_code: int,
|
||||
) -> WinRmCmdResult:
|
||||
ok = exit_code == 0
|
||||
if ok:
|
||||
message = f"WinRM OK ({target}), exit 0"
|
||||
if stdout.strip():
|
||||
message = f"{message}\n{stdout.strip().splitlines()[0][:200]}"
|
||||
else:
|
||||
detail = _winrm_failure_detail(stdout, stderr, exit_code)
|
||||
message = f"WinRM command failed ({target}): {detail}"
|
||||
return WinRmCmdResult(
|
||||
ok=ok,
|
||||
message=message,
|
||||
target=target,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
exit_code=exit_code,
|
||||
)
|
||||
|
||||
|
||||
def run_winrm_ps(
|
||||
*,
|
||||
target: str,
|
||||
user: str,
|
||||
password: str,
|
||||
script: str,
|
||||
timeout_sec: int = 30,
|
||||
) -> WinRmCmdResult:
|
||||
target = target.strip()
|
||||
try:
|
||||
session, winrm = _winrm_session(target, user, password, timeout_sec=timeout_sec)
|
||||
result = session.run_ps(script)
|
||||
except winrm.exceptions.WinRMTransportError as exc:
|
||||
detail = str(exc)
|
||||
hint = ""
|
||||
if "credentials were rejected" in detail.lower():
|
||||
hint = " Проверьте логин (B26\\user), пароль и WinRM на ПК."
|
||||
return WinRmCmdResult(
|
||||
ok=False,
|
||||
message=f"WinRM transport error ({target}): {detail}{hint}",
|
||||
target=target,
|
||||
)
|
||||
except (socket.timeout, TimeoutError):
|
||||
return WinRmCmdResult(
|
||||
ok=False,
|
||||
message=f"WinRM timeout ({timeout_sec}s) to {target}:5985",
|
||||
target=target,
|
||||
)
|
||||
except WinAdminNotConfiguredError as exc:
|
||||
return WinRmCmdResult(ok=False, message=str(exc), target=target)
|
||||
except RuntimeError as exc:
|
||||
return WinRmCmdResult(ok=False, message=str(exc), target=target)
|
||||
except Exception as exc:
|
||||
return WinRmCmdResult(ok=False, message=f"WinRM error ({target}): {exc}", target=target)
|
||||
|
||||
stdout = (result.std_out or b"").decode("utf-8", errors="replace")
|
||||
stderr = (result.std_err or b"").decode("utf-8", errors="replace")
|
||||
exit_code = int(result.status_code)
|
||||
stdout, stderr = _sanitize_winrm_streams(stdout, stderr)
|
||||
return _finalize_winrm_run(
|
||||
target=target,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
exit_code=exit_code,
|
||||
)
|
||||
|
||||
|
||||
def run_winrm_cmd(
|
||||
*,
|
||||
target: str,
|
||||
@@ -109,15 +240,8 @@ def run_winrm_cmd(
|
||||
stdout = (result.std_out or b"").decode("utf-8", errors="replace")
|
||||
stderr = (result.std_err or b"").decode("utf-8", errors="replace")
|
||||
exit_code = int(result.status_code)
|
||||
ok = exit_code == 0
|
||||
if ok:
|
||||
message = f"WinRM OK ({target}), exit 0"
|
||||
else:
|
||||
detail = stderr.strip() or stdout.strip() or f"exit code {exit_code}"
|
||||
message = f"WinRM command failed ({target}): {detail[:500]}"
|
||||
return WinRmCmdResult(
|
||||
ok=ok,
|
||||
message=message,
|
||||
stdout, stderr = _sanitize_winrm_streams(stdout, stderr)
|
||||
return _finalize_winrm_run(
|
||||
target=target,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
@@ -141,38 +265,52 @@ def run_winrm_logoff(*, target: str, user: str, password: str, session_id: int)
|
||||
)
|
||||
|
||||
|
||||
def _encode_powershell(script: str) -> str:
|
||||
return base64.b64encode(script.encode("utf-16-le")).decode("ascii")
|
||||
def _powershell_script_prelude() -> str:
|
||||
return (
|
||||
"$ProgressPreference = 'SilentlyContinue'\n"
|
||||
"$InformationPreference = 'SilentlyContinue'\n"
|
||||
)
|
||||
|
||||
|
||||
def _powershell_literal_path(path: str) -> str:
|
||||
return path.replace("'", "''")
|
||||
|
||||
|
||||
def _winrm_rdp_update_remote_cmd(script_path: str) -> str:
|
||||
def _wrap_powershell_body(body: str) -> str:
|
||||
indented = "\n".join(f" {line}" if line else "" for line in body.splitlines())
|
||||
return (
|
||||
_powershell_script_prelude()
|
||||
+ "try {\n"
|
||||
+ indented
|
||||
+ "\n} catch {\n"
|
||||
+ " Write-Output ('ERROR: ' + $_.Exception.Message)\n"
|
||||
+ " exit 1\n"
|
||||
+ "}\n"
|
||||
)
|
||||
|
||||
|
||||
def _rdp_update_powershell_script(script_path: str) -> str:
|
||||
script = script_path.strip()
|
||||
if script:
|
||||
literal = _powershell_literal_path(script)
|
||||
ps = (
|
||||
body = (
|
||||
f"$p = '{literal}'\n"
|
||||
"if (-not (Test-Path -LiteralPath $p)) { throw 'Deploy script not found' }\n"
|
||||
"if (-not (Test-Path -LiteralPath $p)) { throw \"Deploy script not found: $p\" }\n"
|
||||
"& $p"
|
||||
)
|
||||
else:
|
||||
ps = (
|
||||
body = (
|
||||
"$candidates = @(\n"
|
||||
" (Join-Path $env:ProgramData 'RDP-login-monitor\\Deploy-LoginMonitor.ps1'),\n"
|
||||
" (Join-Path $env:ProgramData 'LoginMonitor\\Deploy-LoginMonitor.ps1'),\n"
|
||||
" (Join-Path $env:USERPROFILE 'Deploy-LoginMonitor.ps1')\n"
|
||||
")\n"
|
||||
"$p = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1\n"
|
||||
"if (-not $p) { throw 'Deploy-LoginMonitor.ps1 not found' }\n"
|
||||
"if (-not $p) { throw 'Deploy-LoginMonitor.ps1 not found on client PC' }\n"
|
||||
"Write-Output \"Running: $p\"\n"
|
||||
"& $p"
|
||||
)
|
||||
encoded = _encode_powershell(ps)
|
||||
return (
|
||||
"powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass "
|
||||
f"-EncodedCommand {encoded}"
|
||||
)
|
||||
return _wrap_powershell_body(body)
|
||||
|
||||
|
||||
def run_winrm_rdp_monitor_update(
|
||||
@@ -182,11 +320,11 @@ def run_winrm_rdp_monitor_update(
|
||||
password: str,
|
||||
script_path: str = "",
|
||||
) -> WinRmCmdResult:
|
||||
return run_winrm_cmd(
|
||||
return run_winrm_ps(
|
||||
target=target,
|
||||
user=user,
|
||||
password=password,
|
||||
remote_cmd=_winrm_rdp_update_remote_cmd(script_path),
|
||||
script=_rdp_update_powershell_script(script_path),
|
||||
timeout_sec=900,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user