diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-21 10:12:29 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-21 10:12:29 +0200 |
| commit | 26d7799fe90e621994503231ac917336f99b6145 (patch) | |
| tree | 286306f6f9bd74e9e1090f7927e9efd343d2df01 /remote/sbb-remote | |
| parent | 1c1fb069f7f3bae5946c485ef48846c150ef1d54 (diff) | |
| download | sb-builder-26d7799fe90e621994503231ac917336f99b6145.tar.gz sb-builder-26d7799fe90e621994503231ac917336f99b6145.zip | |
sbb-remote 0.10.0: config-get/config-set; GUI watches its config
Keystone's settings pages edit both configs. config-get returns the VM
config text, the defaults and the path; config-set validates new text
with the same check() as load_config, keeps .bak and replaces the file
atomically. Both run before load_config, so a broken config can still be
read and fixed. The GUI reloads its config when the file changes,
re-adding the watch after an atomic write replaces the inode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'remote/sbb-remote')
| -rwxr-xr-x | remote/sbb-remote | 63 |
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()) |
