]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
portable: add PORTABLE_NAME_AND_VERSION= and other metadata to LogsExtraFields=
authorLuca Boccassi <bluca@debian.org>
Thu, 23 Mar 2023 01:23:04 +0000 (01:23 +0000)
committerLuca Boccassi <bluca@debian.org>
Tue, 28 Mar 2023 11:14:21 +0000 (12:14 +0100)
This is useful to identify log messages with metadata from the images
they run on. Look for ID/VERSION_ID/IMAGE_ID/IMAGE_VERSION/BUILD_ID,
with a SYSEXT_ prefix if we are looking at an extension, and append via
LogExtraFields= as respectively PORTABLE_NAME_AND_VERSION= in case of a
single image. In case of extensions, append as PORTABLE_ROOT_NAME_AND_VERSION=
for the base and one PORTABLE_EXTENSION_AND_VERSION= for each extension.

Example with a base and two extensions, with the unit coming from the
first extension:

[Service]
RootImage=/home/bluca/git/systemd/base.raw
Environment=PORTABLE=app0.raw
BindReadOnlyPaths=/etc/os-release:/run/host/os-release
LogExtraFields=PORTABLE=app0.raw
Environment=PORTABLE_ROOT=base.raw
LogExtraFields=PORTABLE_ROOT=base.raw
LogExtraFields=PORTABLE_ROOT_NAME_AND_VERSION=debian_10

ExtensionImages=/home/bluca/git/systemd/app0.raw
LogExtraFields=PORTABLE_EXTENSION=app0.raw
LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app_0

ExtensionImages=/home/bluca/git/systemd/app1.raw
LogExtraFields=PORTABLE_EXTENSION=app1.raw
LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app_1

docs/PORTABLE_SERVICES.md
src/portable/portable.c
test/test-functions
test/units/testsuite-29.sh

index 7936eebccb293065369154966ece12e5a8ceed6d..8d65c9002d0a361d990275eaf616a78009bd0b82 100644 (file)
@@ -345,15 +345,33 @@ was loaded from. In case extensions are used, additionally there will be a
 (i.e.: `RootImage=` or `RootDirectory=`), and one `PORTABLE_EXTENSION=` field per
 each extension image used.
 
+The `os-release` file from the portable image will be parsed and added as structured
+metadata to the journal log entries. The parsed fields will be the first ID field which
+is set from the set of `IMAGE_ID` and `ID` in this order of preference, and the first
+version field which is set from a set of `IMAGE_VERSION`, `VERSION_ID`, and `BUILD_ID`
+in this order of preference. The ID and version, if any, are concatenated with an
+underscore (`_`) as separator. If only either one is found, it will be used by itself.
+The field will be named `PORTABLE_NAME_AND_VERSION=`.
+
+In case extensions are used, the same fields in the same order are, but prefixed by
+`SYSEXT_`, are parsed from each `extension-release` file, and are appended to the
+journal as log entries, using `PORTABLE_EXTENSION_NAME_AND_VERSION=` as the field
+name. The base layer's field will be named `PORTABLE_ROOT_NAME_AND_VERSION=` instead
+of `PORTABLE_NAME_AND_VERSION=` in this case.
+
 For example, a portable service `app0` using two extensions `app0.raw` and
-`app1.raw`, and a base layer `base.raw`, will create log entries with the
-following fields:
+`app1.raw` (with `SYSEXT_ID=app`, and `SYSEXT_VERSION_ID=` `0` and `1` in their
+respective extension-releases), and a base layer `base.raw` (with `VERSION_ID=10` and
+`ID=debian` in `os-release`), will create log entries with the following fields:
 
 ```
 PORTABLE=app0.raw
 PORTABLE_ROOT=base.raw
+PORTABLE_ROOT_NAME_AND_VERSION=debian_10
 PORTABLE_EXTENSION=app0.raw
+PORTABLE_EXTENSION_NAME_AND_VERSION=app_0
 PORTABLE_EXTENSION=app1.raw
+PORTABLE_EXTENSION_NAME_AND_VERSION=app_1
 ```
 
 ## Links
index d19253debb6588cda27e403b00b114c6d52fd0ab..dbebebf4cefb77c568868d2c92886a5682798b41 100644 (file)
@@ -940,11 +940,67 @@ static int make_marker_text(const char *image_path, OrderedHashmap *extension_im
         return 0;
 }
 
