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.
This commit is contained in:
Dusty Mabe 2024-01-08 23:19:44 -05:00 committed by Brian C. Lane
parent 962b7f4d4b
commit 83a14886d3
4 changed files with 26 additions and 8 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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
)

View file

@ -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