fixes and improvements
This commit is contained in:
@@ -126,6 +126,7 @@ docker compose up --build
|
|||||||
2. After the API is running, open the app, go to **Settings**, and add machine entries:
|
2. After the API is running, open the app, go to **Settings**, and add machine entries:
|
||||||
- **Local**: monitors the API host itself without SSH.
|
- **Local**: monitors the API host itself without SSH.
|
||||||
- **SSH**: monitors another machine using a host, username, and a private key pasted directly into the machine settings, with an optional passphrase.
|
- **SSH**: monitors another machine using a host, username, and a private key pasted directly into the machine settings, with an optional passphrase.
|
||||||
|
- The first successful SSH connection uses trust-on-first-use: the backend records that machine's host key into its managed `known_hosts` file automatically, then continues verifying it strictly on later connects.
|
||||||
|
|
||||||
3. Open **Monitoring** to see one section per configured machine. Each section uses its own collector state, disk path, metrics queries, and recent action history, which are populated automatically by the backend poller.
|
3. Open **Monitoring** to see one section per configured machine. Each section uses its own collector state, disk path, metrics queries, and recent action history, which are populated automatically by the backend poller.
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from typing import Any
|
|||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
|
|
||||||
|
from media_library_viewer_api.services.known_hosts import ensure_known_host
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -64,12 +66,14 @@ class RemoteSSHClient:
|
|||||||
def connect(self) -> paramiko.SSHClient:
|
def connect(self) -> paramiko.SSHClient:
|
||||||
"""Create or reuse the Paramiko connection.
|
"""Create or reuse the Paramiko connection.
|
||||||
|
|
||||||
Unknown host keys are rejected. The application can synthesize a managed
|
Unknown host keys are recorded on first contact in the managed
|
||||||
known_hosts file under its cache directory so users do not need to mount
|
known_hosts file when one is configured. After that, strict checking
|
||||||
their local SSH directory into the container.
|
remains in effect so host key changes are still rejected.
|
||||||
"""
|
"""
|
||||||
if self._client:
|
if self._client:
|
||||||
return self._client
|
return self._client
|
||||||
|
if self.known_hosts_path:
|
||||||
|
ensure_known_host(self.host, self.port, Path(self.known_hosts_path), strict=True)
|
||||||
client = paramiko.SSHClient()
|
client = paramiko.SSHClient()
|
||||||
client.load_system_host_keys()
|
client.load_system_host_keys()
|
||||||
if self.known_hosts_path and Path(self.known_hosts_path).is_file():
|
if self.known_hosts_path and Path(self.known_hosts_path).is_file():
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
|||||||
from media_library_viewer_api.clients.jellyseerr import JellyseerrClient
|
from media_library_viewer_api.clients.jellyseerr import JellyseerrClient
|
||||||
from media_library_viewer_api.clients.local import LocalCommandClient
|
from media_library_viewer_api.clients.local import LocalCommandClient
|
||||||
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||||
from media_library_viewer_api.services.known_hosts import ensure_known_host
|
|
||||||
from media_library_viewer_api.config import get_settings
|
from media_library_viewer_api.config import get_settings
|
||||||
from media_library_viewer_api.services.mail_queue import MailQueue, get_mail_queue as _get_mail_queue
|
from media_library_viewer_api.services.mail_queue import MailQueue, get_mail_queue as _get_mail_queue
|
||||||
from media_library_viewer_api.services.monitoring_poller import (
|
from media_library_viewer_api.services.monitoring_poller import (
|
||||||
@@ -153,7 +152,6 @@ def get_ssh_client(request: Request = None):
|
|||||||
return LocalCommandClient()
|
return LocalCommandClient()
|
||||||
if machine and machine.get("host") and machine.get("username"):
|
if machine and machine.get("host") and machine.get("username"):
|
||||||
known_hosts_path = get_settings().ssh_known_hosts_file
|
known_hosts_path = get_settings().ssh_known_hosts_file
|
||||||
ensure_known_host(str(machine.get("host")), int(machine.get("port") or 22), known_hosts_path)
|
|
||||||
key_data = None
|
key_data = None
|
||||||
key_passphrase = None
|
key_passphrase = None
|
||||||
ssh_key_id = str(machine.get("ssh_key_id") or "").strip()
|
ssh_key_id = str(machine.get("ssh_key_id") or "").strip()
|
||||||
@@ -190,7 +188,6 @@ def get_ssh_client(request: Request = None):
|
|||||||
)
|
)
|
||||||
if not settings.ssh_key_path:
|
if not settings.ssh_key_path:
|
||||||
raise RuntimeError("No SSH machine is configured and SSH key settings must be configured")
|
raise RuntimeError("No SSH machine is configured and SSH key settings must be configured")
|
||||||
ensure_known_host(settings.ssh_host, settings.ssh_port, settings.ssh_known_hosts_file)
|
|
||||||
return _ssh_client_for(("legacy", settings.ssh_host, settings.ssh_username, settings.ssh_port, settings.ssh_key_path, settings.ssh_password or None, None, None, str(settings.ssh_known_hosts_file)))
|
return _ssh_client_for(("legacy", settings.ssh_host, settings.ssh_username, settings.ssh_port, settings.ssh_key_path, settings.ssh_password or None, None, None, str(settings.ssh_known_hosts_file)))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from media_library_viewer_api.clients.local import LocalCommandClient
|
|||||||
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||||
from media_library_viewer_api.config import get_settings
|
from media_library_viewer_api.config import get_settings
|
||||||
from media_library_viewer_api.dependencies import get_settings_store
|
from media_library_viewer_api.dependencies import get_settings_store
|
||||||
from media_library_viewer_api.services.known_hosts import ensure_known_host
|
|
||||||
from media_library_viewer_api.services.settings_store import SettingsStore
|
from media_library_viewer_api.services.settings_store import SettingsStore
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -67,7 +66,6 @@ def _client_for_machine(store: SettingsStore, machine: dict[str, Any]):
|
|||||||
raise HTTPException(status_code=400, detail="SSH machine is missing host or username")
|
raise HTTPException(status_code=400, detail="SSH machine is missing host or username")
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
ensure_known_host(host, int(machine.get("port") or 22), settings.ssh_known_hosts_file)
|
|
||||||
|
|
||||||
private_key = str(machine.get("ssh_private_key") or "")
|
private_key = str(machine.get("ssh_private_key") or "")
|
||||||
passphrase = str(machine.get("ssh_private_key_passphrase") or "")
|
passphrase = str(machine.get("ssh_private_key_passphrase") or "")
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ def build_machine_client(machine: dict[str, Any]):
|
|||||||
key_directory = str(machine.get("key_directory") or "").strip()
|
key_directory = str(machine.get("key_directory") or "").strip()
|
||||||
key_name = str(machine.get("key_name") or "").strip()
|
key_name = str(machine.get("key_name") or "").strip()
|
||||||
key_path = f"{key_directory}/{key_name}" if key_directory and key_name else None
|
key_path = f"{key_directory}/{key_name}" if key_directory and key_name else None
|
||||||
|
settings = get_settings()
|
||||||
return RemoteSSHClient(
|
return RemoteSSHClient(
|
||||||
host=str(machine.get("host") or ""),
|
host=str(machine.get("host") or ""),
|
||||||
username=str(machine.get("username") or ""),
|
username=str(machine.get("username") or ""),
|
||||||
port=int(machine.get("port") or 22),
|
port=int(machine.get("port") or 22),
|
||||||
key_filename=key_path,
|
key_filename=key_path,
|
||||||
password=str(machine.get("password") or "") or None,
|
password=str(machine.get("password") or "") or None,
|
||||||
|
known_hosts_path=str(settings.ssh_known_hosts_file),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -275,3 +275,4 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo
|
|||||||
- 2026-05-06: Library stats, Jellyfin activity, and Monitoring overview now use shared section-container patterns so subcontainers stay consistent across the app.
|
- 2026-05-06: Library stats, Jellyfin activity, and Monitoring overview now use shared section-container patterns so subcontainers stay consistent across the app.
|
||||||
- 2026-05-07: The app versioning scheme should be hybrid: auto-detect package/build metadata when available, but allow explicit overrides for deployments that need fixed labels.
|
- 2026-05-07: The app versioning scheme should be hybrid: auto-detect package/build metadata when available, but allow explicit overrides for deployments that need fixed labels.
|
||||||
- 2026-05-07: The shell should display both frontend and backend version labels so deployed builds are easy to identify without opening a separate diagnostics screen.
|
- 2026-05-07: The shell should display both frontend and backend version labels so deployed builds are easy to identify without opening a separate diagnostics screen.
|
||||||
|
- 2026-05-07: SSH host verification should use trust-on-first-use for new machines by recording the first observed host key into the backend-managed known_hosts file, while still rejecting later key mismatches.
|
||||||
|
|||||||
Reference in New Issue
Block a user