"""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, )