Files
insight-updater/backend/app/services/winrm.py
T
B0rbor4d cf7c29639c Initial scaffold: FastAPI backend + Vue 3 frontend + Docker setup
Backend: config/db/security/logging core, SQLAlchemy models (Server,
Credential, UpdateJob, UpdateLog, AuditLog, User), services (winrm, ssh,
cau, audit, job_runner), REST API (auth, servers, updates, audit),
Socket.io WebSocket layer.
Frontend: Vue 3 + TS + Pinia + Tailwind, Views (Dashboard, Servers,
Updates, Audit, Login), axios + socket.io-client, nginx prod config.
2026-07-31 23:45:31 +00:00

123 lines
4.6 KiB
Python

"""WinRM service: connect to Windows hosts, run PowerShell, stream output.
Uses python-winrm (pywinrm). All blocking calls run in a thread pool
so the async event loop is never blocked.
"""
import asyncio
from collections.abc import AsyncIterator
from dataclasses import dataclass
from app.core.config import get_settings
from app.core.exceptions import WinRMError
from app.core.logging import get_logger
settings = get_settings()
logger = get_logger(__name__)
@dataclass
class WinRMCredentials:
username: str
password: str
class WinRMService:
"""Wraps pywinrm for async usage."""
def __init__(
self,
hostname: str,
port: int = 5985,
credentials: WinRMCredentials | None = None,
transport: str | None = None,
) -> None:
self.hostname = hostname
self.port = port
self.credentials = credentials
self.transport = transport or settings.winrm_transport
def _build_session(self): # type: ignore[no-untyped-def]
import winrm # pywinrm
scheme = "https" if self.port == 5986 else "http"
endpoint = f"{scheme}://{self.hostname}:{self.port}/wsman"
kwargs: dict = {
"transport": self.transport,
"server_cert_validation": settings.winrm_cert_validation,
"operation_timeout_sec": settings.winrm_operation_timeout,
"read_timeout_sec": settings.winrm_read_timeout,
}
if self.credentials:
kwargs["username"] = self.credentials.username
kwargs["password"] = self.credentials.password
return winrm.Session(endpoint, **kwargs)
async def test_connection(self) -> tuple[bool, str]:
"""Run a trivial command to verify connectivity."""
def _run() -> tuple[bool, str]:
try:
session = self._build_session()
result = session.run_ps("$env:COMPUTERNAME")
if result.status_code == 0:
name = result.std_out.decode(errors="replace").strip()
return True, f"Verbunden mit {name}"
return False, result.std_err.decode(errors="replace").strip()
except Exception as exc: # noqa: BLE001 - surface any transport error
return False, str(exc)
return await asyncio.to_thread(_run)
async def run_powershell(self, script: str) -> str:
"""Run a PowerShell script and return stdout. Raises WinRMError on failure."""
def _run() -> str:
session = self._build_session()
result = session.run_ps(script)
if result.status_code != 0:
err = result.std_err.decode(errors="replace").strip()
raise WinRMError(f"PowerShell exit {result.status_code}: {err}")
return result.std_out.decode(errors="replace")
return await asyncio.to_thread(_run)
async def stream_powershell(self, script: str) -> AsyncIterator[str]:
"""Yield output lines as they arrive (scaffold: runs to completion, then yields).
TODO: switch to winrm Protocol with shell polling for true streaming.
"""
output = await self.run_powershell(script)
for line in output.splitlines():
yield line
await asyncio.sleep(0) # keep the loop responsive
async def get_pending_updates(self) -> list[str]:
"""Query Windows Update for pending updates (titles only)."""
script = """
$session = New-Object -ComObject Microsoft.Update.Session
$searcher = $session.CreateUpdateSearcher()
$result = $searcher.Search("IsInstalled=0")
$result.Updates | ForEach-Object { $_.Title }
"""
output = await self.run_powershell(script)
return [line.strip() for line in output.splitlines() if line.strip()]
async def install_updates(self) -> AsyncIterator[str]:
"""Install all pending Windows Updates, yielding progress lines."""
script = """
$session = New-Object -ComObject Microsoft.Update.Session
$searcher = $session.CreateUpdateSearcher()
$result = $searcher.Search("IsInstalled=0")
Write-Output "Gefundene Updates: $($result.Updates.Count)"
$toInstall = New-Object -ComObject Microsoft.Update.UpdateColl
$result.Updates | ForEach-Object { $toInstall.Add($_) | Out-Null }
$installer = $session.CreateUpdateInstaller()
$installer.Updates = $toInstall
$installResult = $installer.Install()
Write-Output "ResultCode: $($installResult.ResultCode)"
Write-Output "RebootRequired: $($installResult.RebootRequired)"
"""
async for line in self.stream_powershell(script):
yield line