Skip to content

vm-less and daemon-less ELBE build without mknod - #487

Open
koalo wants to merge 24 commits into
Linutronix:masterfrom
koalo:devel/koalo/container-nomknod
Open

koalo wants to merge 24 commits into
Linutronix:masterfrom
koalo:devel/koalo/container-nomknod

Conversation

@koalo

@koalo koalo commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

This supersedes #482 by removing the manual mknod procedures with passing udev and /dev directly into the container for the rootful case.

Also reordered the commits as discussed:

  1. ELBE without initvm (rootful)

fc80da4
54c6257
da13574

  1. Container for ELBE without initvm (rootful)

d532e23
9843820
d7f66a3
707b6c6

  1. Test for 2)

ea2994e
ec47114
b5b9d22

  1. ELBE without initvm (rootless)

No additional commits, but requires to manually unshare if not done via container.

  1. Test XML that allows to build rootless

0095a8d

  1. Container for ELBE without initvm (rootless)

43a09d5
41e12bf
76aa59e
79d2fd0

  1. Test for 6)

d4cabf0

@t-8ch t-8ch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I focused on step 1) for now.
You could also drop the other steps for now and we do one after the other.

Comment thread elbepack/commands/build.py Outdated
Comment thread elbepack/commands/build.py Outdated
Comment thread elbepack/localbuildaction.py Outdated
Comment thread elbepack/localbuildaction.py Outdated
Comment thread elbepack/localbuildaction.py
Comment thread elbepack/asyncworker.py Outdated
Comment thread elbepack/localbuildaction.py Outdated
Comment thread elbepack/repomanager.py
Comment thread elbepack/repomanager.py
Comment thread elbepack/paths.py Outdated
@koalo
koalo force-pushed the devel/koalo/container-nomknod branch 2 times, most recently from 3b2caf8 to 90dc4e3 Compare September 8, 2026 15:39
Comment thread elbepack/repomanager.py
for lic in repo_conf.splitlines():
if lic.startswith('SignWith'):
self.keyid = lic.split()[1]
conf_dist = self.volume.joinpath('conf', 'distributions')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the end we want to have a repository/SBOM for the build environment, even if that wasn't an initvm.
We can probably skip it for now, but will need some solution before making this a stable feature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought of that, but wasn't sure how/if to approach it properly, because I am not so sure if copying the exact same approach from initvm is actually useful for the container use case.

Sure, SBOM generation for the container environment should be done. But for that I guess we should first look how we want to approach SBOM generation for ELBE in the first place (as you mentioned debsbom before).

For the packages, do you think it is worth to assemble the Debian packages from the container standalone like we do for initvm? Wouldn't it be more convenient to just archive the whole container including all the configuration / scripting / ...

@t-8ch t-8ch Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be more convenient to just archive the whole container including all the configuration / scripting /

We'll need a machine-readable description of it, not only the raw artifacts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need a machine-readable description of it, not only the raw artifacts.

Yes, definitely! I meant the "archived container" just as replacement for the binary artifacts of the initvm CD, not as replacement for the SBOM.

