From 83a14886d38cff6dbb448a4cf044a83d63418eb0 Mon Sep 17 00:00:00 2001 From: Dusty Mabe Date: Mon, 8 Jan 2024 23:19:44 -0500 Subject: [PATCH] add --break for requesting a debug shell Similar to rd.break for dracut this allows a user to specify: - --break or --break=* - to get a shell before each stage is run - --break=stage.name - to get a shell each time the stage with that name is run - example: --break=org.osbuild.copy - --break=stage.id - to get a shell each time the stage with that ID is run - get the ID for the stages for your manifest by running osbuild on the manifest with --inspect - example: --break=dc6e3a66fef3ebe7c815eb24d348215b9e5e2ed0cd808c15ebbe85fc73181a86 and get a bash shell where they can inspect the environment to debug and develop OSBuild stages. --- docs/osbuild.1.rst | 2 ++ osbuild/buildroot.py | 10 ++++++++-- osbuild/main_cli.py | 5 +++++ osbuild/pipeline.py | 17 +++++++++++------ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/osbuild.1.rst b/docs/osbuild.1.rst index 62b3ed63..82a2e0ad 100644 --- a/docs/osbuild.1.rst +++ b/docs/osbuild.1.rst @@ -57,6 +57,8 @@ is not listed here, **osbuild** will deny startup and exit with an error. --monitor-fd=NUM file-descriptor to be used for the monitor --stage-timeout set the maximal time (in seconds) each stage is allowed to run +--break, --break=ID open debug shell when executing stages; accepts + stage name or id (from --inspect) or * (for all) NB: If neither ``--output-directory`` nor ``--checkpoint`` is specified, no attempt to build the manifest will be made. diff --git a/osbuild/buildroot.py b/osbuild/buildroot.py index 5b47d70e..ddc60a0b 100644 --- a/osbuild/buildroot.py +++ b/osbuild/buildroot.py @@ -70,7 +70,7 @@ class ProcOverrides: self.overrides.add("cmdline") -# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-instance-attributes,too-many-branches class BuildRoot(contextlib.AbstractContextManager): """Build Root @@ -177,7 +177,7 @@ class BuildRoot(contextlib.AbstractContextManager): if self._exitstack: self._exitstack.enter_context(api) - def run(self, argv, monitor, timeout=None, binds=None, readonly_binds=None, extra_env=None): + def run(self, argv, monitor, timeout=None, binds=None, readonly_binds=None, extra_env=None, debug_shell=False): """Runs a command in the buildroot. Takes the command and arguments, as well as bind mounts to mirror @@ -289,6 +289,7 @@ class BuildRoot(contextlib.AbstractContextManager): cmd += self.build_capabilities_args() cmd += mounts + debug_shell_cmd = cmd + ["--", "/bin/bash"] # used for debugging if requested cmd += ["--", runner] cmd += argv @@ -304,6 +305,11 @@ class BuildRoot(contextlib.AbstractContextManager): if extra_env: env.update(extra_env) + # If the user requested it then break into a shell here + # for debugging. + if debug_shell: + subprocess.run(debug_shell_cmd, check=True) + proc = subprocess.Popen(cmd, bufsize=0, env=env, diff --git a/osbuild/main_cli.py b/osbuild/main_cli.py index 81459470..19a72579 100644 --- a/osbuild/main_cli.py +++ b/osbuild/main_cli.py @@ -95,6 +95,9 @@ def parse_arguments(sys_argv): parser.add_argument("--version", action="version", help="return the version of osbuild", version="%(prog)s " + osbuild.__version__) + # nargs='?' const='*' means `--break` is equivalent to `--break=*` + parser.add_argument("--break", dest='debug_break', type=str, nargs='?', const='*', + help="open debug shell when executing stage. Accepts stage name or id or * (for all)") return parser.parse_args(sys_argv[1:]) @@ -163,6 +166,7 @@ def osbuild_cli(): object_store.maximum_size = args.cache_max_size stage_timeout = args.stage_timeout + debug_break = args.debug_break pipelines = manifest.depsolve(object_store, exports) @@ -173,6 +177,7 @@ def osbuild_cli(): pipelines, monitor, args.libdir, + debug_break, stage_timeout=stage_timeout ) diff --git a/osbuild/pipeline.py b/osbuild/pipeline.py index a450d791..af4c3944 100644 --- a/osbuild/pipeline.py +++ b/osbuild/pipeline.py @@ -142,7 +142,7 @@ class Stage: with open(location, "w", encoding="utf-8") as fp: json.dump(args, fp) - def run(self, tree, runner, build_tree, store, monitor, libdir, timeout=None): + def run(self, tree, runner, build_tree, store, monitor, libdir, debug_break="", timeout=None): with contextlib.ExitStack() as cm: build_root = buildroot.BuildRoot(build_tree, runner.path, libdir, store.tmp) @@ -234,12 +234,15 @@ class Stage: if self.source_epoch is not None: extra_env["SOURCE_DATE_EPOCH"] = str(self.source_epoch) + debug_shell = debug_break in ('*', self.name, self.id) + r = build_root.run([f"/run/osbuild/bin/{self.name}"], monitor, timeout=timeout, binds=binds, readonly_binds=ro_binds, - extra_env=extra_env) + extra_env=extra_env, + debug_shell=debug_shell) return BuildResult(self, r.returncode, r.output, api.error) @@ -290,7 +293,7 @@ class Pipeline: self.assembler.base = stage.id return stage - def build_stages(self, object_store, monitor, libdir, stage_timeout=None): + def build_stages(self, object_store, monitor, libdir, debug_break="", stage_timeout=None): results = {"success": True} # If there are no stages, just return here @@ -348,6 +351,7 @@ class Pipeline: object_store, monitor, libdir, + debug_break, stage_timeout) monitor.result(r) @@ -365,13 +369,14 @@ class Pipeline: return results - def run(self, store, monitor, libdir, stage_timeout=None): + def run(self, store, monitor, libdir, debug_break="", stage_timeout=None): monitor.begin(self) results = self.build_stages(store, monitor, libdir, + debug_break, stage_timeout) monitor.finish(results) @@ -461,11 +466,11 @@ class Manifest: return list(map(lambda x: x.name, reversed(build.values()))) - def build(self, store, pipelines, monitor, libdir, stage_timeout=None): + def build(self, store, pipelines, monitor, libdir, debug_break="", stage_timeout=None): results = {"success": True} for pl in map(self.get, pipelines): - res = pl.run(store, monitor, libdir, stage_timeout) + res = pl.run(store, monitor, libdir, debug_break, stage_timeout) results[pl.id] = res if not res["success"]: results["success"] = False