Repository: osbuild/osbuild
Pull request #248 (2020-02-29 11:02:55)

Add blank runner
This adds a new runner called `org.osbuild.blank`, which assumes /usr
is pre-populated and ready to go. It does not perform any OS setup. It
only initializes the environment and executes the stage.

This runner allows easy bootstrapping of new systems. It assumes our
ideal setup where `/usr` describes a host system in its entirety,
without any local policy applied. Thus, this runner is also what we
ultimately want to work towards as a default. This might not happen
anytime soon, though, given how `passwd`, `ldconfig`, `nss`, etc. still
depend on prepopulated caches in `/etc`.


Patch:
diff --git a/osbuild/pipeline.py b/osbuild/pipeline.py
index 50b3c9f1..6fda954e 100644
--- a/osbuild/pipeline.py
+++ b/osbuild/pipeline.py
@@ -302,31 +302,36 @@ class Pipeline:
         return results
 
 
-def detect_os(*paths):
-    """Detect the os from an os-release file.
+def describe_os(*paths):
+    """Read the Operating System Description from `os-release`
 
-    The first file in 'paths' is used and interpreted like a os-release(5)
-    file.
+    This creates a string describing the running operating-system name and
+    version. It reads the information from the path array provided as `paths`.
+    The first available file takes precedence. It must be formatted according
+    to the rules in `os-release(5)`.
 
-    Returns ID + VERSION_ID (without dots), which is the same format that
-    runners are named as.
+    The returned string uses the format `${ID}${VERSION_ID}` with all dots
+    stripped.
     """
+    osrelease = {}
+
     path = next((p for p in paths if os.path.exists(p)), None)
-    if not path:
-        raise FileNotFoundError("none of the specified os-release files exist")
+    if path:
+        with open(path) as f:
+            for line in f:
+                line = line.strip()
+                if not line:
+                    continue
+                if line[0] == "#":
+                    continue
+                key, value = line.split("=", 1)
+                osrelease[key] = value.strip('"')
 
-    with open(path) as f:
-        osrelease = {}
-        for line in f:
-            line = line.strip()
-            if not line:
-                continue
-            if line[0] == "#":
-                continue
-            key, value = line.split("=", 1)
-            osrelease[key] = value.strip('"')
+    # Fetch `ID` and `VERSION_ID`. Defaults are defined in `os-release(5)`.
+    osrelease_id = osrelease.get("ID", "linux")
+    osrelease_version_id = osrelease.get("VERSION_ID", "")
 
-    return osrelease["ID"] + osrelease["VERSION_ID"].replace(".", "")
+    return osrelease_id + osrelease_version_id.replace(".", "")
 
 
 def load_build(description, sources_options):
@@ -344,7 +349,7 @@ def load(description, sources_options):
     if build:
         build_pipeline, runner = load_build(build, sources_options)
     else:
-        build_pipeline, runner = None, "org.osbuild." + detect_os("/etc/os-release", "/usr/lib/os-release")
+        build_pipeline, runner = None, "org.osbuild." + describe_os("/etc/os-release", "/usr/lib/os-release")
 
     pipeline = Pipeline(runner, build_pipeline)
 
diff --git a/runners/org.osbuild.blank b/runners/org.osbuild.blank
new file mode 100755
index 00000000..b58ec583
--- /dev/null
+++ b/runners/org.osbuild.blank
@@ -0,0 +1,38 @@
+#!/usr/bin/python3
+
+import array
+import json
+import os
+import socket
+import subprocess
+import sys
+
+
+def load_fds(sock, msglen):
+    fds = array.array("i")   # Array of ints
+    msg, ancdata, _, addr = sock.recvmsg(msglen, socket.CMSG_LEN(253 * fds.itemsize))
+    for cmsg_level, cmsg_type, cmsg_data in ancdata:
+        if (cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS):
+            # Append data, ignoring any truncated integers at the end.
+            fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
+    return json.loads(msg), list(fds), addr
+
+def setup_stdio():
+    with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
+        sock.connect("/run/osbuild/api/osbuild")
+        req = {'method': 'setup-stdio'}
+        sock.send(json.dumps(req).encode('utf-8'))
+        msg, fds, _ = load_fds(sock, 1024)
+        for io in ['stdin', 'stdout', 'stderr']:
+            target = getattr(sys, io)
+            source = fds[msg[io]]
+            os.dup2(source, target.fileno())
+            os.close(source)
+
+
+if __name__ == "__main__":
+    setup_stdio()
+
+    r = subprocess.run(sys.argv[1:], check=False)
+    sys.exit(r.returncode)


Test patch:
diff --git a/test/os-release/arch b/test/os-release/arch
new file mode 100644
index 00000000..560b8ae4
--- /dev/null
+++ b/test/os-release/arch
@@ -0,0 +1,10 @@
+NAME="Arch Linux"
+PRETTY_NAME="Arch Linux"
+ID=arch
+BUILD_ID=rolling
+ANSI_COLOR="0;36"
+HOME_URL="https://www.archlinux.org/"
+DOCUMENTATION_URL="https://wiki.archlinux.org/"
+SUPPORT_URL="https://bbs.archlinux.org/"
+BUG_REPORT_URL="https://bugs.archlinux.org/"
+LOGO=archlinux
diff --git a/test/os-release/linux b/test/os-release/linux
new file mode 100644
index 00000000..e69de29b
diff --git a/test/test_osrelease.py b/test/test_osrelease.py
index 1c8578cd..5d687399 100644
--- a/test/test_osrelease.py
+++ b/test/test_osrelease.py
@@ -5,13 +5,14 @@ import osbuild
 
 class TestOSRelease(unittest.TestCase):
     def test_non_existant(self):
-        self.assertRaises(FileNotFoundError, osbuild.pipeline.detect_os, "💩")
+        """Verify default os-release value, if no files are given."""
+        self.assertEqual(osbuild.pipeline.describe_os(), "linux")
 
-    def test_detect_os(self):
+    def test_describe_os(self):
         """Test host os detection. test/os-release contains the os-release files
         for all supported runners.
         """
         for entry in os.scandir("test/os-release"):
             with self.subTest(entry.name):
-                self.assertEqual(osbuild.pipeline.detect_os(entry.path), entry.name)
+                self.assertEqual(osbuild.pipeline.describe_os(entry.path), entry.name)