Comment thread elbepack/buildsubmitaction.py Outdated
Comment thread elbepack/buildsubmitaction.py Outdated
Comment thread elbepack/buildsubmitaction.py Outdated
Comment thread elbepack/buildsubmitaction.py
Comment thread elbepack/tests/test_rootcheck.py Outdated
Comment thread elbepack/tests/test_rootcheck.py Outdated
Comment thread elbepack/tests/test_rootcheck.py Outdated
Comment thread elbepack/tests/test_rootcheck.py Outdated
Comment thread elbepack/tests/test_xml_initvm.py
@koalo
koalo force-pushed the devel/koalo/container-nomknod branch 3 times, most recently from 3ad19f5 to 0cbcbdb Compare September 30, 2026 13:42
Comment thread elbepack/buildsubmitaction.py
Comment thread elbepack/buildsubmitaction.py
Comment thread elbepack/shellhelper.py Outdated
Comment thread elbepack/shellhelper.py
Comment thread elbepack/tests/test_rootcheck.py Outdated
Comment thread docs/elbe-build.rst Outdated
Comment thread newsfragments/+vmless-build-container.feature.rst Outdated
Comment thread conftest.py Outdated
Comment thread elbepack/tests/test_xml_container.py Outdated
Comment thread elbepack/imgutils.py Outdated
@koalo
koalo force-pushed the devel/koalo/container-nomknod branch from 0cbcbdb to b415f97 Compare October 1, 2026 08:59
Comment thread elbepack/initvmaction.py
Comment thread elbepack/initvmaction.py Outdated
Comment thread elbepack/initvmaction.py
Comment thread elbepack/buildsubmitaction.py Outdated
Comment thread elbepack/initvmaction.py Outdated
Comment thread elbepack/initvmaction.py Outdated
Comment thread elbepack/initvmaction.py Outdated
Comment thread elbepack/initvmaction.py Outdated
Comment thread elbepack/buildsubmitaction.py
Comment thread elbepack/localbuildaction.py Outdated
@koalo
koalo force-pushed the devel/koalo/container-nomknod branch 4 times, most recently from 95af6b2 to 37ead4c Compare October 5, 2026 06:23
Comment thread elbepack/fstab.py
Comment thread elbepack/shellhelper.py Outdated
Comment thread elbepack/buildsubmitaction.py Outdated
Comment thread contrib/containerfile/Containerfile Outdated
Comment thread contrib/containerfile/Containerfile.local
Comment thread docs/article-container.rst
Comment thread elbepack/tests/test_xml_container.py
Comment thread elbepack/tests/test_xml_container.py Outdated
Comment thread contrib/containerfile/entrypoint.sh Outdated
Comment thread conftest.py
' RAM. Use --basetemp=<dir> or set TMPDIR to select another'
' location.',
)
os.environ['TMPDIR'] = str(var_tmp)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I am aware that I asked for this, it is quite complicated...
Maybe we test for the free space instead? That could be done easily in pure python.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. If you really want it, I can of course implement it. The change for that would be small (see below). But I am not really sure if just checking for the amount of free space is the correct approach. I see two problems with that

  1. It introduces an arbitrary constant. How could we know how much space we really need?
  2. Even if there is enough space in the RAM for the full build, it still leads to excessive RAM usage, leaving fewer RAM for the build as well as parallel processes. My initial issue that triggered the idea to move to /var/tmp was actually not that the build was failing, but that my browser that I had open in parallel got unusable slow. We can then of course keep some buffer, but again, how much buffer is sufficient?
--- a/conftest.py
+++ b/conftest.py
@@ -2,10 +2,12 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 # SPDX-FileCopyrightText: 2024 Linutronix GmbH
 
 import os
-import subprocess
+import shutil
 import tempfile
 from pathlib import Path
 
 import pytest
 
+# Minimum free space required in the temp dir for large build artifacts.
+MIN_TMP_FREE = 20 * 1024**3
 
 # https://stackoverflow.com/a/61193490
 def pytest_addoption(parser):
@@ -22,12 +24,6 @@
     )
 
 
-def _is_ram_backed(path):
-    fstype = subprocess.run(
-        ['stat', '-f', '-c', '%T', path],
-        capture_output=True, text=True, check=False,
-    ).stdout.strip()
-    return fstype in ('tmpfs', 'ramfs')
-
-
 def pytest_configure(config):
     config.addinivalue_line('markers', 'slow: mark test as slow to run')
@@ -46,19 +42,22 @@
         os.environ.setdefault('PYTHONWARNINGS', ' '.join(warnings))
 
-    # Avoid filling up the RAM with large build artifacts: unless the user
-    # chose a location, move away from the default temp dir if it is RAM-backed.
+    # Avoid filling up the temp dir (often RAM-backed) with large build
+    # artifacts: unless the user chose a location, move away from the default
+    # temp dir if it has too little free space.
     if 'TMPDIR' not in os.environ and not config.option.basetemp:
-        if _is_ram_backed(Path(tempfile.gettempdir())):
+        if shutil.disk_usage(tempfile.gettempdir()).free < MIN_TMP_FREE:
             var_tmp = Path('/var/tmp')
