From: Daan De Meyer Date: Fri, 21 Apr 2023 20:19:47 +0000 (+0200) Subject: Rework bwrap run functions X-Git-Tag: v15~207^2~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ad65a4001e48829fd7393424ebbb3f04e62c2bda;p=thirdparty%2Fmkosi.git Rework bwrap run functions - To split up our dependencies more, we need to make run.py independent of MkosiState, so let's do that. - To get rid of the shell hacks in both functions to chmod /tmp, /var/tmp and /dev/shm, let's just mount the relevant files from the host which have the right permissions. - Fixing the above exposed a bug in the logic to set up rpm based systems, which all ship a filesystem package that includes directories such as /tmp, /proc, /sys, ... which we overmount with apivfs or tmpfs filesystems when running rpm, causing errors when the filesystem package tries to set up these directories. To ensure these directories are created with the permissions from the filesystem package, the run_with_apivfs() function is renamed to bwrap() and gains an apivfs argument, which takes a path to set up apivfs directories in. If not provided, no apivfs is set up. This is then used to install the filesystem package without apivfs so that the directories can be created with the right permissions. - The various rpm distributions now install the filesystem package instead of the setup package by default, so we can disable apivfs properly while filesystem is being installed. system-user-root was removed for opensuse because the filesystem package depends on it. --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index a5283e3a1..0941390a8 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -426,21 +426,21 @@ def run_prepare_script(state: MkosiState, cached: bool, build: bool) -> None: if build: with complete_step("Running prepare script in build overlay…"), mount_build_overlay(state): run_workspace_command( - state, + state.root, ["/root/prepare", "build"], network=True, bwrap_params=bwrap, - env=dict(SRCDIR="/root/src"), + env=dict(SRCDIR="/root/src") | state.environment, ) clean() else: with complete_step("Running prepare script…"): run_workspace_command( - state, + state.root, ["/root/prepare", "final"], network=True, bwrap_params=bwrap, - env=dict(SRCDIR="/root/src"), + env=dict(SRCDIR="/root/src") | state.environment, ) clean() @@ -456,8 +456,8 @@ def run_postinst_script(state: MkosiState) -> None: "--bind", state.config.postinst_script, "/root/postinst", ] - run_workspace_command(state, ["/root/postinst", "final"], bwrap_params=bwrap, - network=state.config.with_network) + run_workspace_command(state.root, ["/root/postinst", "final"], bwrap_params=bwrap, + network=state.config.with_network, env=state.environment) state.root.joinpath("root/postinst").unlink() @@ -1606,7 +1606,8 @@ def run_kernel_install(state: MkosiState, cached: bool) -> None: not state.root.joinpath("usr/lib/kernel/install.d/50-dracut.install").exists() and not state.root.joinpath("etc/kernel/install.d/50-dracut.install").exists()): with complete_step("Running dpkg-reconfigure dracut…"): - run_workspace_command(state, ["dpkg-reconfigure", "dracut"], env=dict(hostonly_l="no")) + run_workspace_command(state.root, ["dpkg-reconfigure", "dracut"], + env=dict(hostonly_l="no") | state.environment) return with complete_step("Running kernel-install…"): @@ -1617,7 +1618,7 @@ def run_kernel_install(state: MkosiState, cached: bool) -> None: cmd.insert(1, "--verbose") # Make dracut think --no-host-only was passed via the CLI. - run_workspace_command(state, cmd, env=dict(hostonly_l="no")) + run_workspace_command(state.root, cmd, env=dict(hostonly_l="no") | state.environment) if machine_id and (p := state.root / "boot" / machine_id / kver / "initrd").exists(): shutil.move(p, state.root / state.installer.initrd_path(kver)) @@ -1661,7 +1662,7 @@ def run_selinux_relabel(state: MkosiState) -> None: cmd = f"mkdir /tmp/relabel && mount --bind / /tmp/relabel && exec setfiles -m -r /tmp/relabel -F {fc} /tmp/relabel || exit $?" with complete_step(f"Relabeling files using {policy} policy"): - run_workspace_command(state, ["sh", "-c", cmd]) + run_workspace_command(state.root, ["sh", "-c", cmd], env=state.environment) def reuse_cache_tree(state: MkosiState) -> bool: @@ -1866,8 +1867,8 @@ def run_build_script(state: MkosiState) -> None: # build-script output goes to stdout so we can run language servers from within mkosi # build-scripts. See https://github.com/systemd/mkosi/pull/566 for more information. - run_workspace_command(state, cmd, network=state.config.with_network, bwrap_params=bwrap, - stdout=sys.stdout, env=env) + run_workspace_command(state.root, cmd, network=state.config.with_network, bwrap_params=bwrap, + stdout=sys.stdout, env=env | state.environment) def need_cache_tree(state: MkosiState) -> bool: diff --git a/mkosi/distributions/arch.py b/mkosi/distributions/arch.py index 5a6ddb56b..f9bfe4323 100644 --- a/mkosi/distributions/arch.py +++ b/mkosi/distributions/arch.py @@ -4,7 +4,7 @@ from collections.abc import Sequence from textwrap import dedent from mkosi.distributions import DistributionInstaller -from mkosi.run import run_with_apivfs +from mkosi.run import bwrap from mkosi.types import PathString from mkosi.util import MkosiState, sort_packages @@ -96,4 +96,4 @@ def invoke_pacman(state: MkosiState, packages: Sequence[str]) -> None: if state.config.initrds: cmdline += ["--assume-installed", "initramfs"] - run_with_apivfs(state, cmdline, env=dict(KERNEL_INSTALL_BYPASS="1")) + bwrap(cmdline, apivfs=state.root, env=dict(KERNEL_INSTALL_BYPASS="1") | state.environment) diff --git a/mkosi/distributions/centos.py b/mkosi/distributions/centos.py index 2c784a194..4962965cb 100644 --- a/mkosi/distributions/centos.py +++ b/mkosi/distributions/centos.py @@ -76,14 +76,14 @@ class CentosInstaller(DistributionInstaller): @classmethod def install(cls, state: MkosiState) -> None: - cls.install_packages(state, ["setup"]) + cls.install_packages(state, ["filesystem"], apivfs=False) # On Fedora, the default rpmdb has moved to /usr/lib/sysimage/rpm so if that's the case we need to # move it back to /var/lib/rpm on CentOS. move_rpm_db(state.root) @classmethod - def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: + def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None: release = int(state.config.release) if release <= 7: @@ -100,7 +100,7 @@ class CentosInstaller(DistributionInstaller): else: env = {} - invoke_dnf(state, "install", packages, env) + invoke_dnf(state, "install", packages, env, apivfs=apivfs) @classmethod def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: diff --git a/mkosi/distributions/debian.py b/mkosi/distributions/debian.py index 133ae18ec..62d3a53b8 100644 --- a/mkosi/distributions/debian.py +++ b/mkosi/distributions/debian.py @@ -7,7 +7,7 @@ from pathlib import Path from textwrap import dedent from mkosi.distributions import DistributionInstaller -from mkosi.run import run, run_with_apivfs +from mkosi.run import bwrap, run from mkosi.types import CompletedProcess, PathString from mkosi.util import MkosiState @@ -220,4 +220,4 @@ def invoke_apt( INITRD="No", ) - return run_with_apivfs(state, ["apt-get", operation, *extra], env=env) + return bwrap(["apt-get", operation, *extra], apivfs=state.root, env=env | state.environment) diff --git a/mkosi/distributions/fedora.py b/mkosi/distributions/fedora.py index 2b1543b3d..ebb13ec7f 100644 --- a/mkosi/distributions/fedora.py +++ b/mkosi/distributions/fedora.py @@ -12,7 +12,7 @@ from typing import Any, NamedTuple, Optional from mkosi.distributions import DistributionInstaller from mkosi.remove import unlink_try_hard -from mkosi.run import run_with_apivfs +from mkosi.run import bwrap from mkosi.util import Distribution, MkosiState, detect_distribution, sort_packages @@ -23,10 +23,10 @@ class FedoraInstaller(DistributionInstaller): @classmethod def install(cls, state: MkosiState) -> None: - cls.install_packages(state, ["setup"]) + cls.install_packages(state, ["filesystem"], apivfs=False) @classmethod - def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: + def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None: release, releasever = parse_fedora_release(state.config.release) if state.config.local_mirror: @@ -60,7 +60,7 @@ class FedoraInstaller(DistributionInstaller): repos += [Repo("updates", updates_url, gpgpath, gpgurl)] setup_dnf(state, repos) - invoke_dnf(state, "install", packages) + invoke_dnf(state, "install", packages, apivfs=apivfs) @classmethod def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: @@ -123,7 +123,13 @@ def setup_dnf(state: MkosiState, repos: Sequence[Repo] = ()) -> None: ) -def invoke_dnf(state: MkosiState, command: str, packages: Iterable[str], env: Mapping[str, Any] = {}) -> None: +def invoke_dnf( + state: MkosiState, + command: str, + packages: Iterable[str], + env: Mapping[str, Any] = {}, + apivfs: bool = True +) -> None: if state.config.distribution == Distribution.fedora: release, _ = parse_fedora_release(state.config.release) else: @@ -167,7 +173,8 @@ def invoke_dnf(state: MkosiState, command: str, packages: Iterable[str], env: Ma cmdline += sort_packages(packages) - run_with_apivfs(state, cmdline, env=dict(KERNEL_INSTALL_BYPASS="1") | env) + bwrap(cmdline, apivfs=state.root if apivfs else None, + env=dict(KERNEL_INSTALL_BYPASS="1") | env | state.environment) distribution, _ = detect_distribution() if distribution not in (Distribution.debian, Distribution.ubuntu): diff --git a/mkosi/distributions/gentoo.py b/mkosi/distributions/gentoo.py index e3261cad6..eb9d20db6 100644 --- a/mkosi/distributions/gentoo.py +++ b/mkosi/distributions/gentoo.py @@ -46,7 +46,7 @@ def invoke_emerge( else: emerge_default_opts += ["--quiet-build", "--quiet"] cmd = ["emerge", *pkgs, *emerge_default_opts, *opts, *actions] - run_workspace_command(state, cmd, network=True) + run_workspace_command(state.root, cmd, network=True, env=state.environment) class Gentoo: @@ -298,7 +298,8 @@ class Gentoo: ) def get_snapshot_of_portage_tree(self) -> None: - run_workspace_command(self.state, ["/usr/bin/emerge-webrsync"], network=True) + run_workspace_command(self.state.root, ["/usr/bin/emerge-webrsync"], network=True, + env=self.state.environment) def update_stage3(self) -> None: invoke_emerge(self.state, opts=self.EMERGE_UPDATE_OPTS, pkgs=self.pkgs['boot']) diff --git a/mkosi/distributions/mageia.py b/mkosi/distributions/mageia.py index cc8f13317..140ca97c4 100644 --- a/mkosi/distributions/mageia.py +++ b/mkosi/distributions/mageia.py @@ -15,10 +15,10 @@ class MageiaInstaller(DistributionInstaller): @classmethod def install(cls, state: MkosiState) -> None: - cls.install_packages(state, ["setup"]) + cls.install_packages(state, ["filesystem"], apivfs=False) @classmethod - def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: + def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None: release = state.config.release.strip("'") if state.config.local_mirror: @@ -46,7 +46,7 @@ class MageiaInstaller(DistributionInstaller): repos += [Repo(f"mageia-{release}-updates", updates_url, gpgpath)] setup_dnf(state, repos) - invoke_dnf(state, "install", packages) + invoke_dnf(state, "install", packages, apivfs=apivfs) @classmethod def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: diff --git a/mkosi/distributions/openmandriva.py b/mkosi/distributions/openmandriva.py index 01e08d4f0..8355b8771 100644 --- a/mkosi/distributions/openmandriva.py +++ b/mkosi/distributions/openmandriva.py @@ -15,10 +15,10 @@ class OpenmandrivaInstaller(DistributionInstaller): @classmethod def install(cls, state: MkosiState) -> None: - cls.install_packages(state, ["setup"]) + cls.install_packages(state, ["filesystem"]) @classmethod - def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: + def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None: release = state.config.release.strip("'") if release[0].isdigit(): @@ -47,7 +47,7 @@ class OpenmandrivaInstaller(DistributionInstaller): repos += [Repo("updates", updates_url, gpgpath)] setup_dnf(state, repos) - invoke_dnf(state, "install", packages) + invoke_dnf(state, "install", packages, apivfs=apivfs) @classmethod def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: diff --git a/mkosi/distributions/opensuse.py b/mkosi/distributions/opensuse.py index 96f914963..dd56d3ac8 100644 --- a/mkosi/distributions/opensuse.py +++ b/mkosi/distributions/opensuse.py @@ -5,7 +5,7 @@ from pathlib import Path from textwrap import dedent from mkosi.distributions import DistributionInstaller -from mkosi.run import run_with_apivfs +from mkosi.run import bwrap from mkosi.types import PathString from mkosi.util import MkosiState @@ -17,10 +17,10 @@ class OpensuseInstaller(DistributionInstaller): @classmethod def install(cls, state: MkosiState) -> None: - cls.install_packages(state, ["filesystem", "system-user-root"]) + cls.install_packages(state, ["filesystem"], apivfs=False) @classmethod - def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: + def install_packages(cls, state: MkosiState, packages: Sequence[str], apivfs: bool = True) -> None: release = state.config.release if release == "leap": release = "stable" @@ -45,7 +45,7 @@ class OpensuseInstaller(DistributionInstaller): repos += [("repo-update", updates_url)] setup_zypper(state, repos) - invoke_zypper(state, "install", ["-y", "--download-in-advance", "--no-recommends"], packages) + invoke_zypper(state, "install", ["-y", "--download-in-advance", "--no-recommends"], packages, apivfs=apivfs) @classmethod def remove_packages(cls, state: MkosiState, packages: Sequence[str]) -> None: @@ -85,7 +85,13 @@ def setup_zypper(state: MkosiState, repos: Sequence[tuple[str, str]] = ()) -> No ) -def invoke_zypper(state: MkosiState, verb: str, options: Sequence[str], packages: Sequence[str]) -> None: +def invoke_zypper( + state: MkosiState, + verb: str, + options: Sequence[str], + packages: Sequence[str], + apivfs: bool = True +) -> None: cmdline: list[PathString] = [ "zypper", "--root", state.root, @@ -98,5 +104,7 @@ def invoke_zypper(state: MkosiState, verb: str, options: Sequence[str], packages *packages, ] - run_with_apivfs(state, cmdline, - env=dict(ZYPP_CONF=str(state.workspace / "zypp.conf"), KERNEL_INSTALL_BYPASS="1")) + env = dict(ZYPP_CONF=str(state.workspace / "zypp.conf"), KERNEL_INSTALL_BYPASS="1") | state.environment + + bwrap(cmdline, apivfs=state.root if apivfs else None, env=env) + diff --git a/mkosi/run.py b/mkosi/run.py index 11c464b89..eca150f85 100644 --- a/mkosi/run.py +++ b/mkosi/run.py @@ -9,6 +9,7 @@ import shutil import signal import subprocess import sys +import tempfile import traceback from pathlib import Path from types import TracebackType @@ -16,7 +17,7 @@ from typing import Any, Callable, Mapping, Optional, Sequence, Type, TypeVar from mkosi.log import ARG_DEBUG, die from mkosi.types import _FILE, CompletedProcess, PathString, Popen -from mkosi.util import MkosiState, current_user +from mkosi.util import current_user CLONE_NEWNS = 0x00020000 CLONE_NEWUSER = 0x10000000 @@ -219,9 +220,6 @@ def run( LANG="C.UTF-8", ) | env - if env["PATH"] == "": - del env["PATH"] - if "run" in ARG_DEBUG: env["SYSTEMD_LOG_LEVEL"] = "debug" @@ -270,10 +268,10 @@ def spawn( die(f'"{shlex.join(str(s) for s in cmdline)}" returned non-zero exit code {e.returncode}.', e) -def run_with_apivfs( - state: MkosiState, +def bwrap( cmd: Sequence[PathString], - bwrap_params: Sequence[PathString] = tuple(), + *, + apivfs: Optional[Path] = None, stdout: _FILE = None, env: Mapping[str, PathString] = {}, ) -> CompletedProcess: @@ -283,45 +281,41 @@ def run_with_apivfs( "--unshare-pid", "--dev-bind", "/", "/", "--chdir", Path.cwd(), - "--tmpfs", state.root / "run", - "--tmpfs", state.root / "tmp", - "--proc", state.root / "proc", - "--dev", state.root / "dev", - "--ro-bind", "/sys", state.root / "sys", - "--bind", state.var_tmp, state.root / "var/tmp", "--die-with-parent", ] - # If passwd or a related file exists in the root directory, bind mount it over the host files while we - # run the command, to make sure that the command we run uses user/group information from the root instead - # of from the host. If the file doesn't exist yet, mount over /dev/null instead. - for f in ("passwd", "group", "shadow", "gshadow"): - p = state.root / "etc" / f - if p.exists(): - cmdline += ["--bind", p, f"/etc/{f}"] - else: - cmdline += ["--bind", "/dev/null", f"/etc/{f}"] - - cmdline += [ - *bwrap_params, - "sh", "-c", - ] - - env = env | state.environment + if apivfs: + cmdline += [ + "--tmpfs", apivfs / "run", + "--proc", apivfs / "proc", + "--dev", apivfs / "dev", + "--ro-bind", "/sys", apivfs / "sys", + "--bind", "/tmp", apivfs / "tmp", + "--bind", "/var/tmp", apivfs / "var/tmp", + "--bind", "/dev/shm", apivfs / "dev/shm", + ] - template = f"chmod 1777 {state.root / 'tmp'} {state.root / 'var/tmp'} {state.root / 'dev/shm'} && exec {{}} || exit $?" + # If passwd or a related file exists in the apivfs directory, bind mount it over the host files while + # we run the command, to make sure that the command we run uses user/group information from the + # apivfs directory instead of from the host. If the file doesn't exist yet, mount over /dev/null + # instead. + for f in ("passwd", "group", "shadow", "gshadow"): + p = apivfs / "etc" / f + if p.exists(): + cmdline += ["--bind", p, f"/etc/{f}"] + else: + cmdline += ["--bind", "/dev/null", f"/etc/{f}"] try: - return run([*cmdline, template.format(shlex.join(str(s) for s in cmd))], - text=True, stdout=stdout, env=env, log=False) + return run([*cmdline, *cmd], text=True, stdout=stdout, env=env, log=False) except subprocess.CalledProcessError as e: if "run" in ARG_DEBUG: - run([*cmdline, template.format("sh")], check=False, env=env, log=False) + run([*cmdline, "sh"], stdin=sys.stdin, check=False, env=env, log=False) die(f'"{shlex.join(str(s) for s in cmd)}" returned non-zero exit code {e.returncode}.') def run_workspace_command( - state: MkosiState, + root: Path, cmd: Sequence[PathString], bwrap_params: Sequence[PathString] = tuple(), network: bool = False, @@ -333,50 +327,47 @@ def run_workspace_command( "--unshare-ipc", "--unshare-pid", "--unshare-cgroup", - "--bind", state.root, "/", + "--bind", root, "/", "--tmpfs", "/run", - "--tmpfs", "/tmp", "--dev", "/dev", "--proc", "/proc", "--ro-bind", "/sys", "/sys", - "--bind", state.var_tmp, "/var/tmp", + "--bind", "/tmp", "/tmp", + "--bind", "/var/tmp", "/var/tmp", + "--bind", "/dev/shm", "/dev/shm", "--die-with-parent", *bwrap_params, ] - resolve = state.root.joinpath("etc/resolv.conf") + resolve = root.joinpath("etc/resolv.conf") + + tmp = Path(tempfile.NamedTemporaryFile(delete=False).name) if network: # Bubblewrap does not mount over symlinks and /etc/resolv.conf might be a symlink. Deal with this by # temporarily moving the file somewhere else. if resolve.is_symlink(): - shutil.move(resolve, state.workspace / "resolv.conf") + shutil.move(resolve, tmp) # If we're using the host network namespace, use the same resolver cmdline += ["--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf"] else: cmdline += ["--unshare-net"] - cmdline += ["sh", "-c"] - env = dict( container="mkosi", SYSTEMD_OFFLINE=str(int(network)), HOME="/", - # Make sure the default PATH of the distro shell is used. - PATH="", - ) | env | state.environment - - template = "chmod 1777 /tmp /var/tmp /dev/shm && PATH=$PATH:/usr/bin:/usr/sbin exec {} || exit $?" + PATH="/usr/bin:/usr/sbin", + ) | env try: - return run([*cmdline, template.format(shlex.join(str(s) for s in cmd))], - text=True, stdout=stdout, env=env, log=False) + return run([*cmdline, *cmd], text=True, stdout=stdout, env=env, log=False) except subprocess.CalledProcessError as e: if "run" in ARG_DEBUG: - run([*cmdline, template.format("sh")], check=False, env=env, log=False) + run([*cmdline, "sh"], stdin=sys.stdin, check=False, env=env, log=False) die(f'"{shlex.join(str(s) for s in cmd)}" returned non-zero exit code {e.returncode}.') finally: - if state.workspace.joinpath("resolv.conf").is_symlink(): + if tmp.is_symlink(): resolve.unlink(missing_ok=True) - shutil.move(state.workspace.joinpath("resolv.conf"), resolve) + shutil.move(tmp, resolve)