portable: add support for confext

Support confexts for portable services
This commit is contained in:
Maanya Goenka 2023-08-16 18:43:06 +00:00 committed by Luca Boccassi
parent f0304df6e4
commit db776f6935
8 changed files with 113 additions and 51 deletions

View file

@ -277,12 +277,14 @@ following must be also be observed:
2. The upper extension images must contain an extension-release file in
`/usr/lib/extension-release.d/`, with an `ID=` and `SYSEXT_LEVEL=`/`VERSION_ID=`
matching the base image.
matching the base image for sysexts, or `/etc/extension-release.d/`, with an
`ID=` and `CONFEXT_LEVEL=`/`VERSION_ID=` matching the base image for confexts.
3. The base/OS image does not need to have any unit files.
4. The upper extension images must contain at least one matching unit file
each, with the right name prefix and suffix (see above).
4. The upper sysext images must contain at least one matching unit file each,
with the right name prefix and suffix (see above). Confext images do not have
to contain units.
5. As with the base/OS image, each upper extension image must be a plain
sub-directory, btrfs subvolume, or a raw disk image.
@ -354,10 +356,10 @@ underscore (`_`) as separator. If only either one is found, it will be used by i
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.
`SYSEXT_`/`CONFEXT_`, 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

View file

@ -309,14 +309,14 @@ node /org/freedesktop/portable1 {
<function>ReattachImageWithExtensions()</function> methods take in options as flags instead of
booleans to allow for extendability. <varname>SD_SYSTEMD_PORTABLE_FORCE_ATTACH</varname> will cause
safety checks that ensure the units are not running while the new image is attached or detached
to be skipped. <varname>SD_SYSTEMD_PORTABLE_FORCE_SYSEXT</varname> will cause the check that the
to be skipped. <varname>SD_SYSTEMD_PORTABLE_FORCE_EXTENSION</varname> will cause the check that the
<filename>extension-release.<replaceable>NAME</replaceable></filename> file in the extension image
matches the image name to be skipped. They are defined as follows:</para>
<programlisting>
#define SD_SYSTEMD_PORTABLE_RUNTIME (UINT64_C(1) &lt;&lt; 0)
#define SD_SYSTEMD_PORTABLE_FORCE_ATTACH (UINT64_C(1) &lt;&lt; 1)
#define SD_SYSTEMD_PORTABLE_FORCE_SYSEXT (UINT64_C(1) &lt;&lt; 2)
#define SD_SYSTEMD_PORTABLE_RUNTIME (UINT64_C(1) &lt;&lt; 0)
#define SD_SYSTEMD_PORTABLE_FORCE_ATTACH (UINT64_C(1) &lt;&lt; 1)
#define SD_SYSTEMD_PORTABLE_FORCE_EXTENSION (UINT64_C(1) &lt;&lt; 2)
</programlisting>
</refsect2>

View file

@ -397,7 +397,8 @@
multiple times, in which case the order in which images are laid down follows the rules specified in
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for the <varname>ExtensionImages=</varname> directive and for the
<citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry> tool.
<citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>8</manvolnum></citerefentry> and.
<citerefentry><refentrytitle>systemd-confext</refentrytitle><manvolnum>8</manvolnum></citerefentry> tools.
The images must contain an <filename>extension-release</filename> file with metadata that matches
what is defined in the <filename>os-release</filename> of <replaceable>IMAGE</replaceable>. See:
<citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry>.

View file

@ -198,8 +198,18 @@ static int extract_now(
/* First, find os-release/extension-release and send it upstream (or just save it). */
if (path_is_extension) {
os_release_id = strjoina("/usr/lib/extension-release.d/extension-release.", image_name);
ImageClass class = IMAGE_SYSEXT;
r = open_extension_release(where, IMAGE_SYSEXT, image_name, relax_extension_release_check, &os_release_path, &os_release_fd);
if (r == -ENOENT) {
r = open_extension_release(where, IMAGE_CONFEXT, image_name, relax_extension_release_check, &os_release_path, &os_release_fd);
if (r >= 0)
class = IMAGE_CONFEXT;
}
if (r < 0)
return log_error_errno(r, "Failed to open extension release from '%s': %m", image_name);
os_release_id = strjoina((class == IMAGE_SYSEXT) ? "/usr/lib" : "/etc", "/extension-release.d/extension-release.", image_name);
} else {
os_release_id = "/etc/os-release";
r = open_os_release(where, &os_release_path, &os_release_fd);
@ -530,7 +540,7 @@ static int extract_image_and_extensions(
const char *name_or_path,
char **matches,
char **extension_image_paths,
bool validate_sysext,
bool validate_extension,
bool relax_extension_release_check,
const ImagePolicy *image_policy,
Image **ret_image,
@ -541,7 +551,7 @@ static int extract_image_and_extensions(
char ***ret_valid_prefixes,
sd_bus_error *error) {
_cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL;
_cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL, *confext_level = NULL;
_cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
_cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL;
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
@ -596,13 +606,14 @@ static int extract_image_and_extensions(
/* If we are layering extension images on top of a runtime image, check that the os-release and
* 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) {
if (validate_extension || ret_valid_prefixes) {
_cleanup_free_ char *prefixes = NULL;
r = parse_env_file_fd(os_release->fd, os_release->name,
"ID", &id,
"VERSION_ID", &version_id,
"SYSEXT_LEVEL", &sysext_level,
"CONFEXT_LEVEL", &confext_level,
"PORTABLE_PREFIXES", &prefixes);
if (r < 0)
return r;
@ -638,15 +649,18 @@ static int extract_image_and_extensions(
if (r < 0)
return r;
if (!validate_sysext && !ret_valid_prefixes && !ret_extension_releases)
if (!validate_extension && !ret_valid_prefixes && !ret_extension_releases)
continue;
r = load_env_file_pairs_fd(extension_release_meta->fd, extension_release_meta->name, &extension_release);
if (r < 0)
return r;
if (validate_sysext) {
if (validate_extension) {
r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release, IMAGE_SYSEXT);
if (r < 0)
r = extension_release_validate(ext->path, id, version_id, confext_level, "portable", extension_release, IMAGE_CONFEXT);
if (r == 0)
return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path);
if (r < 0)
@ -717,8 +731,8 @@ int portable_extract(
name_or_path,
matches,
extension_image_paths,
/* validate_sysext= */ false,
/* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT),
/* validate_extension= */ false,
/* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_EXTENSION),
image_policy,
&image,
&extension_images,
@ -979,16 +993,18 @@ static int append_release_log_fields(
static const char *const field_versions[_IMAGE_CLASS_MAX][4]= {
[IMAGE_PORTABLE] = { "IMAGE_VERSION", "VERSION_ID", "BUILD_ID", NULL },
[IMAGE_SYSEXT] = { "SYSEXT_IMAGE_VERSION", "SYSEXT_VERSION_ID", "SYSEXT_BUILD_ID", NULL },
[IMAGE_CONFEXT] = { "CONFEXT_IMAGE_VERSION", "CONFEXT_VERSION_ID", "CONFEXT_BUILD_ID", NULL },
};
static const char *const field_ids[_IMAGE_CLASS_MAX][3]= {
[IMAGE_PORTABLE] = { "IMAGE_ID", "ID", NULL },
[IMAGE_SYSEXT] = { "SYSEXT_IMAGE_ID", "SYSEXT_ID", NULL },
[IMAGE_CONFEXT] = { "CONFEXT_IMAGE_ID", "CONFEXT_ID", NULL },
};
_cleanup_strv_free_ char **fields = NULL;
const char *id = NULL, *version = NULL;
int r;
assert(IN_SET(type, IMAGE_PORTABLE, IMAGE_SYSEXT));
assert(IN_SET(type, IMAGE_PORTABLE, IMAGE_SYSEXT, IMAGE_CONFEXT));
assert(!strv_isempty((char *const *)field_ids[type]));
assert(!strv_isempty((char *const *)field_versions[type]));
assert(field_name);
@ -1111,7 +1127,7 @@ static int install_chroot_dropin(
/* 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) ?
FLAGS_SET(flags, PORTABLE_FORCE_EXTENSION) ?
":x-systemd.relax-extension-release-check\n" :
"\n",
/* In PORTABLE= we list the 'main' image name for this unit
@ -1131,6 +1147,13 @@ static int install_chroot_dropin(
"PORTABLE_EXTENSION_NAME_AND_VERSION");
if (r < 0)
return r;
r = append_release_log_fields(&text,
ordered_hashmap_get(extension_releases, ext->name),
IMAGE_CONFEXT,
"PORTABLE_EXTENSION_NAME_AND_VERSION");
if (r < 0)
return r;
}
}
@ -1432,8 +1455,8 @@ int portable_attach(
name_or_path,
matches,
extension_image_paths,
/* validate_sysext= */ true,
/* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_SYSEXT),
/* validate_extension= */ true,
/* relax_extension_release_check= */ FLAGS_SET(flags, PORTABLE_FORCE_EXTENSION),
image_policy,
&image,
&extension_images,

View file

@ -18,19 +18,19 @@ typedef struct PortableMetadata {
} PortableMetadata;
#define PORTABLE_METADATA_IS_OS_RELEASE(m) (streq((m)->name, "/etc/os-release"))
#define PORTABLE_METADATA_IS_EXTENSION_RELEASE(m) (startswith((m)->name, "/usr/lib/extension-release.d/extension-release."))
#define PORTABLE_METADATA_IS_EXTENSION_RELEASE(m) (startswith_strv((m)->name, STRV_MAKE("/usr/lib/extension-release.d/extension-release.", "/etc/extension-release.d/extension-release.")))
#define PORTABLE_METADATA_IS_UNIT(m) (!IN_SET((m)->name[0], 0, '/'))
typedef enum PortableFlags {
PORTABLE_RUNTIME = 1 << 0, /* Public API via DBUS, do not change */
PORTABLE_FORCE_ATTACH = 1 << 1, /* Public API via DBUS, do not change */
PORTABLE_FORCE_SYSEXT = 1 << 2, /* Public API via DBUS, do not change */
PORTABLE_PREFER_COPY = 1 << 3,
PORTABLE_PREFER_SYMLINK = 1 << 4,
PORTABLE_REATTACH = 1 << 5,
_PORTABLE_MASK_PUBLIC = PORTABLE_RUNTIME | PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_SYSEXT,
PORTABLE_RUNTIME = 1 << 0, /* Public API via DBUS, do not change */
PORTABLE_FORCE_ATTACH = 1 << 1, /* Public API via DBUS, do not change */
PORTABLE_FORCE_EXTENSION = 1 << 2, /* Public API via DBUS, do not change */
PORTABLE_PREFER_COPY = 1 << 3,
PORTABLE_PREFER_SYMLINK = 1 << 4,
PORTABLE_REATTACH = 1 << 5,
_PORTABLE_MASK_PUBLIC = PORTABLE_RUNTIME | PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_EXTENSION,
_PORTABLE_TYPE_MAX,
_PORTABLE_TYPE_INVALID = -EINVAL,
_PORTABLE_TYPE_INVALID = -EINVAL,
} PortableFlags;
/* This enum is anonymous, since we usually store it in an 'int', as we overload it with negative errno

View file

@ -248,7 +248,7 @@ static int maybe_reload(sd_bus **bus) {
static int get_image_metadata(sd_bus *bus, const char *image, char **matches, sd_bus_message **reply) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
uint64_t flags = arg_force ? PORTABLE_FORCE_SYSEXT : 0;
uint64_t flags = arg_force ? PORTABLE_FORCE_EXTENSION : 0;
const char *method;
int r;
@ -384,9 +384,16 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
fflush(stdout);
nl = true;
} else {
_cleanup_free_ char *pretty_portable = NULL, *pretty_os = NULL, *sysext_level = 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_free_ char *pretty_portable = NULL, *sysext_pretty_os = NULL,
*sysext_level = NULL, *sysext_id = NULL,
*sysext_version_id = NULL, *sysext_scope = NULL,
*portable_prefixes = NULL, *id = NULL, *version_id = NULL,
*sysext_image_id = NULL, *sysext_image_version = NULL,
*sysext_build_id = NULL, *confext_pretty_os = NULL,
*confext_level = NULL, *confext_id = NULL,
*confext_version_id = NULL, *confext_scope = NULL,
*confext_image_id = NULL, *confext_image_version = NULL,
*confext_build_id = NULL;
_cleanup_fclose_ FILE *f = NULL;
f = fmemopen_unlocked((void*) data, sz, "r");
@ -396,12 +403,20 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
r = parse_env_file(f, name,
"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_BUILD_ID", &sysext_build_id,
"SYSEXT_IMAGE_ID", &sysext_image_id,
"SYSEXT_IMAGE_VERSION", &sysext_image_version,
"SYSEXT_SCOPE", &sysext_scope,
"SYSEXT_LEVEL", &sysext_level,
"SYSEXT_PRETTY_NAME", &sysext_pretty_os,
"CONFEXT_ID", &confext_id,
"CONFEXT_VERSION_ID", &confext_version_id,
"CONFEXT_BUILD_ID", &confext_build_id,
"CONFEXT_IMAGE_ID", &confext_image_id,
"CONFEXT_IMAGE_VERSION", &confext_image_version,
"CONFEXT_SCOPE", &confext_scope,
"CONFEXT_LEVEL", &confext_level,
"CONFEXT_PRETTY_NAME", &confext_pretty_os,
"ID", &id,
"VERSION_ID", &version_id,
"PORTABLE_PRETTY_NAME", &pretty_portable,
@ -418,18 +433,18 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
"\tPortable Prefixes:\n\t\t%s\n"
"\tExtension Image:\n\t\t%s%s%s %s%s%s\n",
name,
strna(sysext_scope),
strna(sysext_level),
strna(sysext_scope ?: confext_scope),
strna(sysext_level ?: confext_level),
strna(id),
strna(version_id),
strna(pretty_portable),
strna(portable_prefixes),
strempty(pretty_os),
pretty_os ? " (" : "ID: ",
strna(sysext_id ?: image_id),
pretty_os ? "" : "Version: ",
strna(sysext_version_id ?: image_version ?: build_id),
pretty_os ? ")" : "");
strempty(sysext_pretty_os ?: confext_pretty_os),
(sysext_pretty_os ?: confext_pretty_os) ? " (" : "ID: ",
strna(sysext_id ?: sysext_image_id ?: confext_id ?: confext_image_id),
(sysext_pretty_os ?: confext_pretty_os) ? "" : "Version: ",
strna(sysext_version_id ?: sysext_image_version ?: sysext_build_id ?: confext_version_id ?: confext_image_version ?: confext_build_id),
(sysext_pretty_os ?: confext_pretty_os) ? ")" : "");
}
r = sd_bus_message_exit_container(reply);
@ -871,7 +886,7 @@ static int attach_reattach_image(int argc, char *argv[], const char *method) {
return bus_log_create_error(r);
if (STR_IN_SET(method, "AttachImageWithExtensions", "ReattachImageWithExtensions")) {
uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_SYSEXT : 0);
uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_EXTENSION : 0);
r = sd_bus_message_append(m, "st", arg_copy_mode, flags);
} else
@ -943,7 +958,7 @@ static int detach_image(int argc, char *argv[], void *userdata) {
if (streq(method, "DetachImage"))
r = sd_bus_message_append(m, "b", arg_runtime);
else {
uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_SYSEXT : 0);
uint64_t flags = (arg_runtime ? PORTABLE_RUNTIME : 0) | (arg_force ? PORTABLE_FORCE_ATTACH | PORTABLE_FORCE_EXTENSION : 0);
r = sd_bus_message_append(m, "t", flags);
}

View file

@ -814,6 +814,15 @@ EOF
echo MARKER=1 >"$initdir/usr/lib/systemd/system/some_file"
mksquashfs "$initdir" "$oldinitdir/usr/share/app0.raw" -noappend
export initdir="$TESTDIR/conf0"
mkdir -p "$initdir/etc/extension-release.d" "$initdir/etc/systemd/system" "$initdir/opt"
grep "^ID=" "$os_release" >"$initdir/etc/extension-release.d/extension-release.conf0"
echo "${version_id}" >>"$initdir/etc/extension-release.d/extension-release.conf0"
( echo "${version_id}"
echo "CONFEXT_IMAGE_ID=app" ) >>"$initdir/etc/extension-release.d/extension-release.conf0"
echo MARKER_1 >"$initdir/etc/systemd/system/some_file"
mksquashfs "$initdir" "$oldinitdir/usr/share/conf0.raw" -noappend
export initdir="$TESTDIR/app1"
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.app2"

View file

@ -33,6 +33,7 @@ systemd-dissect --no-pager /usr/share/minimal_0.raw | grep -q '✓ portable serv
systemd-dissect --no-pager /usr/share/minimal_1.raw | grep -q '✓ 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'
systemd-dissect --no-pager /usr/share/conf0.raw | grep -q '✓ confext for portable service'
export SYSTEMD_LOG_LEVEL=debug
mkdir -p /run/systemd/system/systemd-portabled.service.d/
@ -187,6 +188,17 @@ portablectl inspect --force --cat --extension /tmp/app10.raw /usr/share/minimal_
portablectl detach --force --now --runtime --extension /tmp/app10.raw /usr/share/minimal_0.raw app0
# portablectl also accepts confexts
portablectl "${ARGS[@]}" attach --now --runtime --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0
systemctl is-active app0.service
status="$(portablectl is-attached --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw)"
[[ "${status}" == "running-runtime" ]]
portablectl inspect --force --cat --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0 | grep -q -F "Extension Release: /usr/share/conf0.raw"
portablectl detach --now --runtime --extension /usr/share/app0.raw --extension /usr/share/conf0.raw /usr/share/minimal_0.raw app0
# portablectl also works with directory paths rather than images
mkdir /tmp/rootdir /tmp/app0 /tmp/app1 /tmp/overlay /tmp/os-release-fix /tmp/os-release-fix/etc