116 lines
4.2 KiB
Python
116 lines
4.2 KiB
Python
|
|
"""
|
||
|
|
Remote PC manager with SSH, SCP, and HTTP API helpers.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import subprocess
|
||
|
|
import requests
|
||
|
|
import logging
|
||
|
|
from typing import Dict, Any, Optional, Tuple
|
||
|
|
|
||
|
|
logger = logging.getLogger("YTFactory.Remote")
|
||
|
|
|
||
|
|
class RemotePC:
|
||
|
|
def __init__(self, name: str, host: str, ssh_user: str, services: Optional[Dict[str, Any]] = None):
|
||
|
|
self.name = name
|
||
|
|
self.host = host
|
||
|
|
self.ssh_user = ssh_user
|
||
|
|
self.services = services or {}
|
||
|
|
self.ssh_base = [
|
||
|
|
"ssh",
|
||
|
|
"-o", "StrictHostKeyChecking=no",
|
||
|
|
"-o", "ConnectTimeout=5",
|
||
|
|
"-o", "BatchMode=yes",
|
||
|
|
f"{ssh_user}@{host}"
|
||
|
|
]
|
||
|
|
|
||
|
|
def check(self, port: int) -> bool:
|
||
|
|
"""Check if service is responding on port."""
|
||
|
|
for path in ("/health", "/system_stats", "/v1/models", "/"):
|
||
|
|
try:
|
||
|
|
r = requests.get(f"http://{self.host}:{port}{path}", timeout=3)
|
||
|
|
if r.status_code in (200, 204, 404): # 404 still means server is alive
|
||
|
|
return True
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return False
|
||
|
|
|
||
|
|
def post_json(self, port: int, endpoint: str, payload: Dict[str, Any], timeout: int = 180) -> Optional[Dict[str, Any]]:
|
||
|
|
"""Send JSON POST request to service."""
|
||
|
|
url = f"http://{self.host}:{port}{endpoint}"
|
||
|
|
try:
|
||
|
|
r = requests.post(url, json=payload, timeout=timeout)
|
||
|
|
r.raise_for_status()
|
||
|
|
return r.json()
|
||
|
|
except Exception as e:
|
||
|
|
logger.debug(f"POST {self.name}:{port}{endpoint} failed: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def post_binary(self, port: int, endpoint: str, payload: Dict[str, Any], timeout: int = 180) -> Optional[bytes]:
|
||
|
|
"""Send JSON POST request expecting binary audio/image output."""
|
||
|
|
url = f"http://{self.host}:{port}{endpoint}"
|
||
|
|
try:
|
||
|
|
r = requests.post(url, json=payload, timeout=timeout)
|
||
|
|
r.raise_for_status()
|
||
|
|
return r.content
|
||
|
|
except Exception as e:
|
||
|
|
logger.debug(f"POST binary {self.name}:{port}{endpoint} failed: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_json(self, port: int, endpoint: str, timeout: int = 30) -> Optional[Dict[str, Any]]:
|
||
|
|
"""Send GET request expecting JSON."""
|
||
|
|
url = f"http://{self.host}:{port}{endpoint}"
|
||
|
|
try:
|
||
|
|
r = requests.get(url, timeout=timeout)
|
||
|
|
r.raise_for_status()
|
||
|
|
return r.json()
|
||
|
|
except Exception as e:
|
||
|
|
logger.debug(f"GET {self.name}:{port}{endpoint} failed: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def run(self, cmd: str, timeout: int = 600) -> Tuple[int, str, str]:
|
||
|
|
"""Execute command over SSH."""
|
||
|
|
try:
|
||
|
|
r = subprocess.run(
|
||
|
|
self.ssh_base + [cmd],
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
timeout=timeout
|
||
|
|
)
|
||
|
|
return r.returncode, r.stdout, r.stderr
|
||
|
|
except subprocess.TimeoutExpired:
|
||
|
|
logger.error(f"SSH command timed out on {self.name}: {cmd[:80]}")
|
||
|
|
return -1, "", "TimeoutExpired"
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"SSH command failed on {self.name}: {e}")
|
||
|
|
return -1, "", str(e)
|
||
|
|
|
||
|
|
def scp_to(self, local_path: str, remote_path: str) -> bool:
|
||
|
|
"""Upload file via SCP."""
|
||
|
|
cmd = [
|
||
|
|
"scp",
|
||
|
|
"-o", "StrictHostKeyChecking=no",
|
||
|
|
"-o", "ConnectTimeout=10",
|
||
|
|
local_path,
|
||
|
|
f"{self.ssh_user}@{self.host}:{remote_path}"
|
||
|
|
]
|
||
|
|
r = subprocess.run(cmd, capture_output=True, text=True)
|
||
|
|
if r.returncode != 0:
|
||
|
|
logger.error(f"SCP to {self.name}:{remote_path} failed: {r.stderr}")
|
||
|
|
return False
|
||
|
|
return True
|
||
|
|
|
||
|
|
def scp_from(self, remote_path: str, local_path: str) -> bool:
|
||
|
|
"""Download file via SCP."""
|
||
|
|
cmd = [
|
||
|
|
"scp",
|
||
|
|
"-o", "StrictHostKeyChecking=no",
|
||
|
|
"-o", "ConnectTimeout=10",
|
||
|
|
f"{self.ssh_user}@{self.host}:{remote_path}",
|
||
|
|
local_path
|
||
|
|
]
|
||
|
|
r = subprocess.run(cmd, capture_output=True, text=True)
|
||
|
|
if r.returncode != 0:
|
||
|
|
logger.error(f"SCP from {self.name}:{remote_path} failed: {r.stderr}")
|
||
|
|
return False
|
||
|
|
return True
|