add logging and other things
This commit is contained in:
parent
a4650fd589
commit
067cadbe0f
10 changed files with 870 additions and 98 deletions
262
tools/run_vm.py
262
tools/run_vm.py
|
|
@ -272,83 +272,168 @@ on boot
|
|||
start selinux-probe
|
||||
"""
|
||||
|
||||
def build_initramfs(init: Path, busybox: Path, rc_text: str, root_password: str | None,
|
||||
selinux: bool, keep: bool,
|
||||
bundles: list[tuple[str, Path]] | None = None) -> Path:
|
||||
if not shutil.which("cpio"):
|
||||
sys.exit("cpio not found (install cpio)")
|
||||
root = Path(tempfile.mkdtemp(prefix="bajia-root-"))
|
||||
try:
|
||||
for sub in ("etc/bajia", "bin", "sbin", "usr/sbin", "usr/bin",
|
||||
"dev", "proc", "sys", "run", "tmp"):
|
||||
(root / sub).mkdir(parents=True)
|
||||
# subdirectories laid out in every staging root tree (initramfs and disk root).
|
||||
COMMON_SUBDIRS = ("etc/bajia", "bin", "sbin", "usr/sbin", "usr/bin",
|
||||
"dev", "proc", "sys", "run", "tmp", "mnt")
|
||||
|
||||
if selinux:
|
||||
for src, rel in selinux_policy_files():
|
||||
dst = root / rel
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(src, dst)
|
||||
selcfg = root / "etc" / "selinux" / "config"
|
||||
selcfg.write_text(f"SELINUX=permissive\nSELINUXTYPE={host_selinux_type()}\n")
|
||||
bundle_dynamic_libs(init, root)
|
||||
elif not is_static(init):
|
||||
print("warning: bajia is dynamically linked; /init will fail to exec "
|
||||
"inside the initramfs (error -2). Rebuild with --no-static "
|
||||
"unset (static is the default) or drop --no-build.")
|
||||
def stage_root_tree(root: Path, init: Path, init_rel: str, busybox: Path,
|
||||
rc_text: str, rc_rel: str, root_password: str | None,
|
||||
selinux: bool, bundles: list[tuple[str, Path]]) -> None:
|
||||
"""Lay out the common bajia + busybox tree into `root`.
|
||||
|
||||
if selinux:
|
||||
rc_text = rc_text + SELINUX_RC_PROBE
|
||||
`init_rel` is where bajia lands ('init' for the stage-1 initramfs,
|
||||
'sbin/init' for the stage-2 root disk); `rc_rel` is where init.rc lands.
|
||||
The core payload is identical for both stages."""
|
||||
for sub in COMMON_SUBDIRS:
|
||||
(root / sub).mkdir(parents=True)
|
||||
|
||||
shutil.copy(init, root / "init")
|
||||
(root / "init").chmod(0o755)
|
||||
|
||||
ctl = BUILD / "bctl"
|
||||
if ctl.is_file():
|
||||
shutil.copy(ctl, root / "bin" / "bctl")
|
||||
(root / "bin" / "bctl").chmod(0o755)
|
||||
|
||||
shutil.copy(busybox, root / "bin" / "busybox")
|
||||
(root / "bin" / "busybox").chmod(0o755)
|
||||
for applet in BUSYBOX_APPLETS:
|
||||
(root / "bin" / applet).symlink_to("busybox")
|
||||
|
||||
(root / "etc" / "bajia" / "init.rc").write_text(rc_text)
|
||||
(root / "etc" / "passwd").write_text(
|
||||
root_passwd_line(root_password) +
|
||||
"nobody:x:65534:65534:nobody:/:/bin/sh\n")
|
||||
(root / "etc" / "group").write_text(
|
||||
"root:x:0:\n"
|
||||
"nobody:x:65534:\n"
|
||||
"daemon:x:1:\n")
|
||||
|
||||
for rel, src in (bundles or []):
|
||||
dst = root / rel.lstrip("/")
|
||||
if selinux:
|
||||
for src, rel in selinux_policy_files():
|
||||
dst = root / rel
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(src, dst)
|
||||
dst.chmod(0o644)
|
||||
selcfg = root / "etc" / "selinux" / "config"
|
||||
selcfg.write_text(f"SELINUX=permissive\nSELINUXTYPE={host_selinux_type()}\n")
|
||||
bundle_dynamic_libs(init, root)
|
||||
elif not is_static(init):
|
||||
print("warning: bajia is dynamically linked; init will fail to exec "
|
||||
"inside the initramfs (error -2). Rebuild with --no-static "
|
||||
"unset (static is the default) or drop --no-build.")
|
||||
|
||||
# The staging tree is owned by the host user and mkdtemp makes the
|
||||
# top dir 0700; GNU cpio preserves both, so without this the guest's
|
||||
# "/" would be mode 0700 owned by uid 1000 -- fine for root services,
|
||||
# but dropped-privilege services couldn't traverse it. Stamp owner
|
||||
# root:root (no host chown needed) and make the root traversable.
|
||||
p = run(["bash", "-c",
|
||||
"cd \"$1\" && chmod 0755 . && "
|
||||
"find . -print0 | cpio --null -o -H newc --owner=0:0",
|
||||
"bajia-initramfs", str(root)], stdout=subprocess.PIPE)
|
||||
if p.returncode != 0:
|
||||
sys.exit("cpio packing failed")
|
||||
initrd = Path(tempfile.gettempdir()) / "bajia-initrd.cpio.gz"
|
||||
initrd.write_bytes(gzip.compress(p.stdout))
|
||||
if selinux:
|
||||
rc_text = rc_text + SELINUX_RC_PROBE
|
||||
|
||||
if keep:
|
||||
print("initramfs root tree kept at:", root)
|
||||
return initrd
|
||||
dst = root / init_rel
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(init, dst)
|
||||
dst.chmod(0o755)
|
||||
|
||||
ctl = BUILD / "bctl"
|
||||
if ctl.is_file():
|
||||
shutil.copy(ctl, root / "bin" / "bctl")
|
||||
(root / "bin" / "bctl").chmod(0o755)
|
||||
|
||||
shutil.copy(busybox, root / "bin" / "busybox")
|
||||
(root / "bin" / "busybox").chmod(0o755)
|
||||
for applet in BUSYBOX_APPLETS:
|
||||
(root / "bin" / applet).symlink_to("busybox")
|
||||
|
||||
rcdst = root / rc_rel
|
||||
rcdst.parent.mkdir(parents=True, exist_ok=True)
|
||||
rcdst.write_text(rc_text)
|
||||
|
||||
(root / "etc" / "passwd").write_text(
|
||||
root_passwd_line(root_password) +
|
||||
"nobody:x:65534:65534:nobody:/:/bin/sh\n")
|
||||
(root / "etc" / "group").write_text(
|
||||
"root:x:0:\n"
|
||||
"nobody:x:65534:\n"
|
||||
"daemon:x:1:\n")
|
||||
|
||||
for rel, src in (bundles or []):
|
||||
dst = root / rel.lstrip("/")
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(src, dst)
|
||||
dst.chmod(0o644)
|
||||
|
||||
# traversable by dropped-privilege services; the packers stamp ownership.
|
||||
root.chmod(0o755)
|
||||
|
||||
def pack_cpio(root: Path, keep: bool) -> Path:
|
||||
if not shutil.which("cpio"):
|
||||
sys.exit("cpio not found (install cpio)")
|
||||
# The staging tree is owned by the host user; --owner=0:0 and a traversable
|
||||
# root make every path root-owned inside the guest.
|
||||
p = run(["bash", "-c",
|
||||
"cd \"$1\" && find . -print0 | cpio --null -o -H newc --owner=0:0",
|
||||
"bajia-initramfs", str(root)], stdout=subprocess.PIPE)
|
||||
if p.returncode != 0:
|
||||
sys.exit("cpio packing failed")
|
||||
initrd = Path(tempfile.gettempdir()) / "bajia-initrd.cpio.gz"
|
||||
initrd.write_bytes(gzip.compress(p.stdout))
|
||||
if keep:
|
||||
print("initramfs root tree kept at:", root)
|
||||
return initrd
|
||||
|
||||
def pack_ext4(root: Path, keep: bool, size_mb: int = 128) -> Path:
|
||||
"""Pack `root` into a writable ext4 disk image via `mkfs.ext4 -d`.
|
||||
No loop mount needed, so it works unprivileged. The image is the second
|
||||
stage's real root filesystem, attached to the guest as a virtio-blk disk."""
|
||||
if not shutil.which("mkfs.ext4"):
|
||||
sys.exit("mkfs.ext4 not found (install e2fsprogs)")
|
||||
img = Path(tempfile.gettempdir()) / "bajia-stage2.ext4"
|
||||
if img.is_file():
|
||||
img.unlink()
|
||||
# mkfs.ext4 -d does not create the image file; pre-size a sparse file.
|
||||
r = run(["truncate", "-s", f"{size_mb}M", str(img)])
|
||||
if r.returncode != 0:
|
||||
sys.exit("truncate failed")
|
||||
r = run(["mkfs.ext4", "-q", "-d", str(root), "-F", str(img)])
|
||||
if r.returncode != 0:
|
||||
sys.exit("mkfs.ext4 failed")
|
||||
if keep:
|
||||
print("stage2 root tree kept at:", root)
|
||||
return img
|
||||
|
||||
def build_initramfs(init: Path, busybox: Path, rc_text: str,
|
||||
root_password: str | None, selinux: bool, keep: bool,
|
||||
bundles: list[tuple[str, Path]] | None = None) -> Path:
|
||||
root = Path(tempfile.mkdtemp(prefix="bajia-root-"))
|
||||
try:
|
||||
stage_root_tree(root, init, "init", busybox, rc_text,
|
||||
"etc/bajia/init.rc", root_password, selinux, bundles)
|
||||
return pack_cpio(root, keep)
|
||||
finally:
|
||||
if not keep:
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
|
||||
def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[str]:
|
||||
# first-stage config for --two-stage: bring up the basics, mount the real root
|
||||
# disk, and switch_root onto it. `root_dev` is the virtio-blk target (/dev/vda).
|
||||
TWO_STAGE_FIRST_RC = """\
|
||||
# generated by tools/run_vm.py --two-stage - first stage (initramfs).
|
||||
on early-init
|
||||
mount proc /proc proc
|
||||
mount sysfs /sys sysfs
|
||||
mount devtmpfs /dev devtmpfs
|
||||
|
||||
on init
|
||||
mkdir /mnt/root 0755
|
||||
mount {dev} /mnt/root {fstype}
|
||||
|
||||
on boot
|
||||
switch_root /mnt/root /sbin/init /etc/bajia/init.rc
|
||||
"""
|
||||
|
||||
def build_two_stage(init: Path, busybox: Path, second_rc: str,
|
||||
root_password: str | None, selinux: bool, keep: bool,
|
||||
bundles: list[tuple[str, Path]],
|
||||
root_dev: str = "/dev/vda",
|
||||
root_fstype: str = "ext4",
|
||||
root_size_mb: int = 128) -> tuple[Path, Path]:
|
||||
"""Return (stage1_initrd, stage2_root_img) for a classic two-stage boot.
|
||||
|
||||
stage1 is a minimal initramfs running bajia as /init with a generated
|
||||
first-stage config that mounts the real root and switch_roots onto it.
|
||||
stage2 is a writable ext4 disk image (the "real root"), bundled with its
|
||||
own copy of bajia at /sbin/init plus the full init.rc and services."""
|
||||
first_rc = TWO_STAGE_FIRST_RC.format(dev=root_dev, fstype=root_fstype)
|
||||
root1 = Path(tempfile.mkdtemp(prefix="bajia-stage1-"))
|
||||
root2 = Path(tempfile.mkdtemp(prefix="bajia-stage2-"))
|
||||
try:
|
||||
stage_root_tree(root1, init, "init", busybox, first_rc,
|
||||
"etc/bajia/init.rc", root_password, selinux, [])
|
||||
initrd = pack_cpio(root1, keep)
|
||||
stage_root_tree(root2, init, "sbin/init", busybox, second_rc,
|
||||
"etc/bajia/init.rc", root_password, selinux, bundles)
|
||||
img = pack_ext4(root2, keep, size_mb=root_size_mb)
|
||||
return initrd, img
|
||||
finally:
|
||||
if not keep:
|
||||
shutil.rmtree(root1, ignore_errors=True)
|
||||
shutil.rmtree(root2, ignore_errors=True)
|
||||
|
||||
def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace,
|
||||
root_img: Path | None = None) -> list[str]:
|
||||
qemu = args.qemu or shutil.which("qemu-system-x86_64") or "qemu-system-x86_64"
|
||||
display = args.display
|
||||
if display is None:
|
||||
|
|
@ -367,6 +452,9 @@ def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[s
|
|||
"-display", display,
|
||||
"-serial", "stdio",
|
||||
]
|
||||
if root_img is not None:
|
||||
# second-stage root disk; virtio-blk (built into modern kernels) -> /dev/vda
|
||||
cmd += ["-drive", f"file={root_img},format=raw,if=virtio"]
|
||||
if args.nographic:
|
||||
cmd[cmd.index("-display") + 1] = "none"
|
||||
if args.serial_log:
|
||||
|
|
@ -386,7 +474,22 @@ def main() -> int:
|
|||
help="download busybox source (github.com/mirror/busybox), "
|
||||
"build it static, cache in ~/.cache/bajia")
|
||||
ap.add_argument("--config", type=Path,
|
||||
help="use this init.rc instead of the bundled test config")
|
||||
help="use this init.rc instead of the bundled test config "
|
||||
"(with --two-stage this is the *second stage* config)")
|
||||
ap.add_argument("--two-stage", action="store_true",
|
||||
help="boot a classic two-stage initramfs: a minimal stage-1 "
|
||||
"initramfs runs bajia as /init, mounts a real root disk "
|
||||
"and switch_roots onto it; stage-2 is a writable ext4 "
|
||||
"root running bajia from /sbin/init")
|
||||
ap.add_argument("--root-dev", default="/dev/vda",
|
||||
help="--two-stage: block device for the real root "
|
||||
"(default /dev/vda)")
|
||||
ap.add_argument("--root-fstype", default="ext4",
|
||||
help="--two-stage: filesystem type of the real root "
|
||||
"(default ext4)")
|
||||
ap.add_argument("--root-size", type=int, default=128,
|
||||
help="--two-stage: stage-2 root image size in MiB "
|
||||
"(default 128)")
|
||||
ap.add_argument("--bundle", action="append", default=[],
|
||||
metavar="REL=HOSTPATH",
|
||||
help="copy HOSTPATH into the initramfs at absolute REL "
|
||||
|
|
@ -458,12 +561,27 @@ def main() -> int:
|
|||
if ".." in [c for c in Path(rel).parts]:
|
||||
sys.exit(f"--bundle: REL must not contain '..': {rel}")
|
||||
bundles.append((rel, src))
|
||||
initrd = build_initramfs(init, busybox, rc_text, args.root_password,
|
||||
selinux=args.selinux, keep=args.keep_initramfs,
|
||||
bundles=bundles)
|
||||
print("initramfs:", initrd, f"({initrd.stat().st_size / 1024:.0f} KB)")
|
||||
|
||||
cmd = qemu_command(kernel, initrd, args)
|
||||
if args.two_stage:
|
||||
if args.selinux:
|
||||
print("note: SELinux is bundled into both stages' roots")
|
||||
initrd, root_img = build_two_stage(
|
||||
init, busybox, rc_text, args.root_password,
|
||||
selinux=args.selinux, keep=args.keep_initramfs, bundles=bundles,
|
||||
root_dev=args.root_dev, root_fstype=args.root_fstype,
|
||||
root_size_mb=args.root_size)
|
||||
print("stage1 initramfs:", initrd,
|
||||
f"({initrd.stat().st_size / 1024:.0f} KB)")
|
||||
print("stage2 root disk:", root_img,
|
||||
f"({root_img.stat().st_size / 1024:.0f} KB)")
|
||||
else:
|
||||
initrd = build_initramfs(init, busybox, rc_text, args.root_password,
|
||||
selinux=args.selinux, keep=args.keep_initramfs,
|
||||
bundles=bundles)
|
||||
root_img = None
|
||||
print("initramfs:", initrd, f"({initrd.stat().st_size / 1024:.0f} KB)")
|
||||
|
||||
cmd = qemu_command(kernel, initrd, args, root_img)
|
||||
print("$", " ".join(cmd))
|
||||
return subprocess.run(cmd).returncode
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue