aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--remote/config.example.toml4
-rwxr-xr-xremote/sbb-remote244
-rw-r--r--tests/test_remote.py159
3 files changed, 388 insertions, 19 deletions
diff --git a/remote/config.example.toml b/remote/config.example.toml
index d7fcd7f..e9b76d0 100644
--- a/remote/config.example.toml
+++ b/remote/config.example.toml
@@ -7,3 +7,7 @@ upstream_branch = "current"
slackrepo_db = "/var/lib/slackrepo/SBo-danix/database_SBo-danix_x86_64.sqlite3"
sbpull_script = "/usr/local/share/slackrepo_setup/sbpull.bash"
overlays = ["personal", "pentesting"]
+slackrepo_repo = "SBo-danix"
+hintdir = "/etc/slackrepo/SBo-danix/hintfiles"
+default_hintdir = "" # searched after hintdir when set
+genreprc = "/root/.genreprc" # REPOSOWNERGPG names the signing key
diff --git a/remote/sbb-remote b/remote/sbb-remote
index 2d877d6..b26fa9f 100755
--- a/remote/sbb-remote
+++ b/remote/sbb-remote
@@ -18,7 +18,7 @@
"""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. Jobs run detached (setsid, stdin=/dev/null), one at a
+streams raw log bytes. gpg-unlock reads the passphrase from stdin. 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.
"""
@@ -28,15 +28,17 @@ import glob
import json
import os
import re
+import shlex
import shutil
import signal
import sqlite3
import subprocess
import sys
+import tempfile
import time
import tomllib
-VERSION = "0.1.0"
+VERSION = "0.2.0"
DEFAULTS = {
"state_dir": "/var/lib/sb-builder",
"repo_dir": "/var/lib/sbopkg/SBo-danix",
@@ -44,10 +46,17 @@ DEFAULTS = {
"slackrepo_db": "/var/lib/slackrepo/SBo-danix/database_SBo-danix_x86_64.sqlite3",
"sbpull_script": "/usr/local/share/slackrepo_setup/sbpull.bash",
"overlays": ["personal", "pentesting"],
+ "slackrepo_repo": "SBo-danix",
+ "hintdir": "/etc/slackrepo/SBo-danix/hintfiles",
+ "default_hintdir": "",
+ "genreprc": "/root/.genreprc",
}
FLAGS = ("--yes", "--no")
JOB_ID = re.compile(r"^\d{8}-\d{6}-[a-z]+(-\d+)?$")
PKG = re.compile(r"^[^/]+/([^/]+)/\1\.SlackBuild$")
+PKGNAME = re.compile(r"^[A-Za-z0-9._+-]+$")
+ACTIONS = ("build", "rebuild", "update", "remove")
+PRESET = "/usr/libexec/gpg-preset-passphrase"
ACTIVE = ("starting", "running")
START_GRACE = 10 # seconds a job may sit in "starting" before it counts as lost
@@ -77,7 +86,7 @@ def load_config(path=None):
return cfg
-# ── job kinds: (cfg, args) -> argv, raising Error on bad args ───────────────
+# ── job kinds: (cfg, args) -> (argv, env), raising Error on bad args ────────
def _flag(args):
if len(args) != 1 or args[0] not in FLAGS:
@@ -89,14 +98,49 @@ def kind_sbpull(cfg, args):
if not args or args[0] not in cfg["overlays"]:
raise Error(f"sbpull: overlay must be one of {cfg['overlays']}")
return ["bash", "-c", '. "$1" && sbpull "$2" "$3"', "_",
- cfg["sbpull_script"], args[0], _flag(args[1:])]
+ cfg["sbpull_script"], args[0], _flag(args[1:])], {}
def kind_sbshadow(cfg, args):
- return ["sbshadow", _flag(args)]
+ return ["sbshadow", _flag(args)], {}
-KINDS = {"sbpull": kind_sbpull, "sbshadow": kind_sbshadow}
+def kind_slackrepo(cfg, args):
+ if not args or args[0] not in ACTIONS:
+ raise Error(f"slackrepo: action must be one of {', '.join(ACTIONS)}")
+ action, rest = args[0], args[1:]
+ opts = [a for a in rest if a.startswith("--")]
+ for o in opts:
+ if o not in ("--preview", "--no-publish"):
+ raise Error(f"slackrepo: unknown option {o}")
+ known = {os.path.basename(d) for d in _pkgdirs(cfg["repo_dir"], "*/*")}
+ pkgs = []
+ for p in (a for a in rest if not a.startswith("--")):
+ if not PKGNAME.match(p) or p not in known:
+ raise Error(f"slackrepo: no such package: {p}")
+ if p not in pkgs:
+ pkgs.append(p)
+ if not pkgs and action != "update":
+ raise Error(f"slackrepo: {action} needs at least one package")
+ argv = ["slackrepo", f"--repo={cfg['slackrepo_repo']}", action]
+ if "--preview" in opts:
+ argv.append("--preview")
+ return argv + pkgs, {"SBB_PUBLISH": "n"} if "--no-publish" in opts else {}
+
+
+def kind_publish(cfg, args):
+ if args:
+ raise Error("publish takes no arguments")
+ return ["sb-publish"], {}
+
+
+KINDS = {"sbpull": kind_sbpull, "sbshadow": kind_sbshadow,
+ "slackrepo": kind_slackrepo, "publish": kind_publish}
+
+
+def signs(kind, args):
+ """A real slackrepo run ends in gen_repos_files.sh, which signs."""
+ return kind == "slackrepo" and "--preview" not in args
# ── job state ───────────────────────────────────────────────────────────────
@@ -173,7 +217,9 @@ def cmd_jobs(cfg):
def cmd_job_start(cfg, kind=None, *args):
if kind not in KINDS:
raise Error(f"unknown kind: {kind}")
- argv = KINDS[kind](cfg, list(args))
+ argv, env = KINDS[kind](cfg, list(args))
+ if signs(kind, args) and gpg_unlocked(cfg) is False:
+ raise Error("signing key locked")
base = time.strftime("%Y%m%d-%H%M%S-") + kind
jid, n = base, 1
while True:
@@ -185,7 +231,7 @@ def cmd_job_start(cfg, kind=None, *args):
n += 1
jid = f"{base}-{n}"
write_meta(d, {"id": jid, "kind": kind, "args": list(args), "argv": argv,
- "status": "starting", "created": time.time()})
+ "env": env, "status": "starting", "created": time.time()})
open(os.path.join(d, "log"), "wb").close()
subprocess.Popen([sys.executable, os.path.abspath(__file__), "_run", jid],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
@@ -215,7 +261,8 @@ def cmd_run(cfg, jid=None):
meta["start"] = time.time()
try:
p = subprocess.Popen(meta["argv"], stdin=subprocess.DEVNULL, stdout=log,
- stderr=subprocess.STDOUT, start_new_session=True)
+ stderr=subprocess.STDOUT, start_new_session=True,
+ env={**os.environ, **meta.get("env", {})})
except OSError as e:
log.write(f"sbb-remote: {e}\n".encode())
rc = 127
@@ -268,20 +315,24 @@ def _try(fn):
def _git(cfg, *args):
- return subprocess.run(["git", "-C", cfg["repo_dir"], *args],
+ return subprocess.run(["git", "-C", cfg["repo_dir"], *args], stdin=subprocess.DEVNULL,
capture_output=True, text=True, check=True).stdout
-def _pkgs(root, pattern):
- """Names of the <pkg>/<pkg>.SlackBuild dirs matching pattern under root."""
+def _pkgdirs(root, pattern):
+ """Paths of the <pkg>/<pkg>.SlackBuild dirs matching pattern under root."""
out = []
for d in glob.glob(os.path.join(root, pattern, "")):
- name = os.path.basename(os.path.dirname(d))
- if os.path.isfile(os.path.join(d, name + ".SlackBuild")):
- out.append(name)
+ d = os.path.dirname(d)
+ if os.path.isfile(os.path.join(d, os.path.basename(d) + ".SlackBuild")):
+ out.append(d)
return out
+def _pkgs(root, pattern):
+ return [os.path.basename(d) for d in _pkgdirs(root, pattern)]
+
+
def _packages(cfg):
r = cfg["repo_dir"]
mine = {o: _pkgs(r, o + "/*") for o in cfg["overlays"]}
@@ -320,6 +371,166 @@ def cmd_stats(cfg):
}
+# ── inventory ───────────────────────────────────────────────────────────────
+
+def parse_assignments(path):
+ """KEY=value assignments of a shell-style file, without running it.
+
+ Values with a $ are returned as {"value": raw, "shell": True}: the shell
+ would expand them, this parser does not.
+ """
+ with open(path, errors="replace") as f:
+ text = f.read().replace("\\\n", " ")
+ out = {}
+ for line in text.splitlines():
+ try:
+ tokens = shlex.split(line, comments=True)
+ except ValueError:
+ continue # unbalanced quotes: slackrepo would choke too, skip the line
+ raw = line.split("=", 1)[1] if "=" in line else ""
+ for t in tokens:
+ name, eq, value = t.partition("=")
+ if eq and re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
+ out[name] = {"value": raw.strip(), "shell": True} if "$" in raw else value
+ return out
+
+
+def _value(v):
+ return v["value"] if isinstance(v, dict) else v
+
+
+def find_hint(cfg, path):
+ """slackrepo's search order (parsefunctions.sh): first <pkg>.hint found."""
+ pkg = os.path.basename(path)
+ dirs = [os.path.join(cfg["repo_dir"], path), cfg["hintdir"],
+ os.path.join(cfg["hintdir"], path)]
+ if cfg["default_hintdir"]:
+ dirs += [cfg["default_hintdir"], os.path.join(cfg["default_hintdir"], path)]
+ for d in dirs:
+ f = os.path.join(d, pkg + ".hint")
+ if os.path.isfile(f):
+ return f
+ return None
+
+
+def _summary(pkgdir, pkg):
+ try:
+ with open(os.path.join(pkgdir, "slack-desc"), errors="replace") as f:
+ for line in f:
+ m = re.match(rf"^{re.escape(pkg)}: {re.escape(pkg)} \((.*)\)\s*$", line)
+ if m:
+ return m.group(1)
+ except OSError:
+ pass
+ return None
+
+
+def _builds(cfg):
+ """{path: (built_version, built_time)}, {path: result} from the slackrepo DB."""
+ try:
+ db = sqlite3.connect(f"file:{cfg['slackrepo_db']}?mode=ro", uri=True)
+ try:
+ built = {i: (v, int(t)) for i, v, t in
+ db.execute("select itemid, version, built from revisions where dep = '/'")}
+ results = dict(db.execute("select itemid, result from buildresults"))
+ finally:
+ db.close()
+ except sqlite3.Error:
+ return {}, {}
+ return built, results
+
+
+COLUMNS = ["path", "version", "info_version", "hint_version", "built_version", "built_time",
+ "result", "maintainer", "requires", "summary", "hint_path", "hints"]
+
+
+def cmd_inventory(cfg):
+ built, results = _builds(cfg)
+ rows = []
+ for d in sorted(_pkgdirs(cfg["repo_dir"], "*/*")):
+ path = os.path.relpath(d, cfg["repo_dir"])
+ pkg = os.path.basename(d)
+ try:
+ info = parse_assignments(os.path.join(d, pkg + ".info"))
+ except OSError:
+ info = {}
+ hint_path = find_hint(cfg, path)
+ hints = parse_assignments(hint_path) if hint_path else None
+ info_version = _value(info.get("VERSION"))
+ hint_version = _value(hints.get("VERSION")) if hints else None
+ bv, bt = built.get(path, (None, None))
+ rows.append([path, hint_version or info_version, info_version, hint_version, bv, bt,
+ results.get(path), _value(info.get("MAINTAINER")),
+ (_value(info.get("REQUIRES")) or "").split(), _summary(d, pkg),
+ hint_path, hints])
+ return {"columns": COLUMNS, "rows": rows}
+
+
+# ── signing key ─────────────────────────────────────────────────────────────
+
+def _signing_key(cfg):
+ try:
+ with open(cfg["genreprc"]) as f:
+ m = re.search(r'^\s*REPOSOWNERGPG=["\']?([^"\'\s]+)', f.read(), re.M)
+ except OSError:
+ return None
+ return m and m.group(1)
+
+
+def _keygrips(key):
+ """Keygrips of the signing-capable (sub)keys of key."""
+ out = subprocess.run(["gpg", "--batch", "--with-colons", "--with-keygrip",
+ "--list-secret-keys", key], stdin=subprocess.DEVNULL,
+ capture_output=True, text=True).stdout
+ grips, can_sign = [], False
+ for line in out.splitlines():
+ f = line.split(":")
+ if f[0] in ("sec", "ssb"):
+ can_sign = "s" in f[11]
+ elif f[0] == "grp" and can_sign:
+ grips.append(f[9])
+ return grips
+
+
+def gpg_unlocked(cfg):
+ """True/False, or None when no signing key is configured or found."""
+ key = _signing_key(cfg)
+ grips = _keygrips(key) if key else []
+ if not grips:
+ return None
+ for g in grips:
+ out = subprocess.run(["gpg-connect-agent", f"keyinfo {g}", "/bye"],
+ stdin=subprocess.DEVNULL, capture_output=True, text=True).stdout
+ info = next((ln.split() for ln in out.splitlines() if ln.startswith("S KEYINFO")), [])
+ if len(info) < 7 or info[6] != "1":
+ return False
+ return True
+
+
+def cmd_gpg_status(cfg):
+ return {"unlocked": gpg_unlocked(cfg), "key": _signing_key(cfg)}
+
+
+def cmd_gpg_unlock(cfg, stdin=None):
+ key = _signing_key(cfg)
+ grips = _keygrips(key) if key else []
+ if not grips:
+ raise Error("no signing key configured")
+ passphrase = (stdin or sys.stdin.buffer).read().rstrip(b"\n")
+ for g in grips:
+ subprocess.run([PRESET, "--preset", g], input=passphrase, check=True,
+ capture_output=True)
+ with tempfile.NamedTemporaryFile() as t:
+ ok = subprocess.run(["gpg", "--batch", "--pinentry-mode", "error", "-u", key,
+ "-bas", "-o", "-", t.name], stdin=subprocess.DEVNULL,
+ capture_output=True).returncode == 0
+ if not ok:
+ for g in grips:
+ subprocess.run([PRESET, "--forget", g], stdin=subprocess.DEVNULL, capture_output=True)
+ raise Error("bad passphrase")
+ return {"unlocked": True}
+
+
COMMANDS = {
"version": cmd_version,
"stats": cmd_stats,
@@ -327,6 +538,9 @@ COMMANDS = {
"job-start": cmd_job_start,
"job-follow": cmd_job_follow,
"job-kill": cmd_job_kill,
+ "inventory": cmd_inventory,
+ "gpg-status": cmd_gpg_status,
+ "gpg-unlock": cmd_gpg_unlock,
"_run": cmd_run,
}
diff --git a/tests/test_remote.py b/tests/test_remote.py
index 31d0597..c2b9857 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -45,7 +45,9 @@ class Base(unittest.TestCase):
with open(conf, "w") as f:
f.write(f'state_dir = "{self.tmp}"\n'
f'repo_dir = "{self.tmp}/repo"\n'
- f'slackrepo_db = "{self.tmp}/slackrepo.sqlite3"\n')
+ f'slackrepo_db = "{self.tmp}/slackrepo.sqlite3"\n'
+ f'hintdir = "{self.tmp}/hints"\n'
+ f'genreprc = "{self.tmp}/genreprc"\n')
patcher = mock.patch.dict(os.environ, {"SBB_CONFIG": conf})
patcher.start()
self.addCleanup(patcher.stop)
@@ -61,6 +63,34 @@ class Base(unittest.TestCase):
open(os.path.join(d, "log"), "wb").close()
return jid, d
+ def make_pkg(self, path, info="", hint=None, desc=None):
+ d = os.path.join(self.cfg["repo_dir"], path)
+ pkg = os.path.basename(path)
+ os.makedirs(d, exist_ok=True)
+ open(os.path.join(d, pkg + ".SlackBuild"), "w").close()
+ with open(os.path.join(d, pkg + ".info"), "w") as f:
+ f.write(info)
+ if desc:
+ with open(os.path.join(d, "slack-desc"), "w") as f:
+ f.write(f"{pkg}: {pkg} ({desc})\n{pkg}:\n")
+ if hint is not None:
+ os.makedirs(self.cfg["hintdir"], exist_ok=True)
+ with open(os.path.join(self.cfg["hintdir"], pkg + ".hint"), "w") as f:
+ f.write(hint)
+
+ def fake_bin(self, name, script):
+ bindir = os.path.join(self.tmp, "bin")
+ os.makedirs(bindir, exist_ok=True)
+ path = os.path.join(bindir, name)
+ with open(path, "w") as f:
+ f.write("#!/bin/sh\n" + script)
+ os.chmod(path, 0o755)
+ if bindir not in os.environ["PATH"]:
+ patcher = mock.patch.dict(os.environ, {"PATH": bindir + os.pathsep + os.environ["PATH"]})
+ patcher.start()
+ self.addCleanup(patcher.stop)
+ return path
+
def hold_lock(self):
f = open(os.path.join(self.tmp, "lock"), "a")
fcntl.flock(f, fcntl.LOCK_EX)
@@ -109,11 +139,33 @@ class ConfigTest(Base):
class KindsTest(Base):
def test_sbpull_argv(self):
self.assertEqual(sbb.KINDS["sbpull"](self.cfg, ["personal", "--no"]),
- ["bash", "-c", '. "$1" && sbpull "$2" "$3"', "_",
- self.cfg["sbpull_script"], "personal", "--no"])
+ (["bash", "-c", '. "$1" && sbpull "$2" "$3"', "_",
+ self.cfg["sbpull_script"], "personal", "--no"], {}))
def test_sbshadow_argv(self):
- self.assertEqual(sbb.KINDS["sbshadow"](self.cfg, ["--yes"]), ["sbshadow", "--yes"])
+ self.assertEqual(sbb.KINDS["sbshadow"](self.cfg, ["--yes"]), (["sbshadow", "--yes"], {}))
+
+ def test_slackrepo_argv(self):
+ self.make_pkg("misc/nuclei")
+ self.make_pkg("personal/foo")
+ kind = sbb.KINDS["slackrepo"]
+ self.assertEqual(kind(self.cfg, ["update", "--preview", "nuclei", "foo", "nuclei"]),
+ (["slackrepo", "--repo=SBo-danix", "update", "--preview", "nuclei", "foo"], {}))
+ self.assertEqual(kind(self.cfg, ["build", "--no-publish", "foo"]),
+ (["slackrepo", "--repo=SBo-danix", "build", "foo"], {"SBB_PUBLISH": "n"}))
+ self.assertEqual(kind(self.cfg, ["update"]), (["slackrepo", "--repo=SBo-danix", "update"], {}))
+
+ def test_slackrepo_rejects(self):
+ self.make_pkg("misc/nuclei")
+ for args in (["install", "nuclei"], ["build"], ["remove"], ["build", "nope"],
+ ["build", "../etc"], ["build", "--force", "nuclei"]):
+ with self.subTest(args=args), self.assertRaises(sbb.Error):
+ sbb.KINDS["slackrepo"](self.cfg, args)
+
+ def test_publish(self):
+ self.assertEqual(sbb.KINDS["publish"](self.cfg, []), (["sb-publish"], {}))
+ with self.assertRaises(sbb.Error):
+ sbb.KINDS["publish"](self.cfg, ["x"])
def test_rejects_bad_args(self):
for kind, args in (("sbpull", ["evil", "--no"]), ("sbpull", ["personal"]),
@@ -138,6 +190,100 @@ class MainTest(Base):
(0, {"version": sbb.VERSION, "overlays": ["personal", "pentesting"]}))
+class InventoryTest(Base):
+ def test_rows(self):
+ self.make_pkg("misc/nuclei", 'VERSION="3.11.0"\nMAINTAINER="Some One"\n'
+ 'REQUIRES="go \\\n git"\n', desc="Fast scanner")
+ self.make_pkg("personal/foo", 'VERSION="1.0"\n',
+ hint='VERSION="1.1" # bumped\nDELREQUIRES="a b"\nOPTIONS="X=$(uname -m)"\n')
+ os.makedirs(os.path.join(self.cfg["repo_dir"], "misc", "notapkg"))
+ db = sqlite3.connect(self.cfg["slackrepo_db"])
+ db.executescript("""
+ create table buildresults (itemid text primary key, time text, result text);
+ create table revisions (itemid text, dep text, deplist text, version text,
+ built text, rev text, os text, hintcksum text, primary key (itemid, dep));
+ insert into buildresults values ('misc/nuclei', '5', 'ok'), ('nuclei', '1', 'bad');
+ insert into revisions values ('misc/nuclei', '/', '/', '3.11.0', '1788978222', 'r', 'o', '/');
+ """)
+ db.close()
+ inv = sbb.cmd_inventory(self.cfg)
+ rows = [dict(zip(inv["columns"], r)) for r in inv["rows"]]
+ self.assertEqual([r["path"] for r in rows], ["misc/nuclei", "personal/foo"])
+ nuclei, foo = rows
+ self.assertEqual((nuclei["version"], nuclei["built_version"], nuclei["built_time"],
+ nuclei["result"], nuclei["maintainer"], nuclei["requires"],
+ nuclei["summary"], nuclei["hint_path"]),
+ ("3.11.0", "3.11.0", 1788978222, "ok", "Some One", ["go", "git"],
+ "Fast scanner", None))
+ self.assertEqual((foo["version"], foo["info_version"], foo["hint_version"], foo["result"]),
+ ("1.1", "1.0", "1.1", None))
+ self.assertEqual(foo["hints"]["DELREQUIRES"], "a b")
+ self.assertEqual(foo["hints"]["OPTIONS"], {"value": '"X=$(uname -m)"', "shell": True})
+ json.dumps(inv)
+
+ def test_hint_search_order(self):
+ self.make_pkg("misc/foo")
+ sub = os.path.join(self.cfg["hintdir"], "misc", "foo")
+ os.makedirs(sub)
+ with open(os.path.join(sub, "foo.hint"), "w") as f:
+ f.write('VERSION="2"\n')
+ self.assertEqual(sbb.find_hint(self.cfg, "misc/foo"), os.path.join(sub, "foo.hint"))
+ with open(os.path.join(self.cfg["hintdir"], "foo.hint"), "w") as f:
+ f.write('VERSION="3"\n')
+ self.assertEqual(sbb.find_hint(self.cfg, "misc/foo"),
+ os.path.join(self.cfg["hintdir"], "foo.hint"))
+ in_repo = os.path.join(self.cfg["repo_dir"], "misc", "foo", "foo.hint")
+ open(in_repo, "w").close()
+ self.assertEqual(sbb.find_hint(self.cfg, "misc/foo"), in_repo)
+
+ def test_missing_db(self):
+ self.make_pkg("misc/foo", 'VERSION="1"\n')
+ row = sbb.cmd_inventory(self.cfg)["rows"][0]
+ self.assertEqual(row[4:7], [None, None, None])
+
+
+class GpgTest(Base):
+ def setup_key(self, cached):
+ with open(self.cfg["genreprc"], "w") as f:
+ f.write('REPOSOWNERGPG="ABCD1234"\nUSE_GPGAGENT=1\n')
+ self.fake_bin("gpg", 'case "$*" in\n'
+ ' *--list-secret-keys*) printf "sec:u:255:22:K:1::::::scESC:\\n'
+ 'grp:::::::::GRIPSIGN:\\nssb:u:255:18:S:1::::::e:\\ngrp:::::::::GRIPENC:\\n" ;;\n'
+ f' *-bas*) [ -f "{self.tmp}/good" ] ;;\n'
+ 'esac\n')
+ flag = "1" if cached else "-"
+ self.fake_bin("gpg-connect-agent", f'echo "S KEYINFO GRIPSIGN D - - {flag} P - - -"; echo OK\n')
+
+ def test_status(self):
+ self.assertEqual(sbb.cmd_gpg_status(self.cfg), {"unlocked": None, "key": None})
+ self.setup_key(cached=False)
+ self.assertEqual(sbb.cmd_gpg_status(self.cfg), {"unlocked": False, "key": "ABCD1234"})
+ self.setup_key(cached=True)
+ self.assertEqual(sbb.cmd_gpg_status(self.cfg)["unlocked"], True)
+
+ def test_locked_refuses_real_run_only(self):
+ self.setup_key(cached=False)
+ self.make_pkg("misc/foo")
+ with self.assertRaisesRegex(sbb.Error, "signing key locked"):
+ sbb.cmd_job_start(self.cfg, "slackrepo", "build", "foo")
+ self.fake_bin("slackrepo", "exit 0\n")
+ self.assertIn("id", sbb.cmd_job_start(self.cfg, "slackrepo", "build", "--preview", "foo"))
+
+ def test_unlock(self):
+ self.setup_key(cached=False)
+ log = os.path.join(self.tmp, "preset.log")
+ preset = self.fake_bin("preset", f'echo "$* $(cat)" >> {log}\n')
+ with mock.patch.object(sbb, "PRESET", preset):
+ with self.assertRaisesRegex(sbb.Error, "bad passphrase"):
+ sbb.cmd_gpg_unlock(self.cfg, stdin=io.BytesIO(b"wrong\n"))
+ open(os.path.join(self.tmp, "good"), "w").close()
+ self.assertEqual(sbb.cmd_gpg_unlock(self.cfg, stdin=io.BytesIO(b"right\n")),
+ {"unlocked": True})
+ with open(log) as f:
+ self.assertEqual(f.read().splitlines(),
+ ["--preset GRIPSIGN wrong", "--forget GRIPSIGN ", "--preset GRIPSIGN right"])
+
+
class JobsTest(Base):
def log(self, d):
with open(os.path.join(d, "log"), "rb") as f:
@@ -220,6 +366,11 @@ class JobsTest(Base):
patcher.start()
self.addCleanup(patcher.stop)
+ def test_env_reaches_child(self):
+ jid, d = self.make_job(["sh", "-c", 'echo "publish=$SBB_PUBLISH"'], env={"SBB_PUBLISH": "n"})
+ sbb.cmd_run(self.cfg, jid)
+ self.assertEqual(self.log(d), b"publish=n\n")
+
def test_start_end_to_end(self):
self.fake_sbshadow()
rc, data = self.run_main("job-start", "sbshadow", "--no")