cf7c29639c
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.
123 lines
4.6 KiB
Python
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
|