portable: add PORTABLE_NAME_AND_VERSION= and other metadata to LogsExtraFields=

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
This commit is contained in:
Luca Boccassi 2023-03-23 01:23:04 +00:00
parent 8c8331fc50
commit e8114a4f86
4 changed files with 139 additions and 10 deletions

View 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

View 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);
}

View 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

View file

@ -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.