# sb-builder - ssh and virsh transport # Copyright (C) 2026 Danilo M. # # 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 , 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)