Files
manage/backend/src/media_library_viewer_api/integrations/remote_machine.py
T
2026-07-14 20:58:46 +00:00

123 lines
4.0 KiB
Python

"""Remote machine service definition.
An ``remote_machine`` 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="remote_machine",
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 RemoteMachineConfig(ServiceConfigBase):
"""Non-secret Remote machine 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 RemoteMachineTaskOutputWidgetConfig(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="remote_machine",
name="Remote machine",
description="SSH transport for files and reusable actions.",
config_model=RemoteMachineConfig,
secret_fields=[
SecretField(key="passphrase", label="Key passphrase", helper="Optional"),
SecretField(key="password", label="SSH password", helper="Optional"),
],
widget_kinds=[
widget_kind(
kind="task_output",
name="Task output",
description="Output of a saved task run.",
model_cls=RemoteMachineTaskOutputWidgetConfig,
default_config={"task_id": ""},
refresh_interval_ms=0,
),
],
test_callable=test_connection,
)