feat: RDG qwinsta/logoff via WinRM on client workstation (0.11.6)
SAC resolves internal_ip to a registered Windows host and runs qwinsta/logoff synchronously over WinRM instead of queueing commands to the gateway agent.
This commit is contained in:
@@ -28,6 +28,137 @@ class WinRmTestResult:
|
||||
hostname: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WinRmCmdResult:
|
||||
ok: bool
|
||||
message: str
|
||||
target: str
|
||||
stdout: str = ""
|
||||
stderr: str = ""
|
||||
exit_code: int | None = None
|
||||
|
||||
|
||||
def _winrm_session(
|
||||
target: str,
|
||||
user: str,
|
||||
password: str,
|
||||
*,
|
||||
timeout_sec: int = 15,
|
||||
):
|
||||
from app.services.win_admin_settings import normalize_win_admin_user
|
||||
|
||||
user = normalize_win_admin_user(user.strip())
|
||||
target = target.strip()
|
||||
if not target or not user or not password:
|
||||
raise WinAdminNotConfiguredError("Windows admin credentials or target missing")
|
||||
|
||||
try:
|
||||
import winrm
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("pywinrm is not installed on SAC server") from exc
|
||||
|
||||
operation_timeout_sec = max(5, timeout_sec)
|
||||
read_timeout_sec = operation_timeout_sec + 15
|
||||
endpoint = f"http://{target}:5985/wsman"
|
||||
session = winrm.Session(
|
||||
endpoint,
|
||||
auth=(user, password),
|
||||
transport="ntlm",
|
||||
read_timeout_sec=read_timeout_sec,
|
||||
operation_timeout_sec=operation_timeout_sec,
|
||||
)
|
||||
return session, winrm
|
||||
|
||||
|
||||
def run_winrm_cmd(
|
||||
*,
|
||||
target: str,
|
||||
user: str,
|
||||
password: str,
|
||||
remote_cmd: str,
|
||||
timeout_sec: int = 30,
|
||||
) -> WinRmCmdResult:
|
||||
target = target.strip()
|
||||
try:
|
||||
session, winrm = _winrm_session(target, user, password, timeout_sec=timeout_sec)
|
||||
result = session.run_cmd(remote_cmd)
|
||||
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)
|
||||
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,
|
||||
target=target,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
exit_code=exit_code,
|
||||
)
|
||||
|
||||
|
||||
def run_winrm_qwinsta(*, target: str, user: str, password: str) -> WinRmCmdResult:
|
||||
return run_winrm_cmd(target=target, user=user, password=password, remote_cmd="qwinsta", timeout_sec=45)
|
||||
|
||||
|
||||
def run_winrm_logoff(*, target: str, user: str, password: str, session_id: int) -> WinRmCmdResult:
|
||||
if session_id < 0:
|
||||
return WinRmCmdResult(ok=False, message="Invalid session_id", target=target)
|
||||
return run_winrm_cmd(
|
||||
target=target,
|
||||
user=user,
|
||||
password=password,
|
||||
remote_cmd=f"logoff {session_id} /v",
|
||||
timeout_sec=45,
|
||||
)
|
||||
|
||||
|
||||
def run_winrm_on_host_targets(
|
||||
host: Host,
|
||||
*,
|
||||
user: str,
|
||||
password: str,
|
||||
action,
|
||||
) -> tuple[WinRmCmdResult | None, list[str]]:
|
||||
"""Try WinRM targets in hostname-first order; action(target) -> WinRmCmdResult."""
|
||||
attempts: list[str] = []
|
||||
last: WinRmCmdResult | None = None
|
||||
for target in iter_winrm_targets(host):
|
||||
result = action(target=target)
|
||||
last = result
|
||||
attempts.append(f"{target}={'OK' if result.ok else 'fail'}")
|
||||
if result.ok:
|
||||
return result, attempts
|
||||
return last, attempts
|
||||
|
||||
|
||||
def resolve_windows_host_target(host: Host) -> str:
|
||||
targets = iter_winrm_targets(host)
|
||||
if not targets:
|
||||
@@ -100,70 +231,24 @@ def test_winrm_connection(
|
||||
password: str,
|
||||
timeout_sec: int = 15,
|
||||
) -> WinRmTestResult:
|
||||
target = target.strip()
|
||||
user = user.strip()
|
||||
if not target or not user or not password:
|
||||
raise WinAdminNotConfiguredError("Windows admin credentials or target missing")
|
||||
|
||||
from app.services.win_admin_settings import normalize_win_admin_user
|
||||
|
||||
user = normalize_win_admin_user(user)
|
||||
|
||||
try:
|
||||
import winrm
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("pywinrm is not installed on SAC server") from exc
|
||||
|
||||
operation_timeout_sec = max(5, timeout_sec)
|
||||
read_timeout_sec = operation_timeout_sec + 15
|
||||
endpoint = f"http://{target}:5985/wsman"
|
||||
try:
|
||||
session = winrm.Session(
|
||||
endpoint,
|
||||
auth=(user, password),
|
||||
transport="ntlm",
|
||||
read_timeout_sec=read_timeout_sec,
|
||||
operation_timeout_sec=operation_timeout_sec,
|
||||
)
|
||||
result = session.run_cmd("hostname")
|
||||
except winrm.exceptions.WinRMTransportError as exc:
|
||||
detail = str(exc)
|
||||
hint = ""
|
||||
if "credentials were rejected" in detail.lower():
|
||||
hint = (
|
||||
" Проверьте логин (B26\\user), пароль и права admin на ПК; "
|
||||
"подключайтесь по имени хоста (как Enter-PSSession -ComputerName), не по IP."
|
||||
)
|
||||
return WinRmTestResult(
|
||||
ok=False,
|
||||
message=f"WinRM transport error ({target}): {detail}{hint}",
|
||||
target=target,
|
||||
)
|
||||
except (socket.timeout, TimeoutError):
|
||||
return WinRmTestResult(
|
||||
ok=False,
|
||||
message=f"WinRM timeout ({timeout_sec}s) to {target}:5985",
|
||||
target=target,
|
||||
)
|
||||
except Exception as exc:
|
||||
return WinRmTestResult(
|
||||
ok=False,
|
||||
message=f"WinRM error: {exc}",
|
||||
target=target,
|
||||
)
|
||||
|
||||
stdout = (result.std_out or b"").decode("utf-8", errors="replace").strip()
|
||||
stderr = (result.std_err or b"").decode("utf-8", errors="replace").strip()
|
||||
if result.status_code == 0 and stdout:
|
||||
result = run_winrm_cmd(
|
||||
target=target,
|
||||
user=user,
|
||||
password=password,
|
||||
remote_cmd="hostname",
|
||||
timeout_sec=timeout_sec,
|
||||
)
|
||||
if result.ok and result.stdout.strip():
|
||||
host = result.stdout.strip().splitlines()[0]
|
||||
return WinRmTestResult(
|
||||
ok=True,
|
||||
message=f"WinRM OK, hostname={stdout}",
|
||||
target=target,
|
||||
hostname=stdout,
|
||||
message=f"WinRM OK, hostname={host}",
|
||||
target=result.target,
|
||||
hostname=host,
|
||||
)
|
||||
detail = stderr or stdout or f"exit code {result.status_code}"
|
||||
return WinRmTestResult(
|
||||
ok=False,
|
||||
message=f"WinRM command failed: {detail}",
|
||||
target=target,
|
||||
ok=result.ok,
|
||||
message=result.message,
|
||||
target=result.target,
|
||||
hostname=None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user