-            if not var_tmp.is_dir() or not os.access(var_tmp, os.W_OK | os.X_OK):
+            if (
+                not var_tmp.is_dir()
+                or not os.access(var_tmp, os.W_OK | os.X_OK)
+                or shutil.disk_usage(var_tmp).free < MIN_TMP_FREE
+            ):
                 raise pytest.UsageError(
-                    f'{tempfile.gettempdir()} is RAM-backed and {var_tmp} is not a'
-                    ' writable directory. Large build artifacts would fill up the'
-                    ' RAM. Use --basetemp=<dir> or set TMPDIR to select another'
-                    ' location.',
+                    f'{tempfile.gettempdir()} has less than'
+                    f' {MIN_TMP_FREE // 1024**3} GiB free and {var_tmp} is not a'
+                    ' writable directory with enough free space. Use'
+                    ' --basetemp=<dir> or set TMPDIR to select another location.',
                 )
             os.environ['TMPDIR'] = str(var_tmp)
             # tempfile has cached the old location by now, so reset
             tempfile.tempdir = None

Comment thread elbepack/localbuildaction.py Outdated

class LocalProjectBackend(ProjectBackend):
def __init__(self, cache_dir, outdir):
self.pm = ProjectManager(cache_dir)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The database is still stored in /var/cache/elbe. Can we move it into the cache_dir parameter here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored the whole handling around the cache dir. This was also needed for native builds (because it otherwise resulted in too long socket paths with the gpg agent).

koalo added 3 commits October 15, 2026 19:50
The user name, full name, password and email are not used anymore since
the user table has been removed. Keep accepting the corresponding
options of 'elbe db init' for compatibility, but ignore them.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
If the directory of the database can not be created, only an error
message was printed and 'elbe db init' still exited successfully, so a
missing database went unnoticed. Let the exception propagate instead, as
the other database commands do.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Only 'elbe db init' created the directory of the database, so every
other user of ElbeDB failed with a cryptic sqlite error if it did not
exist yet. Create the directory whenever the database is opened.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
@koalo
koalo force-pushed the devel/koalo/container-nomknod branch from 37ead4c to d36335a Compare October 16, 2026 13:00
koalo added 5 commits October 16, 2026 15:16
The update and repository paths, as well as the update scripts, refer
to the deployed target system or to the inside of the rootfs that is
built, not to the cache directory of the build system. The same holds
for the directories the packages for the cdroms are downloaded to. They
are inside of the rootfs, but were named like the directories that are
used for the packages of the initvm.

Prefix them with TARGET_ like TARGET_GNUPG_HOME already is and derive
them from a common TARGET_CACHE_DIR, so they are not mistaken for host
paths. Name the actual directories of the initvm INITVM_ instead, like
the other paths of the initvm.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
The host-side locations below /var/cache/elbe were module level
constants, so every user was bound to the system wide cache directory.
That is fine for the initvm, but prevents running builds that should not
depend on or modify system directories.

Collect these locations in a CacheDirs class that is instantiated with
a root directory and hand it to ElbeDB, ProjectManager, ElbeProject and
the cdrom generation. The default instance DEFAULT_CACHE_DIRS keeps the
current behavior.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
ElbeDB created a sqlalchemy engine that was never disposed, so the pooled
sqlite connections were closed by the garbage collector at an arbitrary
time. This can result in a ResourceWarning.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
The GnuPG home of a project is always located inside the build directory.
Since the gpg-agent socket is located inside the GnuPG home, this breaks
for deeply nested build directories, because the length of UNIX socket
paths is limited to 107 bytes. GnuPG works around that by placing the
socket below /run/user/<uid>, but this does not exist without a login
session, e.g. when building as root via sudo.

Add an optional gnupg_root to CacheDirs and resolve the GnuPG home of the
project through CacheDirs.project_gnupg_home() instead of assembling
<builddir>/gnupg at each place. Without a gnupg_root, the paths stay
unchanged.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Parse the options of an fstab entry into a list instead of keeping the
comma separated string. This allows to extend the options without string
handling, which is needed for the upcoming rbind support in the mount
helper, and removes the need to split them again when evaluating them.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
@koalo
koalo force-pushed the devel/koalo/container-nomknod branch from d36335a to 03eb649 Compare October 16, 2026 13:18
Hooks from the host system (e.g. apt-listchanges) should not be applied
in the target chroot.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
koalo added 9 commits October 16, 2026 15:21
VirtApt modifies the process-global apt_pkg.config (Dir::*, APT::*) to point
into its temporary directory but never reverted it. Remember the original
values and restore them in delete().

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Add a new top-level "elbe build" command as alternative
to "elbe initvm submit". It drives the project manager directly
in-process, without any daemon or SOAP communication. It is meant to
be used in an environment that already provides the required
isolation.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
…ectory

