3391fbc85d
TestResult dataclass + translate_connection_error shared helper in base.py. test_callable field on ServiceDefinition (default None). 7 per-type test_connection routines (qbittorrent, prometheus via Grafana gateway, alertmanager, jellyfin, authentik, ssh_tasks via build_ssh_client, nextcloud). POST /api/services/test endpoint: validation-first (422 on malformed config), dispatch, no-persistence, no-secret-logs. backups has test_callable=None. qBit 'Fails.' → specific auth message (resolves #3 at API layer). Backend: 362 pytest pass (+31 new), ruff clean. Frontend: build green.
122 lines
3.9 KiB
Python
122 lines
3.9 KiB
Python
"""SSH task runner service definition.
|
|
|
|
An ``ssh_tasks`` instance is an SSH endpoint that can run reusable saved tasks.
|
|
Tasks themselves stay in the global saved-task registry; the instance only owns
|
|
transport (host/port/user/key). Every run is recorded in ``service_task_runs``
|
|
and shown as history on the instance's service page.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from media_library_viewer_api.integrations.base import (
|
|
SecretField,
|
|
ServiceConfigBase,
|
|
ServiceDefinition,
|
|
TestResult,
|
|
WidgetConfigBase,
|
|
translate_connection_error,
|
|
widget_kind,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from media_library_viewer_api.services.settings_store import SettingsStore
|
|
|
|
|
|
def test_connection(
|
|
config: dict[str, Any],
|
|
secrets: dict[str, str],
|
|
store: SettingsStore,
|
|
) -> TestResult:
|
|
"""Build an SSH client via build_ssh_client and attempt .connect().
|
|
|
|
Reuses the same error-translation patterns as test_machine_ssh (banner,
|
|
auth failed). Known-host recording is preserved.
|
|
"""
|
|
from media_library_viewer_api.services.task_runner import build_ssh_client
|
|
from media_library_viewer_api.widgets.sources import ServiceRecord
|
|
|
|
host = str(config.get("host") or "").strip()
|
|
port = int(config.get("port") or 22)
|
|
try:
|
|
service = ServiceRecord(
|
|
id="",
|
|
service_type="ssh_tasks",
|
|
name="test",
|
|
config=config,
|
|
secrets=secrets,
|
|
enabled=True,
|
|
)
|
|
client = build_ssh_client(store, service)
|
|
try:
|
|
client.connect()
|
|
except Exception as exc:
|
|
lowered = str(exc).lower()
|
|
if "protocol banner" in lowered:
|
|
return TestResult(
|
|
ok=False,
|
|
detail=f"SSH banner not received from {host}:{port}; confirm the SSH service is running.",
|
|
)
|
|
if "no authentication methods available" in lowered or "authentication failed" in lowered:
|
|
return TestResult(
|
|
ok=False,
|
|
detail=f"SSH authentication failed for {host}:{port}; check the SSH key, passphrase, or username.",
|
|
)
|
|
return translate_connection_error(exc, context=f"SSH {host}:{port}")
|
|
finally:
|
|
client.close()
|
|
return TestResult(
|
|
ok=True,
|
|
detail=f"SSH connection succeeded for {host}:{port}.",
|
|
evidence=f"Connected to {host}:{port}",
|
|
)
|
|
except ValueError as exc:
|
|
return TestResult(ok=False, detail=str(exc))
|
|
except Exception as exc:
|
|
return translate_connection_error(exc, context=f"SSH {host}:{port}")
|
|
|
|
|
|
class SshTasksConfig(ServiceConfigBase):
|
|
"""Non-secret SSH task runner config.
|
|
|
|
The SSH key itself lives in the saved SSH-key registry and is referenced by
|
|
``ssh_key_id``. An optional ``passphrase`` is stored as a secret.
|
|
"""
|
|
|
|
host: str
|
|
port: int = 22
|
|
username: str = ""
|
|
ssh_key_id: str = ""
|
|
timeout_seconds: int = 30
|
|
|
|
|
|
class SshTaskOutputWidgetConfig(WidgetConfigBase):
|
|
"""Output of a saved task run on this instance."""
|
|
|
|
task_id: str
|
|
# service_id is implicit (the widget's service); allow overriding per-widget.
|
|
service_id: str | None = None
|
|
|
|
|
|
DEFINITION = ServiceDefinition(
|
|
service_type="ssh_tasks",
|
|
name="SSH task runner",
|
|
description="Run reusable saved tasks over SSH and keep run history.",
|
|
config_model=SshTasksConfig,
|
|
secret_fields=[
|
|
SecretField(key="passphrase", label="Key passphrase", helper="Optional"),
|
|
],
|
|
widget_kinds=[
|
|
widget_kind(
|
|
kind="task_output",
|
|
name="Task output",
|
|
description="Output of a saved task run.",
|
|
model_cls=SshTaskOutputWidgetConfig,
|
|
default_config={"task_id": ""},
|
|
refresh_interval_ms=0,
|
|
),
|
|
],
|
|
test_callable=test_connection,
|
|
)
|