feat: agent updates from SAC, poll config, SSH/WinRM fallback (0.12.0)
Add agent update settings, pending self-update via poll, desired config per host, and automatic or manual SSH/WinRM fallback when agents do not report agent.update events.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""Desired agent config per host (poll delivery)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from app.models import Host
|
||||
from app.services.agent_update_settings import PRODUCT_RDP, PRODUCT_SSH
|
||||
|
||||
RDP_CONFIG_KEYS = frozenset(
|
||||
{
|
||||
"ServerDisplayName",
|
||||
"EnableRcmShadowControlMonitoring",
|
||||
"EnableWinRmInboundMonitoring",
|
||||
"EnableAdminShareMonitoring",
|
||||
"GetInventory",
|
||||
"SacCommandPollIntervalSec",
|
||||
}
|
||||
)
|
||||
SSH_CONFIG_KEYS = frozenset(
|
||||
{
|
||||
"SERVER_DISPLAY_NAME",
|
||||
"HEARTBEAT_INTERVAL",
|
||||
"DAILY_REPORT_ENABLED",
|
||||
"UseSAC",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _allowed_keys(host: Host) -> frozenset[str]:
|
||||
product = (host.product or "").strip()
|
||||
if product == PRODUCT_RDP or (host.os_family or "").strip().lower() == "windows":
|
||||
return RDP_CONFIG_KEYS
|
||||
if product == PRODUCT_SSH or (host.os_family or "").strip().lower() == "linux":
|
||||
return SSH_CONFIG_KEYS
|
||||
return frozenset()
|
||||
|
||||
|
||||
def get_host_agent_settings(host: Host) -> dict:
|
||||
raw = host.agent_config if isinstance(host.agent_config, dict) else {}
|
||||
allowed = _allowed_keys(host)
|
||||
if not allowed:
|
||||
return {}
|
||||
return {k: v for k, v in raw.items() if k in allowed}
|
||||
|
||||
|
||||
def build_agent_config_payload(host: Host) -> dict:
|
||||
settings = get_host_agent_settings(host)
|
||||
if not settings:
|
||||
return {}
|
||||
return {"settings": settings}
|
||||
|
||||
|
||||
def update_host_agent_config(db: Session, host: Host, settings: dict) -> Host:
|
||||
allowed = _allowed_keys(host)
|
||||
if not allowed:
|
||||
raise ValueError("Host product does not support desired agent config")
|
||||
|
||||
current = host.agent_config if isinstance(host.agent_config, dict) else {}
|
||||
merged = dict(current)
|
||||
for key, value in settings.items():
|
||||
if key not in allowed:
|
||||
raise ValueError(f"Unsupported config key: {key}")
|
||||
if value is None:
|
||||
merged.pop(key, None)
|
||||
else:
|
||||
merged[key] = value
|
||||
|
||||
host.agent_config = merged
|
||||
host.agent_config_revision = int(host.agent_config_revision or 0) + 1
|
||||
flag_modified(host, "agent_config")
|
||||
db.flush()
|
||||
return host
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Agent poll payload: commands, desired config, pending update."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import Host
|
||||
from app.services.agent_commands import command_to_dict, get_command_for_host_poll
|
||||
from app.services.agent_host_config import build_agent_config_payload
|
||||
from app.services.agent_update_settings import get_effective_agent_update_config
|
||||
|
||||
|
||||
def build_agent_poll_payload(db: Session, host: Host) -> dict[str, Any]:
|
||||
cfg = get_effective_agent_update_config(db)
|
||||
pending = get_command_for_host_poll(db, host)
|
||||
config_payload = build_agent_config_payload(host)
|
||||
|
||||
update_block: dict[str, Any] = {
|
||||
"requested": bool(host.pending_agent_update),
|
||||
"target_version": host.pending_update_target_version,
|
||||
"source": "sac" if cfg.sac_managed else "gpo",
|
||||
}
|
||||
|
||||
return {
|
||||
"config_revision": int(host.agent_config_revision or 0),
|
||||
"config": config_payload,
|
||||
"update": update_block,
|
||||
"commands": [command_to_dict(c, include_run_as=True) for c in pending],
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Request agent self-update, ingest lifecycle, SSH/WinRM fallback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import Host
|
||||
from app.services.agent_update_settings import (
|
||||
PRODUCT_RDP,
|
||||
PRODUCT_SSH,
|
||||
AgentUpdateConfig,
|
||||
get_effective_agent_update_config,
|
||||
)
|
||||
from app.services.agent_version import (
|
||||
is_agent_version_outdated,
|
||||
latest_agent_versions_by_product,
|
||||
parse_agent_version,
|
||||
)
|
||||
from app.services.linux_admin_settings import get_effective_linux_admin_config
|
||||
from app.services.ssh_connect import (
|
||||
HostNotLinuxError,
|
||||
HostTargetMissingError,
|
||||
is_linux_host,
|
||||
iter_ssh_targets,
|
||||
run_ssh_monitor_update,
|
||||
)
|
||||
from app.services.win_admin_settings import get_effective_win_admin_config
|
||||
from app.services.winrm_connect import (
|
||||
HostNotWindowsError,
|
||||
HostTargetMissingError as WinRmHostTargetMissingError,
|
||||
is_windows_host,
|
||||
iter_winrm_targets,
|
||||
run_winrm_on_host_targets,
|
||||
run_winrm_rdp_monitor_update,
|
||||
)
|
||||
|
||||
AGENT_UPDATE_STARTED = "agent.update.started"
|
||||
AGENT_UPDATE_SUCCESS = "agent.update.success"
|
||||
AGENT_UPDATE_FAILED = "agent.update.failed"
|
||||
AGENT_UPDATE_TYPES = frozenset(
|
||||
{AGENT_UPDATE_STARTED, AGENT_UPDATE_SUCCESS, AGENT_UPDATE_FAILED}
|
||||
)
|
||||
|
||||
|
||||
class AgentUpdateNotAllowedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def resolve_target_version(db: Session, host: Host, cfg: AgentUpdateConfig) -> str:
|
||||
recommended = cfg.recommended_for_product(host.product or "")
|
||||
if recommended:
|
||||
return recommended
|
||||
latest_map = latest_agent_versions_by_product(db)
|
||||
return latest_map.get(host.product or "", "") or (host.product_version or "")
|
||||
|
||||
|
||||
def host_version_status(
|
||||
host: Host,
|
||||
*,
|
||||
cfg: AgentUpdateConfig,
|
||||
latest_map: dict[str, str],
|
||||
) -> str:
|
||||
current = host.product_version
|
||||
if not current or parse_agent_version(current) is None:
|
||||
return "unknown"
|
||||
recommended = cfg.recommended_for_product(host.product or "")
|
||||
reference = recommended or latest_map.get(host.product or "")
|
||||
if not reference:
|
||||
return "unknown"
|
||||
if is_agent_version_outdated(current, reference):
|
||||
return "outdated"
|
||||
return "ok"
|
||||
|
||||
|
||||
def request_agent_update(db: Session, host: Host) -> Host:
|
||||
cfg = get_effective_agent_update_config(db)
|
||||
if not cfg.sac_managed:
|
||||
raise AgentUpdateNotAllowedError(
|
||||
"Agent updates from SAC are disabled (mode=gpo). Use GPO/manual update or switch to mode=sac."
|
||||
)
|
||||
if not host.agent_instance_id:
|
||||
raise AgentUpdateNotAllowedError("Host has no agent_instance_id (agent never connected via SAC)")
|
||||
|
||||
target = resolve_target_version(db, host, cfg)
|
||||
now = datetime.now(timezone.utc)
|
||||
host.pending_agent_update = True
|
||||
host.pending_update_requested_at = now
|
||||
host.pending_update_target_version = target or None
|
||||
host.agent_update_state = "requested"
|
||||
host.agent_update_last_error = None
|
||||
db.flush()
|
||||
return host
|
||||
|
||||
|
||||
def _clear_pending(host: Host) -> None:
|
||||
host.pending_agent_update = False
|
||||
host.pending_update_requested_at = None
|
||||
host.pending_update_target_version = None
|
||||
|
||||
|
||||
def process_agent_update_ingest(db: Session, host: Host, event_type: str, details: dict | None) -> None:
|
||||
if event_type not in AGENT_UPDATE_TYPES:
|
||||
return
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
host.agent_update_last_at = now
|
||||
details = details if isinstance(details, dict) else {}
|
||||
|
||||
if event_type == AGENT_UPDATE_STARTED:
|
||||
host.agent_update_state = "started"
|
||||
return
|
||||
|
||||
if event_type == AGENT_UPDATE_SUCCESS:
|
||||
host.agent_update_state = "success"
|
||||
host.agent_update_last_error = None
|
||||
_clear_pending(host)
|
||||
version = details.get("product_version") or details.get("version")
|
||||
if isinstance(version, str) and version.strip():
|
||||
host.product_version = version.strip()
|
||||
return
|
||||
|
||||
if event_type == AGENT_UPDATE_FAILED:
|
||||
host.agent_update_state = "failed"
|
||||
err = details.get("error") or details.get("message")
|
||||
host.agent_update_last_error = str(err)[:2000] if err else "agent.update.failed"
|
||||
_clear_pending(host)
|
||||
|
||||
|
||||
def run_linux_agent_update_fallback(host: Host, cfg_linux) -> tuple[bool, str, str | None]:
|
||||
if not is_linux_host(host):
|
||||
return False, "Host is not Linux", None
|
||||
if not cfg_linux.configured:
|
||||
return False, "Linux SSH admin is not configured", None
|
||||
|
||||
last_message = "SSH fallback failed"
|
||||
version: str | None = None
|
||||
try:
|
||||
targets = iter_ssh_targets(host)
|
||||
except (HostNotLinuxError, HostTargetMissingError) as exc:
|
||||
return False, str(exc), None
|
||||
|
||||
for target in targets:
|
||||
result = run_ssh_monitor_update(target=target, user=cfg_linux.user, password=cfg_linux.password)
|
||||
last_message = result.message
|
||||
if result.ok:
|
||||
version = result.agent_version
|
||||
return True, result.message, version
|
||||
return False, last_message, version
|
||||
|
||||
|
||||
def run_windows_agent_update_fallback(host: Host, cfg_win, script_path: str) -> tuple[bool, str, str | None]:
|
||||
if not is_windows_host(host):
|
||||
return False, "Host is not Windows", None
|
||||
if not cfg_win.configured:
|
||||
return False, "Windows domain admin is not configured", None
|
||||
|
||||
try:
|
||||
targets = iter_winrm_targets(host)
|
||||
except (HostNotWindowsError, WinRmHostTargetMissingError) as exc:
|
||||
return False, str(exc), None
|
||||
|
||||
result, attempts = run_winrm_on_host_targets(
|
||||
host,
|
||||
user=cfg_win.user,
|
||||
password=cfg_win.password,
|
||||
action=lambda target: run_winrm_rdp_monitor_update(
|
||||
target=target,
|
||||
user=cfg_win.user,
|
||||
password=cfg_win.password,
|
||||
script_path=script_path,
|
||||
),
|
||||
)
|
||||
if result is None:
|
||||
return False, "WinRM fallback failed", None
|
||||
message = result.message
|
||||
if attempts:
|
||||
message = f"{message} (пробовали: {', '.join(attempts)})"
|
||||
version = None
|
||||
if result.ok and result.stdout:
|
||||
from app.services.ssh_connect import parse_ssh_monitor_version_text
|
||||
|
||||
version = parse_ssh_monitor_version_text(result.stdout)
|
||||
return result.ok, message, version
|
||||
|
||||
|
||||
def execute_agent_update_fallback(db: Session, host: Host) -> tuple[bool, str, str | None]:
|
||||
cfg = get_effective_agent_update_config(db)
|
||||
product = (host.product or "").strip()
|
||||
|
||||
if product == PRODUCT_SSH or is_linux_host(host):
|
||||
linux_cfg = get_effective_linux_admin_config(db)
|
||||
ok, message, version = run_linux_agent_update_fallback(host, linux_cfg)
|
||||
elif product == PRODUCT_RDP or is_windows_host(host):
|
||||
win_cfg = get_effective_win_admin_config(db)
|
||||
ok, message, version = run_windows_agent_update_fallback(host, win_cfg, cfg.win_agent_update_script)
|
||||
else:
|
||||
return False, f"Unsupported product for fallback update: {product}", None
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
host.agent_update_last_at = now
|
||||
if ok:
|
||||
host.agent_update_state = "success"
|
||||
host.agent_update_last_error = None
|
||||
_clear_pending(host)
|
||||
if version:
|
||||
host.product_version = version
|
||||
if is_linux_host(host):
|
||||
host.ssh_admin_ok = True
|
||||
host.ssh_admin_checked_at = now
|
||||
else:
|
||||
host.agent_update_state = "failed"
|
||||
host.agent_update_last_error = message[:2000]
|
||||
|
||||
db.flush()
|
||||
return ok, message, version
|
||||
|
||||
|
||||
def process_agent_update_fallbacks(db: Session) -> list[dict]:
|
||||
"""Find stale pending updates and run SSH/WinRM fallback when enabled."""
|
||||
cfg = get_effective_agent_update_config(db)
|
||||
if not cfg.sac_managed or not cfg.fallback_enabled:
|
||||
return []
|
||||
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=cfg.fallback_after_minutes)
|
||||
rows = db.scalars(
|
||||
select(Host).where(
|
||||
Host.pending_agent_update.is_(True),
|
||||
Host.pending_update_requested_at.isnot(None),
|
||||
Host.pending_update_requested_at <= cutoff,
|
||||
Host.agent_update_state.in_(("requested", "started")),
|
||||
)
|
||||
).all()
|
||||
|
||||
results: list[dict] = []
|
||||
for host in rows:
|
||||
ok, message, version = execute_agent_update_fallback(db, host)
|
||||
results.append(
|
||||
{
|
||||
"host_id": host.id,
|
||||
"hostname": host.hostname,
|
||||
"ok": ok,
|
||||
"message": message,
|
||||
"product_version": version,
|
||||
}
|
||||
)
|
||||
return results
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Global agent update policy (DB overrides env)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.ui_settings import UI_SETTINGS_ROW_ID, UiSettings
|
||||
|
||||
PRODUCT_RDP = "rdp-login-monitor"
|
||||
PRODUCT_SSH = "ssh-monitor"
|
||||
AGENT_UPDATE_MODES = frozenset({"gpo", "sac"})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentUpdateConfig:
|
||||
mode: str
|
||||
fallback_enabled: bool
|
||||
fallback_after_minutes: int
|
||||
recommended_rdp_version: str
|
||||
recommended_ssh_version: str
|
||||
min_rdp_version: str
|
||||
min_ssh_version: str
|
||||
win_agent_update_script: str
|
||||
source: str # env | db
|
||||
|
||||
@property
|
||||
def sac_managed(self) -> bool:
|
||||
return self.mode == "sac"
|
||||
|
||||
def recommended_for_product(self, product: str) -> str:
|
||||
if product == PRODUCT_RDP:
|
||||
return self.recommended_rdp_version
|
||||
if product == PRODUCT_SSH:
|
||||
return self.recommended_ssh_version
|
||||
return ""
|
||||
|
||||
def min_for_product(self, product: str) -> str:
|
||||
if product == PRODUCT_RDP:
|
||||
return self.min_rdp_version
|
||||
if product == PRODUCT_SSH:
|
||||
return self.min_ssh_version
|
||||
return ""
|
||||
|
||||
|
||||
def _agent_update_from_env() -> AgentUpdateConfig:
|
||||
settings = get_settings()
|
||||
mode = (settings.sac_agent_update_mode or "gpo").strip().lower()
|
||||
if mode not in AGENT_UPDATE_MODES:
|
||||
mode = "gpo"
|
||||
return AgentUpdateConfig(
|
||||
mode=mode,
|
||||
fallback_enabled=settings.sac_agent_update_fallback_enabled,
|
||||
fallback_after_minutes=max(1, int(settings.sac_agent_update_fallback_minutes)),
|
||||
recommended_rdp_version=(settings.sac_agent_recommended_rdp_version or "").strip(),
|
||||
recommended_ssh_version=(settings.sac_agent_recommended_ssh_version or "").strip(),
|
||||
min_rdp_version=(settings.sac_agent_min_rdp_version or "").strip(),
|
||||
min_ssh_version=(settings.sac_agent_min_ssh_version or "").strip(),
|
||||
win_agent_update_script=(settings.sac_win_agent_update_script or "").strip(),
|
||||
source="env",
|
||||
)
|
||||
|
||||
|
||||
def _row_has_agent_update_values(row: UiSettings) -> bool:
|
||||
return any(
|
||||
[
|
||||
(row.agent_update_mode or "").strip() not in ("", "gpo"),
|
||||
row.agent_update_fallback_enabled is not None,
|
||||
row.agent_update_fallback_minutes not in (None, 15),
|
||||
bool((row.agent_recommended_rdp_version or "").strip()),
|
||||
bool((row.agent_recommended_ssh_version or "").strip()),
|
||||
bool((row.agent_min_rdp_version or "").strip()),
|
||||
bool((row.agent_min_ssh_version or "").strip()),
|
||||
bool((row.win_agent_update_script or "").strip()),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def get_effective_agent_update_config(db: Session | None = None) -> AgentUpdateConfig:
|
||||
if db is None:
|
||||
from app.database import SessionLocal
|
||||
|
||||
session = SessionLocal()
|
||||
try:
|
||||
return get_effective_agent_update_config(session)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
row = db.get(UiSettings, UI_SETTINGS_ROW_ID)
|
||||
env_cfg = _agent_update_from_env()
|
||||
if row is None:
|
||||
return env_cfg
|
||||
|
||||
mode = (row.agent_update_mode or env_cfg.mode).strip().lower()
|
||||
if mode not in AGENT_UPDATE_MODES:
|
||||
mode = env_cfg.mode
|
||||
|
||||
return AgentUpdateConfig(
|
||||
mode=mode,
|
||||
fallback_enabled=(
|
||||
row.agent_update_fallback_enabled
|
||||
if row.agent_update_fallback_enabled is not None
|
||||
else env_cfg.fallback_enabled
|
||||
),
|
||||
fallback_after_minutes=max(
|
||||
1,
|
||||
int(row.agent_update_fallback_minutes or env_cfg.fallback_after_minutes),
|
||||
),
|
||||
recommended_rdp_version=(row.agent_recommended_rdp_version or "").strip()
|
||||
or env_cfg.recommended_rdp_version,
|
||||
recommended_ssh_version=(row.agent_recommended_ssh_version or "").strip()
|
||||
or env_cfg.recommended_ssh_version,
|
||||
min_rdp_version=(row.agent_min_rdp_version or "").strip() or env_cfg.min_rdp_version,
|
||||
min_ssh_version=(row.agent_min_ssh_version or "").strip() or env_cfg.min_ssh_version,
|
||||
win_agent_update_script=(row.win_agent_update_script or "").strip()
|
||||
or env_cfg.win_agent_update_script,
|
||||
source="db" if _row_has_agent_update_values(row) else env_cfg.source,
|
||||
)
|
||||
|
||||
|
||||
def upsert_agent_update_settings(
|
||||
db: Session,
|
||||
*,
|
||||
mode: str | None = None,
|
||||
fallback_enabled: bool | None = None,
|
||||
fallback_after_minutes: int | None = None,
|
||||
recommended_rdp_version: str | None = None,
|
||||
recommended_ssh_version: str | None = None,
|
||||
min_rdp_version: str | None = None,
|
||||
min_ssh_version: str | None = None,
|
||||
win_agent_update_script: str | None = None,
|
||||
) -> AgentUpdateConfig:
|
||||
row = db.get(UiSettings, UI_SETTINGS_ROW_ID)
|
||||
if row is None:
|
||||
row = UiSettings(id=UI_SETTINGS_ROW_ID, show_sidebar_system_stats=True)
|
||||
db.add(row)
|
||||
|
||||
if mode is not None:
|
||||
normalized = mode.strip().lower()
|
||||
if normalized not in AGENT_UPDATE_MODES:
|
||||
raise ValueError(f"mode must be one of: {sorted(AGENT_UPDATE_MODES)}")
|
||||
row.agent_update_mode = normalized
|
||||
if fallback_enabled is not None:
|
||||
row.agent_update_fallback_enabled = fallback_enabled
|
||||
if fallback_after_minutes is not None:
|
||||
row.agent_update_fallback_minutes = max(1, int(fallback_after_minutes))
|
||||
if recommended_rdp_version is not None:
|
||||
row.agent_recommended_rdp_version = recommended_rdp_version.strip() or None
|
||||
if recommended_ssh_version is not None:
|
||||
row.agent_recommended_ssh_version = recommended_ssh_version.strip() or None
|
||||
if min_rdp_version is not None:
|
||||
row.agent_min_rdp_version = min_rdp_version.strip() or None
|
||||
if min_ssh_version is not None:
|
||||
row.agent_min_ssh_version = min_ssh_version.strip() or None
|
||||
if win_agent_update_script is not None:
|
||||
row.win_agent_update_script = win_agent_update_script.strip() or None
|
||||
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return get_effective_agent_update_config(db)
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import Event, Host
|
||||
from app.services.agent_update import process_agent_update_ingest
|
||||
from app.services.daily_report_format import normalize_daily_report_details
|
||||
from app.services.event_severity_overrides import apply_severity_override
|
||||
from app.services.host_inventory import INVENTORY_EVENT_TYPE, process_inventory_ingest
|
||||
@@ -123,4 +124,6 @@ def ingest_event(db: Session, payload: dict) -> tuple[Event, bool]:
|
||||
if raced is not None:
|
||||
return raced, False
|
||||
raise
|
||||
|
||||
process_agent_update_ingest(db, host, payload.get("type", ""), details)
|
||||
return event, True
|
||||
|
||||
@@ -140,6 +140,36 @@ def run_winrm_logoff(*, target: str, user: str, password: str, session_id: int)
|
||||
)
|
||||
|
||||
|
||||
def _winrm_rdp_update_remote_cmd(script_path: str) -> str:
|
||||
script = script_path.strip()
|
||||
if script:
|
||||
return f'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{script}"'
|
||||
return (
|
||||
"powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
|
||||
'"& { $candidates = @('
|
||||
"(Join-Path $env:ProgramData 'LoginMonitor' 'Deploy-LoginMonitor.ps1'),"
|
||||
"(Join-Path $env:USERPROFILE 'Deploy-LoginMonitor.ps1')"
|
||||
'); $p = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1; '
|
||||
"if (-not $p) { throw 'Deploy-LoginMonitor.ps1 not found' }; & $p }'"
|
||||
)
|
||||
|
||||
|
||||
def run_winrm_rdp_monitor_update(
|
||||
*,
|
||||
target: str,
|
||||
user: str,
|
||||
password: str,
|
||||
script_path: str = "",
|
||||
) -> WinRmCmdResult:
|
||||
return run_winrm_cmd(
|
||||
target=target,
|
||||
user=user,
|
||||
password=password,
|
||||
remote_cmd=_winrm_rdp_update_remote_cmd(script_path),
|
||||
timeout_sec=900,
|
||||
)
|
||||
|
||||
|
||||
def run_winrm_on_host_targets(
|
||||
host: Host,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user