The VM-free build is a one-shot operation and nothing needs the internal
signing key once the build finished. Nevertheless, its GnuPG home was
located inside the build directory, so the agent socket path exceeded the
limit for UNIX socket paths for deeply nested build directories and the
build failed with "gpgme_op_genkey: GnuPG: No agent running".

Keep the GnuPG homes of the LocalProjectBackend below a short temporary
directory instead. On stop(), terminate the agents explicitly and remove
the directory, also in case of a failed build.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
If CAP_SYS_ADMIN is missing, re-execute elbe build in a new user and
mount namespace. This gives sufficient permissions to perform the
(bind) mounts needed for the build process, so running it
unprivileged works without any explicit capability.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Support rbind mounts with the mount helper.
This is needed to rbind pseudo filesystems in an upcoming commit.

For umounting the rbind again, we need a --lazy, because
there might be submounts still busy. We anyway were ignoring the
result of the umount already before (check=False), so we are not
depending on an immediate clean umount and can defer it to the kernel
instead to do it in a lazy way.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
…cratch

Several operations require a chroot with the pseudo filesystems (/proc,
/sys and /dev) in it. Inside a container, the container runtime already
protects parts of it (like /proc/kcore) by overlaying it with a null
bind mount. Doing a fresh mount from scratch (e.g. mount -t proc None foo)
inside a completely new mount and user namespace, would expose the
content again, so the kernel has explicit procection against it
(-> mount_too_revealing).

But we do not need fresh pseudo filesystems. We can just rebind what we
already have. The only issue with that is that we need to scope it more
tightly around the operations that actually require this setup with
chroot and pseudo filesystems, otherwise we would fill up the newly
created filesystem with stuff from the pseudo filesystems we do not
need.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Several actions require elevated privileges for losetup or mknod.
Instead of starting the build and then later failing with an error
message, check beforehand for critical elements in the XML and print
a respective error message.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
/tmp is usually RAM-backed, so when putting build artifacts there, it
will fill up very quickly and exhaust the RAM. Therefore, use /var/tmp
per default. On Debian systems /var/tmp will still follow a retention
policy, so any potential leftovers will be removed after 30 days
(in the default configuration).

On systems where /tmp is not RAM-backed, still use it.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Add a build_driver fixture that hides how the XML files under test are
built, so we can in future support multiple different drivers like
initvm and container based.

Use absolute paths since native builds run in-process and the build
changes the working directory of the pytest process (e.g. asyncworker
chdir('/')). Relative XML paths used by later tests then fail to load.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
@koalo
koalo force-pushed the devel/koalo/container-nomknod branch 2 times, most recently from 9432645 to 4d50acb Compare October 16, 2026 13:38
koalo added 6 commits October 16, 2026 15:49
Split the test_xml to only contain code that is not directly related to
the initvm. This will enable us in the next step to add further tests
that will use the container-based approach.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Build a simple image without a bootloader to demonstrate
an ELBE build that does not require elevated privileges.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
…without udev

Setup UUID as needed e.g. for grub, because that is usually also done by udev.
So without udev, we need a replacement for that functionality.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Run the XML build tests by calling elbe build directly on the host
instead of going through an initvm, using the build_driver abstraction.
Tests that need root privileges are skipped when not run as root.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
With the recent additions, it is no longer necessary to spawn an initvm
to build ELBE images. Therefore, provide a container that enables this
use case and replace the existing legacy containerfile with it.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
Provide a test driver to run the same tests as for the initvm
and native for the container-based build as well.

Detect whether the XML under test actually needs a rootful
container by checking xml_needs_rootful(). When it does not,
run the container rootless via --userns auto instead of
requiring elevated privileges, and only skip the test when a
rootful container is required and the tests are not run as root.

Signed-off-by: Florian Kauer <florian.kauer@linutronix.de>
@koalo
koalo force-pushed the devel/koalo/container-nomknod branch from 4d50acb to 2596a54 Compare October 16, 2026 13:50
Comment thread elbepack/virtapt.py
def __init__(self, xml):

self.xml = xml
self._saved_config = {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you mention in the commit message why we need this?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants