From: Lennart Poettering Date: Tue, 10 Oct 2023 14:04:51 +0000 (+0200) Subject: dissect: allow confext/sysext to be in the same image X-Git-Tag: v255-rc1~247^2~1 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a81fe93e9532bd3286f21ca0696208eb474f686c;p=thirdparty%2Fsystemd.git dissect: allow confext/sysext to be in the same image This reworks the image discovery logic, and conceptually allows DDIs that are both confext and sysext to exist. Previously we'd only extract one type of exension data from a DDI, with this we allow to extract both if both exist. This doesn't add support for true "multi-modal" DDIs, that qualify as various things at once, it just lays some ground work that ensures we at least can dissect such images. This reworks 484d26dac1e8e543fc9e300e3c1fa36be0769f7d quite a bit. This changes systemd-dissect's JSON output, but given the version with the fields it changes/dops has never been released (as the above patch was merged post-v254) this shouldn't be an issue. --- diff --git a/src/basic/os-util.h b/src/basic/os-util.h index 11afc4c6cab..7cee3dd119f 100644 --- a/src/basic/os-util.h +++ b/src/basic/os-util.h @@ -10,7 +10,9 @@ typedef enum ImageClass { IMAGE_MACHINE, IMAGE_PORTABLE, IMAGE_SYSEXT, + _IMAGE_CLASS_EXTENSION_FIRST = IMAGE_SYSEXT, /* First "extension" image type, so that we can easily generically iterate through them */ IMAGE_CONFEXT, + _IMAGE_CLASS_EXTENSION_LAST = IMAGE_CONFEXT, /* Last "extension image type */ _IMAGE_CLASS_MAX, _IMAGE_CLASS_INVALID = -EINVAL, } ImageClass; diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 28cced4173b..a03f9a64db8 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -765,24 +765,40 @@ static void strv_pair_print(char **l, const char *prefix) { printf("%*s %s=%s\n", (int) strlen(prefix), "", *p, *q); } -static int get_extension_scopes(DissectedImage *m, char ***ret_scopes) { +static int get_extension_scopes(DissectedImage *m, ImageClass class, char ***ret_scopes) { _cleanup_strv_free_ char **l = NULL; - const char *e; + const char *e, *field_name; + char **release_data; assert(m); assert(ret_scopes); + switch (class) { + + case IMAGE_SYSEXT: + release_data = m->sysext_release; + field_name = "SYSEXT_SCOPE"; + break; + + case IMAGE_CONFEXT: + release_data = m->confext_release; + field_name = "CONFEXT_SCOPE"; + break; + + default: + return -EINVAL; + } + /* If there's no extension-release file its not a system extension. Otherwise the SYSEXT_SCOPE * field for sysext images and the CONFEXT_SCOPE field for confext images indicates which scope * it is for — and it defaults to "system" + "portable" if unset. */ - if (!m->extension_release) { + + if (!release_data) { *ret_scopes = NULL; return 0; } - e = strv_env_pairs_get(m->extension_release, "SYSEXT_SCOPE"); - if (!e) - e = strv_env_pairs_get(m->extension_release, "CONFEXT_SCOPE"); + e = strv_env_pairs_get(release_data, field_name); if (e) l = strv_split(e, WHITESPACE); else @@ -843,7 +859,6 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { else if (r < 0) return log_error_errno(r, "Failed to acquire image metadata: %m"); else if (arg_json_format_flags & JSON_FORMAT_OFF) { - _cleanup_strv_free_ char **extension_scopes = NULL; if (!sd_id128_is_null(m->image_uuid)) printf("Image UUID: %s\n", SD_ID128_TO_UUID_STRING(m->image_uuid)); @@ -860,15 +875,18 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { "OS Release:"); strv_pair_print(m->initrd_release, "initrd R.:"); - strv_pair_print(m->extension_release, - " Ext. Rel.:"); + strv_pair_print(m->sysext_release, + " sysext R.:"); + strv_pair_print(m->confext_release, + "confext R.:"); if (m->hostname || !sd_id128_is_null(m->machine_id) || !strv_isempty(m->machine_info) || !strv_isempty(m->os_release) || !strv_isempty(m->initrd_release) || - !strv_isempty(m->extension_release)) + !strv_isempty(m->sysext_release) || + !strv_isempty(m->confext_release)) putc('\n', stdout); printf(" Use As: %s bootable system for UEFI\n", COLOR_MARK_BOOL(m->partitions[PARTITION_ESP].found)); @@ -881,26 +899,33 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { printf(" %s initrd\n", COLOR_MARK_BOOL(!strv_isempty(m->initrd_release))); - r = get_extension_scopes(m, &extension_scopes); - if (r < 0) - return log_error_errno(r, "Failed to parse scope: %m"); + for (ImageClass c = _IMAGE_CLASS_EXTENSION_FIRST; c <= _IMAGE_CLASS_EXTENSION_LAST; c++) { + const char *string_class = image_class_to_string(c); + _cleanup_strv_free_ char **extension_scopes = NULL; - const char *string_class = image_class_to_string(m->image_class); - printf(" %s %s extension for system\n", - COLOR_MARK_BOOL(strv_contains(extension_scopes, "system")), string_class); - printf(" %s %s extension for initrd\n", - COLOR_MARK_BOOL(strv_contains(extension_scopes, "initrd")), string_class); - printf(" %s %s extension for portable service\n", - COLOR_MARK_BOOL(strv_contains(extension_scopes, "portable")), string_class); + r = get_extension_scopes(m, c, &extension_scopes); + if (r < 0) + return log_error_errno(r, "Failed to parse scopes: %m"); + + printf(" %s %s for system\n", + COLOR_MARK_BOOL(strv_contains(extension_scopes, "system")), string_class); + printf(" %s %s for portable service\n", + COLOR_MARK_BOOL(strv_contains(extension_scopes, "portable")), string_class); + printf(" %s %s for initrd\n", + COLOR_MARK_BOOL(strv_contains(extension_scopes, "initrd")), string_class); + } putc('\n', stdout); } else { - _cleanup_(json_variant_unrefp) JsonVariant *mi = NULL, *osr = NULL, *irdr = NULL, *exr = NULL; - _cleanup_strv_free_ char **extension_scopes = NULL; + _cleanup_strv_free_ char **sysext_scopes = NULL, **confext_scopes = NULL; + + r = get_extension_scopes(m, IMAGE_SYSEXT, &sysext_scopes); + if (r < 0) + return log_error_errno(r, "Failed to parse sysext scopes: %m"); - r = get_extension_scopes(m, &extension_scopes); + r = get_extension_scopes(m, IMAGE_CONFEXT, &confext_scopes); if (r < 0) - return log_error_errno(r, "Failed to parse scope: %m"); + return log_error_errno(r, "Failed to parse confext scopes: %m"); Architecture a = dissected_image_architecture(m); @@ -915,15 +940,18 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { JSON_BUILD_PAIR_CONDITION(!strv_isempty(m->machine_info), "machineInfo", JSON_BUILD_STRV_ENV_PAIR(m->machine_info)), JSON_BUILD_PAIR_CONDITION(!strv_isempty(m->os_release), "osRelease", JSON_BUILD_STRV_ENV_PAIR(m->os_release)), JSON_BUILD_PAIR_CONDITION(!strv_isempty(m->initrd_release), "initrdRelease", JSON_BUILD_STRV_ENV_PAIR(m->initrd_release)), - JSON_BUILD_PAIR_CONDITION(!strv_isempty(m->extension_release), "extensionRelease", JSON_BUILD_STRV_ENV_PAIR(m->extension_release)), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(m->sysext_release), "sysextRelease", JSON_BUILD_STRV_ENV_PAIR(m->sysext_release)), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(m->confext_release), "confextRelease", JSON_BUILD_STRV_ENV_PAIR(m->confext_release)), JSON_BUILD_PAIR("useBootableUefi", JSON_BUILD_BOOLEAN(m->partitions[PARTITION_ESP].found)), JSON_BUILD_PAIR_CONDITION(m->has_init_system >= 0, "useBootableContainer", JSON_BUILD_BOOLEAN(m->has_init_system)), JSON_BUILD_PAIR("useInitrd", JSON_BUILD_BOOLEAN(!strv_isempty(m->initrd_release))), JSON_BUILD_PAIR("usePortableService", JSON_BUILD_BOOLEAN(strv_env_pairs_get(m->os_release, "PORTABLE_MATCHES"))), - JSON_BUILD_PAIR("ExtensionType", JSON_BUILD_STRING(image_class_to_string(m->image_class))), - JSON_BUILD_PAIR("useSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(extension_scopes, "system"))), - JSON_BUILD_PAIR("useInitRDExtension", JSON_BUILD_BOOLEAN(strv_contains(extension_scopes, "initrd"))), - JSON_BUILD_PAIR("usePortableExtension", JSON_BUILD_BOOLEAN(strv_contains(extension_scopes, "portable"))))); + JSON_BUILD_PAIR("useSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "system"))), + JSON_BUILD_PAIR("useInitRDSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "initrd"))), + JSON_BUILD_PAIR("usePortableSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "portable"))), + JSON_BUILD_PAIR("useConfigurationExtension", JSON_BUILD_BOOLEAN(strv_contains(confext_scopes, "system"))), + JSON_BUILD_PAIR("useInitRDConfigurationExtension", JSON_BUILD_BOOLEAN(strv_contains(confext_scopes, "initrd"))), + JSON_BUILD_PAIR("usePortableConfigurationExtension", JSON_BUILD_BOOLEAN(strv_contains(confext_scopes, "portable"))))); if (r < 0) return log_oom(); } diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index e407820e181..094337616da 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -101,7 +101,8 @@ static Image *image_free(Image *i) { free(i->hostname); strv_free(i->machine_info); strv_free(i->os_release); - strv_free(i->extension_release); + strv_free(i->sysext_release); + strv_free(i->confext_release); return mfree(i); } @@ -1180,10 +1181,9 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { case IMAGE_SUBVOLUME: case IMAGE_DIRECTORY: { - _cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **extension_release = NULL; + _cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **sysext_release = NULL, **confext_release = NULL; + _cleanup_free_ char *hostname = NULL, *path = NULL; sd_id128_t machine_id = SD_ID128_NULL; - _cleanup_free_ char *hostname = NULL; - _cleanup_free_ char *path = NULL; if (i->class == IMAGE_SYSEXT) { r = extension_has_forbidden_content(i->path); @@ -1223,16 +1223,20 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { if (r < 0) log_debug_errno(r, "Failed to read os-release in image, ignoring: %m"); - r = load_extension_release_pairs(i->path, i->class, i->name, /* relax_extension_release_check= */ false, &extension_release); + r = load_extension_release_pairs(i->path, IMAGE_SYSEXT, i->name, /* relax_extension_release_check= */ false, &sysext_release); if (r < 0) - log_debug_errno(r, "Failed to read extension-release in image, ignoring: %m"); + log_debug_errno(r, "Failed to read sysext-release in image, ignoring: %m"); + + r = load_extension_release_pairs(i->path, IMAGE_CONFEXT, i->name, /* relax_extension_release_check= */ false, &confext_release); + if (r < 0) + log_debug_errno(r, "Failed to read confext-release in image, ignoring: %m"); free_and_replace(i->hostname, hostname); i->machine_id = machine_id; strv_free_and_replace(i->machine_info, machine_info); strv_free_and_replace(i->os_release, os_release); - strv_free_and_replace(i->extension_release, extension_release); - + strv_free_and_replace(i->sysext_release, sysext_release); + strv_free_and_replace(i->confext_release, confext_release); break; } @@ -1271,7 +1275,8 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { i->machine_id = m->machine_id; strv_free_and_replace(i->machine_info, m->machine_info); strv_free_and_replace(i->os_release, m->os_release); - strv_free_and_replace(i->extension_release, m->extension_release); + strv_free_and_replace(i->sysext_release, m->sysext_release); + strv_free_and_replace(i->confext_release, m->confext_release); break; } diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index edfb1412a44..bb046fae1ec 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -45,7 +45,8 @@ typedef struct Image { sd_id128_t machine_id; char **machine_info; char **os_release; - char **extension_release; + char **sysext_release; + char **confext_release; bool metadata_valid:1; bool discoverable:1; /* true if we know for sure that image_find() would find the image given just the short name */ @@ -80,6 +81,17 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy); bool image_in_search_path(ImageClass class, const char *root, const char *image); +static inline char **image_extension_release(Image *image, ImageClass class) { + assert(image); + + if (class == IMAGE_SYSEXT) + return image->sysext_release; + if (class == IMAGE_CONFEXT) + return image->confext_release; + + return NULL; +} + static inline bool IMAGE_IS_HIDDEN(const struct Image *i) { assert(i); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 278d2291a28..e63545312a3 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1665,7 +1665,8 @@ DissectedImage* dissected_image_unref(DissectedImage *m) { strv_free(m->machine_info); strv_free(m->os_release); strv_free(m->initrd_release); - strv_free(m->extension_release); + strv_free(m->confext_release); + strv_free(m->sysext_release); return mfree(m); } @@ -3352,7 +3353,8 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ META_MACHINE_INFO, META_OS_RELEASE, META_INITRD_RELEASE, - META_EXTENSION_RELEASE, + META_SYSEXT_RELEASE, + META_CONFEXT_RELEASE, META_HAS_INIT_SYSTEM, _META_MAX, }; @@ -3361,15 +3363,16 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ [META_HOSTNAME] = "/etc/hostname\0", [META_MACHINE_ID] = "/etc/machine-id\0", [META_MACHINE_INFO] = "/etc/machine-info\0", - [META_OS_RELEASE] = ("/etc/os-release\0" - "/usr/lib/os-release\0"), - [META_INITRD_RELEASE] = ("/etc/initrd-release\0" - "/usr/lib/initrd-release\0"), - [META_EXTENSION_RELEASE] = "extension-release\0", /* Used only for logging. */ + [META_OS_RELEASE] = "/etc/os-release\0" + "/usr/lib/os-release\0", + [META_INITRD_RELEASE] = "/etc/initrd-release\0" + "/usr/lib/initrd-release\0", + [META_SYSEXT_RELEASE] = "sysext-release\0", /* String used only for logging. */ + [META_CONFEXT_RELEASE] = "confext-release\0", /* ditto */ [META_HAS_INIT_SYSTEM] = "has-init-system\0", /* ditto */ }; - _cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **initrd_release = NULL, **extension_release = NULL; + _cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **initrd_release = NULL, **sysext_release = NULL, **confext_release = NULL; _cleanup_close_pair_ int error_pipe[2] = PIPE_EBADF; _cleanup_(rmdir_and_freep) char *t = NULL; _cleanup_(sigkill_waitp) pid_t child = 0; @@ -3379,12 +3382,10 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ int fds[2 * _META_MAX], r, v; int has_init_system = -1; ssize_t n; - ImageClass image_class = IMAGE_SYSEXT; BLOCK_SIGNALS(SIGCHLD); assert(m); - assert(image_class); for (; n_meta_initialized < _META_MAX; n_meta_initialized ++) { if (!paths[n_meta_initialized]) { @@ -3439,40 +3440,46 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ switch (k) { - case META_EXTENSION_RELEASE: { - /* As per the os-release spec, if the image is an extension it will have a file - * named after the image name in extension-release.d/ - we use the image name - * and try to resolve it with the extension-release helpers, as sometimes - * the image names are mangled on deployment and do not match anymore. - * Unlike other paths this is not fixed, and the image name - * can be mangled on deployment, so by calling into the helper - * we allow a fallback that matches on the first extension-release - * file found in the directory, if one named after the image cannot - * be found first. */ - ImageClass class = IMAGE_SYSEXT; - r = open_extension_release(t, IMAGE_SYSEXT, m->image_name, /* relax_extension_release_check= */ false, NULL, &fd); - if (r == -ENOENT) { - r = open_extension_release(t, IMAGE_CONFEXT, m->image_name, /* relax_extension_release_check= */ false, NULL, &fd); - if (r >= 0) - class = IMAGE_CONFEXT; - } + case META_SYSEXT_RELEASE: + /* As per the os-release spec, if the image is an extension it will have a + * file named after the image name in extension-release.d/ - we use the image + * name and try to resolve it with the extension-release helpers, as + * sometimes the image names are mangled on deployment and do not match + * anymore. Unlike other paths this is not fixed, and the image name can be + * mangled on deployment, so by calling into the helper we allow a fallback + * that matches on the first extension-release file found in the directory, + * if one named after the image cannot be found first. */ + r = open_extension_release( + t, + IMAGE_SYSEXT, + m->image_name, + /* relax_extension_release_check= */ false, + /* ret_path= */ NULL, + &fd); + if (r < 0) + fd = r; + break; + + case META_CONFEXT_RELEASE: + /* As above */ + r = open_extension_release( + t, + IMAGE_CONFEXT, + m->image_name, + /* relax_extension_release_check= */ false, + /* ret_path= */ NULL, + &fd); if (r < 0) fd = r; - else { - r = loop_write(fds[2*k+1], &class, sizeof(class)); - if (r < 0) - goto inner_fail; /* Propagate the error to the parent */ - } break; - } case META_HAS_INIT_SYSTEM: { bool found = false; FOREACH_STRING(init, - "/usr/lib/systemd/systemd", /* systemd on /usr merged system */ - "/lib/systemd/systemd", /* systemd on /usr non-merged systems */ + "/usr/lib/systemd/systemd", /* systemd on /usr/ merged system */ + "/lib/systemd/systemd", /* systemd on /usr/ non-merged systems */ "/sbin/init") { /* traditional path the Linux kernel invokes */ r = chase(init, t, CHASE_PREFIX_ROOT, NULL, NULL); @@ -3587,23 +3594,19 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ break; - case META_EXTENSION_RELEASE: { - ImageClass cl = IMAGE_SYSEXT; - size_t nr; + case META_SYSEXT_RELEASE: + r = load_env_file_pairs(f, "sysext-release", &sysext_release); + if (r < 0) + log_debug_errno(r, "Failed to read sysext release file of image: %m"); - errno = 0; - nr = fread(&cl, 1, sizeof(cl), f); - if (nr != sizeof(cl)) - log_debug_errno(errno_or_else(EIO), "Failed to read class of extension image: %m"); - else { - image_class = cl; - r = load_env_file_pairs(f, "extension-release", &extension_release); - if (r < 0) - log_debug_errno(r, "Failed to read extension release file of image: %m"); - } + break; + + case META_CONFEXT_RELEASE: + r = load_env_file_pairs(f, "confext-release", &confext_release); + if (r < 0) + log_debug_errno(r, "Failed to read confext release file of image: %m"); break; - } case META_HAS_INIT_SYSTEM: { bool b = false; @@ -3641,9 +3644,9 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ strv_free_and_replace(m->machine_info, machine_info); strv_free_and_replace(m->os_release, os_release); strv_free_and_replace(m->initrd_release, initrd_release); - strv_free_and_replace(m->extension_release, extension_release); + strv_free_and_replace(m->sysext_release, sysext_release); + strv_free_and_replace(m->confext_release, confext_release); m->has_init_system = has_init_system; - m->image_class = image_class; finish: for (unsigned k = 0; k < n_meta_initialized; k++) diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index eb0841bd2e4..21a0f22bcc2 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -108,9 +108,9 @@ struct DissectedImage { char **machine_info; char **os_release; char **initrd_release; - char **extension_release; + char **confext_release; + char **sysext_release; int has_init_system; - ImageClass image_class; }; struct MountOptions { diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 784accc15d0..becfbab44ec 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -741,7 +741,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) { host_os_release_version_id, host_os_release_api_level, in_initrd() ? "initrd" : "system", - img->extension_release, + image_extension_release(img, arg_image_class), arg_image_class); if (r < 0) return r; diff --git a/test/units/testsuite-29.sh b/test/units/testsuite-29.sh index 36e5cdc1263..4bbbd38bee3 100755 --- a/test/units/testsuite-29.sh +++ b/test/units/testsuite-29.sh @@ -31,8 +31,8 @@ fi systemd-dissect --no-pager /usr/share/minimal_0.raw | grep -q '✓ portable service' systemd-dissect --no-pager /usr/share/minimal_1.raw | grep -q '✓ portable service' -systemd-dissect --no-pager /usr/share/app0.raw | grep -q '✓ sysext extension for portable service' -systemd-dissect --no-pager /usr/share/app1.raw | grep -q '✓ sysext extension for portable service' +systemd-dissect --no-pager /usr/share/app0.raw | grep -q '✓ sysext for portable service' +systemd-dissect --no-pager /usr/share/app1.raw | grep -q '✓ sysext for portable service' export SYSTEMD_LOG_LEVEL=debug mkdir -p /run/systemd/system/systemd-portabled.service.d/ diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index b7943397c4c..0cec747397b 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -573,8 +573,8 @@ echo "MARKER_SYSEXT_123" >testkit/usr/lib/testfile mksquashfs testkit/ testkit.raw cp testkit.raw /run/extensions/ unsquashfs -l /run/extensions/testkit.raw -systemd-dissect --no-pager /run/extensions/testkit.raw | grep -q '✓ sysext extension for portable service' -systemd-dissect --no-pager /run/extensions/testkit.raw | grep -q '✓ sysext extension for system' +systemd-dissect --no-pager /run/extensions/testkit.raw | grep -q '✓ sysext for portable service' +systemd-dissect --no-pager /run/extensions/testkit.raw | grep -q '✓ sysext for system' systemd-sysext merge systemd-sysext status grep -q -F "MARKER_SYSEXT_123" /usr/lib/testfile @@ -589,8 +589,8 @@ echo "MARKER_CONFEXT_123" >testjob/etc/testfile mksquashfs testjob/ testjob.raw cp testjob.raw /run/confexts/ unsquashfs -l /run/confexts/testjob.raw -systemd-dissect --no-pager /run/confexts/testjob.raw | grep -q '✓ confext extension for system' -systemd-dissect --no-pager /run/confexts/testjob.raw | grep -q '✓ confext extension for portable service' +systemd-dissect --no-pager /run/confexts/testjob.raw | grep -q '✓ confext for system' +systemd-dissect --no-pager /run/confexts/testjob.raw | grep -q '✓ confext for portable service' systemd-confext merge systemd-confext status grep -q -F "MARKER_CONFEXT_123" /etc/testfile