Scripts currently run chrooted in the image. This is not great because
it means the tool you want to run from a script has to be installed in
the image, even if it has --root support to run from outside the image.
Specifically, to install extra packages from a script, you currently
have to install the package manager inside the image itself. Even then,
it might be completely different than the package manager on the host
(both in version and in build options), possibly leading to all kinds
of weird issues.
To allow users to install extra packages from scripts, we now default
to running scripts on the host. Additionally, to make life easy for
users, we provide a set of scripts in the PATH for the package managers
we support that call them with the necessary options to install packages
in the same way mkosi installs packages into the root directory. This
means all that users have to do is "dnf install xxx" from the script to
install a new package into the root directory.
Because some tools do not have a --root option and to provide an easy
migration for users that depend on scripts running in the image, we
also put a script "mkosi-chroot" in the PATH which uses bubblewrap to chroot
into the image as we did before. Users can keep their old scripts working
by simply adding the following to the top of their script:
```
if [ "$container" != "mkosi" ]; then
exec mkosi-chroot $SCRIPT "$@"
fi
```
When running scripts on the host, no APIVFS directories are mounted into
the image. When using the "mkosi-chroot" and package manager scripts, the APIVFS
directories are automatically mounted before executing the corresponding
command. The apivfs_cmd() function is introduced to make implementing this
easier.
Additionally, we now always consider the current working directory a
BuildSources= directory. BuildSources= is now used to declare additional
source directories. The current working directory can always be overmounted
with another directory by simply specifying a source directory in BuildSources=
without a target directory.
To allow scripts running on the host to find the image, we set the
BUILDROOT variable for all scripts.
To prevent scripts on the host from messing with the host system when mkosi
is running as root, we extend the sandboxing to cover many more directories
which are all mounted read-only while a script is executing.
This change also allows scripts to be written in python or other scripting
languages without having to install python into the image itself.
Daan De Meyer [Wed, 2 Aug 2023 11:20:38 +0000 (13:20 +0200)]
Make sure outputs are always prefixed with the output name
Our output unlinking logic currently works based off the name of
the output. Everything that is prefixed with the output name is
removed. We need to fix that properly, but for now, let's make sure
that all outputs that we generate are prefixed with the name of the
output.
Let's make the following changes
- Instead of having an instance of the installer object stored in
MkosiState, let's have an installer() method on the Distribution
enum that returns the specific type implementing the distribution
- Let's move Distribution to mkosi.distributions
- Let's add a method to DistributionInstaller to return the package
type instead of storing it directly in the Distribution enum
- Let's add methods to the Distribution enum that delegate to its
installer type
- Use enum.auto() instead of specifying values directly.
Let's leave it to bwrap() to set up any sandboxing that we need.
Let's also add a bit more sandboxing to bwrap(), to avoid details
from the host accidentally leaking into the image builds.
This was removed when we had a brief experiment with running everything
with bwrap(), but since we don't do that anymore, let's add this back
to make chroot detection work again.
I initially added this because no shell is pulled into the initrd
by default on gentoo when just installing systemd and util-linux.
Since this seems to override other distro's default shell, let's
limit this to just gentoo and rely on other distros pulling in a
shell via dependencies.
- Drop --update, we never want to update packages, only install new
ones
- Drop --changed-use and --new-use, these apply to updating an existing
system, we're always installing a new one
- Drop --deep and --complete-graph-if-new-use, these don't seem to
have any noticeable effect
- Move getbinpkg from FEATURE to emerge CLI option --getbinpkg
- Do not disable spinner and candy option
- Explicitly disable all sandboxing features
This function is intended to be a more efficient way of moving trees
than shutil.move(). Specifically, it will use reflinks and btrfs
snapshots if possible to make copying more efficient.
Rename btrfs.py to tree.py and move copy_path() into it
Let's hide btrfs subvolumes as an implementation detail in a new
tree module. We have two functions, make_tree() and copy_tree()
(renamed from copy_path()), which will both take advantage of btrfs
subvolumes if configured to do so.
We also extend copy_tree() to not try to snapshot files.
gentoo: Set FEATURES by appending to /etc/portage/make.conf
Setting via the environment variable doesn't work in all cases, so
let's append to /etc/portage/make.conf instead. This allows us to
get rid of the custom devtpms for portage as it doesn't try to chown
tty's anymore now that userpriv is actually disabled.
Drop filesystem_options() and make xfs default for centos again
Requires https://github.com/systemd/systemd/pull/26541 but after that
building XFS root filesystems will work reliably again and we can use
xfs as the default filesystem for centos which removes the need for
filesystem_options().
gentoo: Use bwrap() instead of run_workspace_command() to run emerge
Similar to the other distros, run the package manager with bwrap()
instead of run_workspace_command(). Also disable the sandboxing of
emerge as we do it in mkosi already.
This also reworks the mirror specification for centos and related
distros to duplicate less code. gpgurls of the Repo() struct is also
made into a tuple instead of a list.
- Instead of forking, use spawn() to start the newuidmap, newgidmap
processes early
- Instead of multiprocessing.Event(), use flock to handle the necessary
locking
- Add some more information on what we're doing
- Move running modinfo and calculating dependencies out of the files()
generator so that the "Making cpio" step doesn't include running modinfo
and calculating the dependencies.
Let's run setfiles on the host instead of inside the image. To make
this work, we have to explicitly tell it to use the binary policy
from the image to check contexts against.
Rework user/mount namespace handling and tools tree
The biggest change is that instead of making bwrap() responsible
for mounting the tools tree, we do it ourselves before we build/boot
each image. We do the same for remounting the top level directories
read-only, instead of leaving it to bwrap(), we do it once at the
start of run_verb(). Because we now mess with the host system mounts
ourselves again, we also go back to unconditionally unsharing a mount
namespace, even when running as root.
With the above out of the way, there's no real reason left to run
regular executables with bwrap(), so those are moved back to be
executed using run(). The above changes also remove the need for
bwrap_cmd(), so it is merged back with bwrap() again.
One nasty caveat of overmounting /usr ourselves at the start of
execution is that some python modules are loaded dynamically and we
need to make sure this has happened before we start overmounting /usr.
Finally, this commit also gets rid of running the image build in a
subprocess. Instead, after doing the build and doing the final tools
tree mount for the image we're going to boot/qemu/ssh into, if we're
going to do an unprivleged operation, we change uid/gid to the invoking
user. This is more or less the same as running these operations unprivileged
outside of the user namespace.
For boot/shell, these only run privileged, so we check beforehand
that we're running as root, and this doesn't change after become_root(),
so since we're just root all the time, there's no need to run the image
build in a subprocess.
To keep ssh working, we have to trick it into recognizing our user in
the user namespace by overmounting /etc/passwd with a file containing
an entry for the mapped user uid.
We also unify more of the uid/gid handling in run_verb() in general.
Allow overriding the python interpreter used by mkosi
e.g. on CentOS 8 python3 is python 3.6. python 3.9 can be installed,
but it'll be installed as python3.9, so we need a way to override the
interpreter used by mkosi.
systemd on Fedora prefers to install util-linux-core instead of
util-linux which doesn't ship sulogin which makes starting
emergency.service enter an infinite loop. Let's make sure util-linux
is installed so sulogin is available in the initrd.
Add scripts support to bwrap() and use it for chrooting
The new scripts argument to bwrap() allows providing a mapping of
script names to command lines. Each of these becomes a script in a
directory that's prepended to PATH before executing the command
given to bwrap(). This allows us to make extra commands available
to scripts we execute with bwrap().
The first usage of this is to replace the extra argument which is
used to provide the extra arguments to chroot into the root directory.
Instead, we provide a chroot script and use that in the command line
we pass to bwrap() to perform the chroot.
Implement run_workspace_command() on top of bwrap()
Instead of duplicating the apivfs setup between bwrap() and
run_workspace_command(), let's make bwrap() responsible for setting
up the apivfs and implement chrooting on top of it. Specifically,
to chroot, we just run bwrap nested to do the chroot and any
additional setup before running the actual command. This is implemented
in the new chroot_cmd() function which returns the command to do the
chroot. This also sets the stage for running scripts on the host and
somehow providing the chroot_cmd() command to scripts to use themselves
to run something inside the root directory.
To keep --debug-shell working smoothly for scripts, we pass the chroot
command as a new extra argument to bwrap() so we can also apply it when
starting the debug shell.
We also rework the /etc/resolv.conf bind mounting so we don't have
to do any cleanup after the command finishes.
Replace kwargs from bwrap with individual arguments
kwargs makes it impossible for mypy to point out wrong arguments
so let's drop it in favor of individual arguments to get more out
of the type checker.
It only has to be in front of the cmd specific options, which in
our case was only --allowerasing, so since that's gone now, let's
put the command last again.
logdir= is always taken relative to the installroot. Let's leave it
at it's default value and remove any log files from the install root
after running dnf.