83 lines
2.5 KiB
Python
83 lines
2.5 KiB
Python
import shlex
|
|
from urllib.parse import urlparse
|
|
|
|
import paramiko
|
|
|
|
|
|
class SSHControlError(RuntimeError):
|
|
pass
|
|
|
|
|
|
|
|
def _derive_host_from_url(rpc_url: str) -> str:
|
|
try:
|
|
parsed = urlparse(rpc_url)
|
|
return parsed.hostname or ""
|
|
except ValueError:
|
|
return ""
|
|
|
|
|
|
|
|
def run_remote_command(settings: dict, command: str) -> dict:
|
|
ssh_host = (settings.get("ssh_host") or "").strip() or _derive_host_from_url(settings.get("rpc_url", ""))
|
|
ssh_username = (settings.get("ssh_username") or "").strip()
|
|
ssh_password = settings.get("ssh_password") or ""
|
|
ssh_key_path = (settings.get("ssh_key_path") or "").strip()
|
|
ssh_port = int(settings.get("ssh_port") or 22)
|
|
|
|
if not ssh_host:
|
|
raise SSHControlError("SSH host is required to run start/restart commands")
|
|
if not ssh_username:
|
|
raise SSHControlError("SSH username is required to run start/restart commands")
|
|
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
|
|
connect_args = {
|
|
"hostname": ssh_host,
|
|
"port": ssh_port,
|
|
"username": ssh_username,
|
|
"timeout": 10,
|
|
"look_for_keys": False,
|
|
"allow_agent": False,
|
|
}
|
|
|
|
if ssh_key_path:
|
|
connect_args["key_filename"] = ssh_key_path
|
|
if ssh_password:
|
|
connect_args["passphrase"] = ssh_password
|
|
elif ssh_password:
|
|
connect_args["password"] = ssh_password
|
|
else:
|
|
connect_args["look_for_keys"] = True
|
|
connect_args["allow_agent"] = True
|
|
|
|
try:
|
|
client.connect(**connect_args)
|
|
_, stdout, stderr = client.exec_command(command, timeout=20)
|
|
exit_code = stdout.channel.recv_exit_status()
|
|
out = stdout.read().decode("utf-8", errors="replace").strip()
|
|
err = stderr.read().decode("utf-8", errors="replace").strip()
|
|
except Exception as exc: # noqa: BLE001
|
|
raise SSHControlError(f"SSH command failed: {exc}") from exc
|
|
finally:
|
|
client.close()
|
|
|
|
return {
|
|
"exit_code": exit_code,
|
|
"stdout": out,
|
|
"stderr": err,
|
|
"host": ssh_host,
|
|
}
|
|
|
|
|
|
|
|
def build_start_command(settings: dict) -> str:
|
|
config_path = (settings.get("config_path") or "").strip()
|
|
bitcoin_binary = (settings.get("bitcoin_binary") or "bitcoind").strip() or "bitcoind"
|
|
|
|
if not config_path:
|
|
raise SSHControlError("Config file path is required to start bitcoind")
|
|
|
|
return f"{shlex.quote(bitcoin_binary)} -daemon -conf={shlex.quote(config_path)}"
|