1
1
mirror of https://github.com/systemd/systemd-stable.git synced 2025-02-15 05:57:26 +03:00

Merge pull request #26941 from bluca/portable_version

portable: introduce SYSEXT_ fields to identify sysexts, and include more metadata in log messages via LogExtraFields=
This commit is contained in:
Luca Boccassi 2023-03-28 17:49:52 +01:00 committed by GitHub
commit a4d1d1f63d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 279 additions and 49 deletions

View File

@ -335,6 +335,45 @@ service data may be placed on the host file system. Use `StateDirectory=` in
the unit files to enable such behaviour and add a local data directory to the
services copied onto the host.
## Logging
Several fields are autotmatically added to log messages generated by a portable
service (or about a portable service, e.g.: start/stop logs from systemd).
The `PORTABLE=` field will refer to the name of the portable image where the unit
was loaded from. In case extensions are used, additionally there will be a
`PORTABLE_ROOT=` field, referring to the name of image used as the base layer
(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` (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
[`portablectl(1)`](https://www.freedesktop.org/software/systemd/man/portablectl.html)<br>

View File

@ -111,6 +111,11 @@
<varname>VERSION_ID=</varname> exists and matches. This ensures ABI/API compatibility between the
layers and prevents merging of an incompatible image in an overlay.</para>
<para>In order to identify the extension image itself, the same fields defined below can be added to the
<filename>extension-release</filename> file with a <varname>SYSEXT_</varname> prefix (to disambiguate
from fields used to match on the base image). E.g.: <varname>SYSEXT_ID=myext</varname>,
<varname>SYSEXT_VERSION_ID=1.2.3</varname>.</para>
<para>In the <filename>extension-release.<replaceable>IMAGE</replaceable></filename> filename, the
<replaceable>IMAGE</replaceable> part must exactly match the file name of the containing image with the
suffix removed. In case it is not possible to guarantee that an image file name is stable and doesn't

View File

@ -478,6 +478,7 @@ int load_env_file_pairs(FILE *f, const char *fname, char ***ret) {
int r;
assert(f || fname);
assert(ret);
r = parse_env_file_internal(f, fname, load_env_file_push_pairs, &m);
if (r < 0)
@ -487,6 +488,19 @@ int load_env_file_pairs(FILE *f, const char *fname, char ***ret) {
return 0;
}
int load_env_file_pairs_fd(int fd, const char *fname, char ***ret) {
_cleanup_fclose_ FILE *f = NULL;
int r;
assert(fd >= 0);
r = fdopen_independent(fd, "re", &f);
if (r < 0)
return r;
return load_env_file_pairs(f, fname, ret);
}
static int merge_env_file_push(
const char *filename, unsigned line,
const char *key, char *value,

View File

@ -14,6 +14,7 @@ int parse_env_file_fd_sentinel(int fd, const char *fname, ...) _sentinel_;
#define parse_env_file_fd(fd, fname, ...) parse_env_file_fd_sentinel(fd, fname, __VA_ARGS__, NULL)
int load_env_file(FILE *f, const char *fname, char ***ret);
int load_env_file_pairs(FILE *f, const char *fname, char ***ret);
int load_env_file_pairs_fd(int fd, const char *fname, char ***ret);
int merge_env_file(char ***env, FILE *f, const char *fname);

View File

@ -7,6 +7,7 @@
#include <stdlib.h>
#include "alloc-util.h"
#include "env-util.h"
#include "escape.h"
#include "extract-word.h"
#include "fileio.h"
@ -63,6 +64,16 @@ char* strv_find_startswith(char * const *l, const char *name) {
return NULL;
}
char* strv_find_first_field(char * const *needles, char * const *haystack) {
STRV_FOREACH(k, needles) {
char *value = strv_env_pairs_get((char **)haystack, *k);
if (value)
return value;
}
return NULL;
}
char** strv_free(char **l) {
STRV_FOREACH(k, l)
free(*k);

View File

@ -17,6 +17,9 @@ char* strv_find(char * const *l, const char *name) _pure_;
char* strv_find_case(char * const *l, const char *name) _pure_;
char* strv_find_prefix(char * const *l, const char *name) _pure_;
char* strv_find_startswith(char * const *l, const char *name) _pure_;
/* Given two vectors, the first a list of keys and the second a list of key-value pairs, returns the value
* of the first key from the first vector that is found in the second vector. */
char* strv_find_first_field(char * const *needles, char * const *haystack) _pure_;
#define strv_contains(l, s) (!!strv_find((l), (s)))
#define strv_contains_case(l, s) (!!strv_find_case((l), (s)))

View File

@ -566,18 +566,13 @@ static int extract_image_and_extensions(
* extension-release metadata match, otherwise reject it immediately as invalid, or it will fail when
* the units are started. Also, collect valid portable prefixes if caller requested that. */
if (validate_sysext || ret_valid_prefixes) {
_cleanup_fclose_ FILE *f = NULL;
_cleanup_free_ char *prefixes = NULL;
r = take_fdopen_unlocked(&os_release->fd, "r", &f);
if (r < 0)
return r;
r = parse_env_file(f, os_release->name,
"ID", &id,
"VERSION_ID", &version_id,
"SYSEXT_LEVEL", &sysext_level,
"PORTABLE_PREFIXES", &prefixes);
r = parse_env_file_fd(os_release->fd, os_release->name,
"ID", &id,
"VERSION_ID", &version_id,
"SYSEXT_LEVEL", &sysext_level,
"PORTABLE_PREFIXES", &prefixes);
if (r < 0)
return r;
if (isempty(id))
@ -594,7 +589,6 @@ static int extract_image_and_extensions(
_cleanup_(portable_metadata_unrefp) PortableMetadata *extension_release_meta = NULL;
_cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL;
_cleanup_strv_free_ char **extension_release = NULL;
_cleanup_fclose_ FILE *f = NULL;
const char *e;
r = portable_extract_by_path(ext->path, /* path_is_extension= */ true, relax_extension_release_check, matches, &extension_release_meta, &extra_unit_files, error);
@ -608,12 +602,7 @@ static int extract_image_and_extensions(
if (!validate_sysext && !ret_valid_prefixes && !ret_extension_releases)
continue;
/* We need to keep the fd valid, to return the PortableMetadata to the caller. */
r = fdopen_independent(extension_release_meta->fd, "re", &f);
if (r < 0)
return r;
r = load_env_file_pairs(f, extension_release_meta->name, &extension_release);
r = load_env_file_pairs_fd(extension_release_meta->fd, extension_release_meta->name, &extension_release);
if (r < 0)
return r;
@ -951,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,
@ -1005,19 +1050,67 @@ static int install_chroot_dropin(
"LogExtraFields=PORTABLE=", base_name, "\n"))
return -ENOMEM;
/* 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,
"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)
ORDERED_HASHMAP_FOREACH(ext, extension_images) {
_cleanup_free_ char *extension_base_name = NULL;
r = path_extract_filename(ext->path, &extension_base_name);
if (r < 0)
return log_debug_errno(r, "Failed to extract basename from '%s': %m", ext->path);
if (!strextend(&text,
"\n",
extension_setting_from_image(ext->type),
ext->path,
/* With --force tell PID1 to avoid enforcing that the image <name> and
* extension-release.<name> have to match. */
!IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) &&
FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT) ?
":x-systemd.relax-extension-release-check" :
"",
"\n"))
":x-systemd.relax-extension-release-check\n" :
"\n",
/* In PORTABLE= we list the 'main' image name for this unit
* (the image where the unit was extracted from), but we are
* 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;
}
}
r = write_string_file(dropin, text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
@ -1106,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,
@ -1150,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;
@ -1302,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;
@ -1318,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);
@ -1386,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

@ -400,7 +400,8 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
nl = true;
} else {
_cleanup_free_ char *pretty_portable = NULL, *pretty_os = NULL, *sysext_level = NULL,
*id = NULL, *version_id = NULL, *sysext_scope = NULL, *portable_prefixes = NULL;
*sysext_id = NULL, *sysext_version_id = NULL, *sysext_scope = NULL, *portable_prefixes = NULL,
*id = NULL, *version_id = NULL, *image_id = NULL, *image_version = NULL, *build_id = NULL;
_cleanup_fclose_ FILE *f = NULL;
f = fmemopen_unlocked((void*) data, sz, "r");
@ -408,30 +409,42 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
return log_error_errno(errno, "Failed to open extension-release buffer: %m");
r = parse_env_file(f, name,
"ID", &id,
"VERSION_ID", &version_id,
"SYSEXT_ID", &sysext_id,
"SYSEXT_VERSION_ID", &sysext_version_id,
"SYSEXT_BUILD_ID", &build_id,
"SYSEXT_IMAGE_ID", &image_id,
"SYSEXT_IMAGE_VERSION", &image_version,
"SYSEXT_PRETTY_NAME", &pretty_os,
"SYSEXT_SCOPE", &sysext_scope,
"SYSEXT_LEVEL", &sysext_level,
"ID", &id,
"VERSION_ID", &version_id,
"PORTABLE_PRETTY_NAME", &pretty_portable,
"PORTABLE_PREFIXES", &portable_prefixes,
"PRETTY_NAME", &pretty_os);
"PORTABLE_PREFIXES", &portable_prefixes);
if (r < 0)
return log_error_errno(r, "Failed to parse extension release from '%s': %m", name);
printf("Extension:\n\t%s\n"
"\tExtension Scope:\n\t\t%s\n"
"\tExtension Compatibility Level:\n\t\t%s\n"
"\tExtension Compatibility OS:\n\t\t%s\n"
"\tExtension Compatibility OS Version:\n\t\t%s\n"
"\tPortable Service:\n\t\t%s\n"
"\tPortable Prefixes:\n\t\t%s\n"
"\tOperating System:\n\t\t%s (%s %s)\n",
"\tExtension Image:\n\t\t%s%s%s %s%s%s\n",
name,
strna(sysext_scope),
strna(sysext_level),
strna(id),
strna(version_id),
strna(pretty_portable),
strna(portable_prefixes),
strna(pretty_os),
strna(id),
strna(version_id));
strempty(pretty_os),
pretty_os ? " (" : "ID: ",
strna(sysext_id ?: image_id),
pretty_os ? "" : "Version: ",
strna(sysext_version_id ?: image_version ?: build_id),
pretty_os ? ")" : "");
}
r = sd_bus_message_exit_container(reply);

View File

@ -509,6 +509,23 @@ TEST(write_string_file_verify) {
assert_se(write_string_file("/proc/version", buf2, WRITE_STRING_FILE_VERIFY_ON_FAILURE|WRITE_STRING_FILE_AVOID_NEWLINE) == 0);
}
static void check_file_pairs_one(char **l) {
assert_se(l);
assert_se(strv_length(l) == 14);
STRV_FOREACH_PAIR(k, v, l) {
assert_se(STR_IN_SET(*k, "NAME", "ID", "PRETTY_NAME", "ANSI_COLOR", "HOME_URL", "SUPPORT_URL", "BUG_REPORT_URL"));
printf("%s=%s\n", *k, *v);
assert_se(!streq(*k, "NAME") || streq(*v, "Arch Linux"));
assert_se(!streq(*k, "ID") || streq(*v, "arch"));
assert_se(!streq(*k, "PRETTY_NAME") || streq(*v, "Arch Linux"));
assert_se(!streq(*k, "ANSI_COLOR") || streq(*v, "0;36"));
assert_se(!streq(*k, "HOME_URL") || streq(*v, "https://www.archlinux.org/"));
assert_se(!streq(*k, "SUPPORT_URL") || streq(*v, "https://bbs.archlinux.org/"));
assert_se(!streq(*k, "BUG_REPORT_URL") || streq(*v, "https://bugs.archlinux.org/"));
}
}
TEST(load_env_file_pairs) {
_cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-load_env_file_pairs-XXXXXX";
int fd, r;
@ -529,24 +546,17 @@ TEST(load_env_file_pairs) {
WRITE_STRING_FILE_CREATE);
assert_se(r == 0);
r = load_env_file_pairs_fd(fd, fn, &l);
assert_se(r >= 0);
check_file_pairs_one(l);
l = strv_free(l);
f = fdopen(fd, "r");
assert_se(f);
r = load_env_file_pairs(f, fn, &l);
assert_se(r >= 0);
assert_se(strv_length(l) == 14);
STRV_FOREACH_PAIR(k, v, l) {
assert_se(STR_IN_SET(*k, "NAME", "ID", "PRETTY_NAME", "ANSI_COLOR", "HOME_URL", "SUPPORT_URL", "BUG_REPORT_URL"));
printf("%s=%s\n", *k, *v);
if (streq(*k, "NAME")) assert_se(streq(*v, "Arch Linux"));
if (streq(*k, "ID")) assert_se(streq(*v, "arch"));
if (streq(*k, "PRETTY_NAME")) assert_se(streq(*v, "Arch Linux"));
if (streq(*k, "ANSI_COLOR")) assert_se(streq(*v, "0;36"));
if (streq(*k, "HOME_URL")) assert_se(streq(*v, "https://www.archlinux.org/"));
if (streq(*k, "SUPPORT_URL")) assert_se(streq(*v, "https://bbs.archlinux.org/"));
if (streq(*k, "BUG_REPORT_URL")) assert_se(streq(*v, "https://bugs.archlinux.org/"));
}
check_file_pairs_one(l);
}
TEST(search_and_fopen) {

View File

@ -994,4 +994,16 @@ TEST(strv_copy_n) {
assert_se(strv_equal(l, STRV_MAKE("a", "b", "c", "d", "e")));
}
TEST(strv_find_first_field) {
char **haystack = STRV_MAKE("a", "b", "c", "d", "e", "f", "g", "h", "i", "j");
assert_se(strv_find_first_field(NULL, NULL) == NULL);
assert_se(strv_find_first_field(NULL, haystack) == NULL);
assert_se(strv_find_first_field(STRV_MAKE("k", "l", "m", "d", "b"), NULL) == NULL);
assert_se(strv_find_first_field(STRV_MAKE("k", "l", "m", "d", "b"), haystack) == NULL);
assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("k", "l", "m", "d", "a", "c"), haystack), "b"));
assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("k", "l", "m", "d", "c", "a"), haystack), "d"));
assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("i", "k", "l", "m", "d", "c", "a", "b"), haystack), "j"));
}
DEFINE_TEST_MAIN(LOG_INFO);

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.