aboutsummaryrefslogtreecommitdiffstats
path: root/remote
diff options
context:
space:
mode:
Diffstat (limited to 'remote')
-rwxr-xr-xremote/sbb-remote63
1 files changed, 54 insertions, 9 deletions
diff --git a/remote/sbb-remote b/remote/sbb-remote
index 1c87f80..cabb8d1 100755
--- a/remote/sbb-remote
+++ b/remote/sbb-remote
@@ -18,7 +18,8 @@
"""Helper run over ssh by the sb-builder GUI.
Every command prints one JSON object on stdout, except job-follow, which
-streams raw log bytes. gpg-unlock reads the passphrase from stdin. Jobs run detached (setsid, stdin=/dev/null), one at a
+streams raw log bytes. gpg-unlock reads the passphrase from stdin, config-set
+the new config text. Jobs run detached (setsid, stdin=/dev/null), one at a
time under a flock, from a fixed table of kinds: the GUI sends a kind and
its arguments, this file validates them and builds the argv itself.
"""
@@ -40,7 +41,7 @@ import time
import tomllib
import urllib.request
-VERSION = "0.9.0"
+VERSION = "0.10.0"
DEFAULTS = {
"state_dir": "/var/lib/sb-builder",
"repo_dir": "/var/lib/sbopkg/SBo-danix",
@@ -75,16 +76,24 @@ class Error(Exception):
pass
+def config_path():
+ return os.environ.get("SBB_CONFIG", "/etc/sb-builder/config.toml")
+
+
def load_config(path=None):
- path = path or os.environ.get("SBB_CONFIG", "/etc/sb-builder/config.toml")
- cfg = dict(DEFAULTS)
try:
- with open(path, "rb") as f:
+ with open(path or config_path(), "rb") as f:
data = tomllib.load(f)
except FileNotFoundError:
- return cfg
+ return dict(DEFAULTS)
except tomllib.TOMLDecodeError as e:
raise Error(f"config: {e}")
+ return check(data)
+
+
+def check(data):
+ """The config from parsed TOML over the defaults; Error on an unknown key or a wrong type."""
+ cfg = dict(DEFAULTS)
for k, v in data.items():
if k not in DEFAULTS:
raise Error(f"config: {k}: unknown key")
@@ -1086,6 +1095,38 @@ def cmd_gpg_unlock(cfg, stdin=None):
return {"unlocked": True}
+# ── config editing (Keystone's settings page): no cfg, a broken file must stay fixable ──
+
+def cmd_config_get():
+ try:
+ with open(config_path()) as f:
+ text = f.read()
+ except FileNotFoundError:
+ text = ""
+ return {"text": text, "defaults": DEFAULTS, "path": config_path()}
+
+
+def cmd_config_set(stdin=None):
+ """New config text on stdin: validated, the old file to .bak, then an atomic replace."""
+ try:
+ text = (stdin or sys.stdin.buffer).read().decode()
+ check(tomllib.loads(text))
+ except (UnicodeDecodeError, tomllib.TOMLDecodeError) as e:
+ raise Error(f"config: {e}")
+ path = config_path()
+ mode = 0o644
+ if os.path.exists(path):
+ mode = os.stat(path).st_mode & 0o777
+ shutil.copy2(path, path + ".bak")
+ with tempfile.NamedTemporaryFile("w", dir=os.path.dirname(path), delete=False) as t:
+ t.write(text)
+ os.chmod(t.name, mode)
+ os.replace(t.name, path)
+ return {"ok": True}
+
+
+CONFIG_COMMANDS = {"config-get": cmd_config_get, "config-set": cmd_config_set}
+
COMMANDS = {
"version": cmd_version,
"stats": cmd_stats,
@@ -1108,6 +1149,7 @@ COMMANDS = {
"fix-current-now": cmd_fix_current_now,
"_run": cmd_run,
"_hint": cmd_hint,
+ **CONFIG_COMMANDS,
}
@@ -1116,9 +1158,12 @@ def main(argv):
if not argv or argv[0] not in COMMANDS:
names = ",".join(c for c in COMMANDS if not c.startswith("_"))
raise Error(f"usage: sbb-remote {{{names}}} [args]")
- cfg = load_config()
- os.makedirs(os.path.join(cfg["state_dir"], "jobs"), exist_ok=True)
- result = COMMANDS[argv[0]](cfg, *argv[1:])
+ if argv[0] in CONFIG_COMMANDS:
+ result = CONFIG_COMMANDS[argv[0]](*argv[1:])
+ else:
+ cfg = load_config()
+ os.makedirs(os.path.join(cfg["state_dir"], "jobs"), exist_ok=True)
+ result = COMMANDS[argv[0]](cfg, *argv[1:])
except BrokenPipeError:
# the GUI hung up on job-follow; silence the flush at exit too
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())