Initial commit

This commit is contained in:
2026-02-15 16:28:38 +00:00
commit 0e793197bf
24 changed files with 3268 additions and 0 deletions

82
app/ssh_control.py Normal file
View File

@@ -0,0 +1,82 @@
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)}"