aboutsummaryrefslogtreecommitdiffstats
path: root/sb_builder/remote.py
diff options
context:
space:
mode:
Diffstat (limited to 'sb_builder/remote.py')
-rw-r--r--sb_builder/remote.py84
1 files changed, 84 insertions, 0 deletions
diff --git a/sb_builder/remote.py b/sb_builder/remote.py
new file mode 100644
index 0000000..5340f6c
--- /dev/null
+++ b/sb_builder/remote.py
@@ -0,0 +1,84 @@
+# sb-builder - ssh and virsh transport
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""The only code that talks to the VM: ssh to sbb-remote, and local virsh."""
+
+import json
+import os
+import shlex
+
+from PyQt6.QtCore import QObject, QProcess, QStandardPaths
+
+DOWN = "ssh: connection failed"
+MISSING = "sbb-remote is not installed on the VM"
+
+
+def parse(rc, out, err):
+ """Map one sbb-remote call to (data, error)."""
+ if rc == 255:
+ return None, DOWN
+ if rc == 127:
+ return None, MISSING
+ try:
+ data = json.loads(out)
+ except ValueError:
+ return None, err.decode(errors="replace").strip() or f"exit {rc}"
+ if rc != 0 or "error" in data:
+ return None, data.get("error", f"exit {rc}")
+ return data, None
+
+
+class Remote(QObject):
+ def __init__(self, cfg, parent=None):
+ super().__init__(parent)
+ self.cfg = cfg # shared dict, updated in place on config reload
+ run = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.RuntimeLocation)
+ self.control = os.path.join(run or "/tmp", "sbb-%C")
+
+ def ssh_args(self, *args):
+ r = self.cfg["remote"]
+ # ssh joins its args into a remote shell command: quote every one
+ return ["-o", "ControlMaster=auto", "-o", f"ControlPath={self.control}",
+ "-o", "ControlPersist=10m", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
+ *r["ssh_options"], r["target"], "sbb-remote", *map(shlex.quote, args)]
+
+ def call(self, cb, *args):
+ """Run sbb-remote <args>, then cb(data, error)."""
+ self._run("ssh", self.ssh_args(*args), lambda rc, out, err: cb(*parse(rc, out, err)))
+
+ def virsh(self, cb, *args):
+ """Run local virsh against the configured libvirt URI, then cb(rc, text)."""
+ argv = ["-c", self.cfg["vm"]["libvirt_uri"], *args]
+ self._run("virsh", argv,
+ lambda rc, out, err: cb(rc, (out or err).decode(errors="replace").strip()))
+
+ def follow(self, jid, offset):
+ """Start job-follow; the caller wires readyReadStandardOutput and finished."""
+ p = QProcess(self)
+ p.start("ssh", self.ssh_args("job-follow", jid, str(offset)))
+ return p
+
+ def _run(self, prog, argv, cb):
+ p = QProcess(self)
+
+ def done(rc, _status=None):
+ cb(rc, bytes(p.readAllStandardOutput()), bytes(p.readAllStandardError()))
+ p.deleteLater()
+
+ p.finished.connect(done)
+ p.errorOccurred.connect(
+ lambda e: e == QProcess.ProcessError.FailedToStart and done(-1))
+ p.start(prog, argv)