+static int append_release_log_fields(
+                char **text,
+                const PortableMetadata *release,
+                ImageClass type,
+                const char *field_name) {
+
+        static const char *const field_versions[_IMAGE_CLASS_MAX][4]= {
+                 [IMAGE_PORTABLE] = { "IMAGE_VERSION", "VERSION_ID", "BUILD_ID", NULL },
+                 [IMAGE_EXTENSION] = { "SYSEXT_IMAGE_VERSION", "SYSEXT_VERSION_ID", "SYSEXT_BUILD_ID", NULL },
+        };
+        static const char *const field_ids[_IMAGE_CLASS_MAX][3]= {
+                 [IMAGE_PORTABLE] = { "IMAGE_ID", "ID", NULL },
+                 [IMAGE_EXTENSION] = { "SYSEXT_IMAGE_ID", "SYSEXT_ID", NULL },
+        };
+        _cleanup_strv_free_ char **fields = NULL;
+        const char *id = NULL, *version = NULL;
+        int r;
+
+        assert(IN_SET(type, IMAGE_PORTABLE, IMAGE_EXTENSION));
+        assert(!strv_isempty((char *const *)field_ids[type]));
+        assert(!strv_isempty((char *const *)field_versions[type]));
+        assert(field_name);
+        assert(text);
+
+        if (!release)
+                return 0; /* Nothing to do. */
+
+        r = load_env_file_pairs_fd(release->fd, release->name, &fields);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to parse '%s': %m", release->name);
+
+        /* Find an ID first, in order of preference from more specific to less specific: IMAGE_ID -> ID */
+        id = strv_find_first_field((char *const *)field_ids[type], fields);
+
+        /* Then the version, same logic, prefer the more specific one */
+        version = strv_find_first_field((char *const *)field_versions[type], fields);
+
+        /* If there's no valid version to be found, simply omit it. */
+        if (!id && !version)
+                return 0;
+
+        if (!strextend(text,
+                       "LogExtraFields=",
+                       field_name,
+                       "=",
+                       strempty(id),
+                       id && version ? "_" : "",
+                       strempty(version),
+                       "\n"))
+                return -ENOMEM;
+
+        return 0;
+}
+
 static int install_chroot_dropin(
                 const char *image_path,
                 ImageType type,
                 OrderedHashmap *extension_images,
+                OrderedHashmap *extension_releases,
                 const PortableMetadata *m,
+                const PortableMetadata *os_release,
                 const char *dropin_dir,
                 PortableFlags flags,
                 char **ret_dropin,
@@ -994,16 +1050,30 @@ static int install_chroot_dropin(
                                "LogExtraFields=PORTABLE=", base_name, "\n"))
                         return -ENOMEM;
 
-                if (!ordered_hashmap_isempty(extension_images)) {
+                /* If we have a single image then PORTABLE= will point to it, so we add
+                 * PORTABLE_NAME_AND_VERSION= with the os-release fields and we are done. But if we have
+                 * extensions, PORTABLE= will point to the image where the current unit was found in. So we
+                 * also list PORTABLE_ROOT= and PORTABLE_ROOT_NAME_AND_VERSION= for the base image, and
+                 * PORTABLE_EXTENSION= and PORTABLE_EXTENSION_NAME_AND_VERSION= for each extension, so that
+                 * all needed metadata is available. */
+                if (ordered_hashmap_isempty(extension_images))
+                        r = append_release_log_fields(&text, os_release, IMAGE_PORTABLE, "PORTABLE_NAME_AND_VERSION");
+                else {
                         _cleanup_free_ char *root_base_name = NULL;
 
                         r = path_extract_filename(image_path, &root_base_name);
                         if (r < 0)
                                 return log_debug_errno(r, "Failed to extract basename from '%s': %m", image_path);
 
-                        if (!strextend(&text, "LogExtraFields=PORTABLE_ROOT=", root_base_name, "\n"))
+                        if (!strextend(&text,
+                                       "Environment=PORTABLE_ROOT=", root_base_name, "\n",
+                                       "LogExtraFields=PORTABLE_ROOT=", root_base_name, "\n"))
                                 return -ENOMEM;
+
+                        r = append_release_log_fields(&text, os_release, IMAGE_PORTABLE, "PORTABLE_ROOT_NAME_AND_VERSION");
                 }
+                if (r < 0)
+                        return r;
 
                 if (m->image_path && !path_equal(m->image_path, image_path))
                         ORDERED_HASHMAP_FOREACH(ext, extension_images) {
@@ -1028,6 +1098,18 @@ static int install_chroot_dropin(
                                                 * stacking multiple images, so list those too. */
                                                "LogExtraFields=PORTABLE_EXTENSION=", extension_base_name, "\n"))
                                         return -ENOMEM;
+
+                                /* Look for image/version identifiers in the extension release files. We
+                                 * look for all possible IDs, but typically only 1 or 2 will be set, so
+                                 * the number of fields added shouldn't be too large. We prefix the DDI
+                                 * name to the value, so that we can add the same field multiple times and
+                                 * still be able to identify what applies to what. */
+                                r = append_release_log_fields(&text,
+                                                              ordered_hashmap_get(extension_releases, ext->name),
+                                                              IMAGE_EXTENSION,
+                                                              "PORTABLE_EXTENSION_NAME_AND_VERSION");
+                                if (r < 0)
+                                        return r;
                         }
         }
 
@@ -1117,7 +1199,9 @@ static int attach_unit_file(
                 const char *image_path,
                 ImageType type,
                 OrderedHashmap *extension_images,
+                OrderedHashmap *extension_releases,
                 const PortableMetadata *m,
+                const PortableMetadata *os_release,
                 const char *profile,
                 PortableFlags flags,
                 PortableChange **changes,
@@ -1161,7 +1245,7 @@ static int attach_unit_file(
          * is reloaded while we are creating things here: as long as only the drop-ins exist the unit doesn't exist at
          * all for PID 1. */
 
-        r = install_chroot_dropin(image_path, type, extension_images, m, dropin_dir, flags, &chroot_dropin, changes, n_changes);
+        r = install_chroot_dropin(image_path, type, extension_images, extension_releases, m, os_release, dropin_dir, flags, &chroot_dropin, changes, n_changes);
         if (r < 0)
                 return r;
 
@@ -1313,7 +1397,8 @@ int portable_attach(
                 size_t *n_changes,
                 sd_bus_error *error) {
 
-        _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL;
+        _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL;
+        _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
         _cleanup_hashmap_free_ Hashmap *unit_files = NULL;
         _cleanup_(lookup_paths_free) LookupPaths paths = {};
         _cleanup_strv_free_ char **valid_prefixes = NULL;
@@ -1329,8 +1414,8 @@ int portable_attach(
                         /* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT),
                         &image,
                         &extension_images,
-                        /* extension_releases= */ NULL,
-                        /* os_release= */ NULL,
+                        &extension_releases,
+                        &os_release,
                         &unit_files,
                         &valid_prefixes,
                         error);
@@ -1397,8 +1482,8 @@ int portable_attach(
                 }
 
         HASHMAP_FOREACH(item, unit_files) {
-                r = attach_unit_file(&paths, image->path, image->type, extension_images,
-                                     item, profile, flags, changes, n_changes);
+                r = attach_unit_file(&paths, image->path, image->type, extension_images, extension_releases,
+                                     item, os_release, profile, flags, changes, n_changes);
                 if (r < 0)
                         return sd_bus_error_set_errnof(error, r, "Failed to attach unit '%s': %m", item->name);
         }
index e0cc6a85a51e24ab3708ac0c3f8ffe9b1913b6fd..5a16459aeb8ea4fc490dcfd7ca3f1c480fca8923 100644 (file)
@@ -685,6 +685,8 @@ EOF
         mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
         grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app0"
         echo "${version_id}" >>"$initdir/usr/lib/extension-release.d/extension-release.app0"
+        ( echo "${version_id}"
+          echo "SYSEXT_IMAGE_ID=app" ) >>"$initdir/usr/lib/extension-release.d/extension-release.app0"
         cat >"$initdir/usr/lib/systemd/system/app0.service" <<EOF
 [Service]
 Type=oneshot
@@ -710,6 +712,8 @@ EOF
         grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app2"
         ( echo "${version_id}"
           echo "SYSEXT_SCOPE=portable"
+          echo "SYSEXT_IMAGE_ID=app"
+          echo "SYSEXT_IMAGE_VERSION=1"
           echo "PORTABLE_PREFIXES=app1" ) >>"$initdir/usr/lib/extension-release.d/extension-release.app2"
         setfattr -n user.extension-release.strict -v false "$initdir/usr/lib/extension-release.d/extension-release.app2"
         cat >"$initdir/usr/lib/systemd/system/app1.service" <<EOF
index 343fd9eb22112b3a10a6f9b3d85af6df0f84c061..c1cb2bc33ec57bb870815d4dc903373556b7d450 100755 (executable)
@@ -96,12 +96,20 @@ systemctl is-active app0.service
 status="$(portablectl is-attached --extension app0 minimal_0)"
 [[ "${status}" == "running-runtime" ]]
 
+grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
+
 timeout "$TIMEOUT" portablectl "${ARGS[@]}" reattach --now --runtime --extension /usr/share/app0.raw /usr/share/minimal_1.raw app0
 
 systemctl is-active app0.service
 status="$(portablectl is-attached --extension app0 minimal_1)"
 [[ "${status}" == "running-runtime" ]]
 
+grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_1.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
+
 portablectl detach --now --runtime --extension /usr/share/app0.raw /usr/share/minimal_1.raw app0
 
 portablectl "${ARGS[@]}" attach --now --runtime --extension /usr/share/app1.raw /usr/share/minimal_0.raw app1
@@ -189,6 +197,20 @@ portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 |
 portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app1/usr/lib/systemd/system/app1.service
 portablectl inspect --cat --extension app0 --extension app1 rootdir app0 app1 | grep -q -f /tmp/app0/usr/lib/systemd/system/app0.service
 
+grep -q -F "LogExtraFields=PORTABLE=app0" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_ROOT=rootdir" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app1" /run/systemd/system.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app_1" /run/systemd/system.attached/app0.service.d/20-portable.conf
+
+grep -q -F "LogExtraFields=PORTABLE=app1" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_ROOT=rootdir" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app1" /run/systemd/system.attached/app1.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app_1" /run/systemd/system.attached/app1.service.d/20-portable.conf
+
 portablectl detach --now --runtime --extension /tmp/app0 --extension /tmp/app1 /tmp/rootdir app0 app1
 
 # Attempt to disable the app unit during detaching. Requires --copy=symlink to reproduce.