Merge pull request #26013 from goenkam/maanya/syscfg-feature

confext: extension of sysext
This commit is contained in:
Luca Boccassi
2023-04-06 10:59:18 +01:00
committed by GitHub
21 changed files with 481 additions and 161 deletions

24
TODO
View File

@@ -521,13 +521,13 @@ Features:
* add support for asymmetric LUKS2 TPM based encryption. i.e. allow preparing
an encrypted image on some host given a public key belonging to a specific
other host, so that only hosts possessing the private key in the TPM2 chip
can decrypt the volume key and activate the volume. Usecase: systemd-syscfg
for a central orchestrator to generate syscfg images securely that can only
can decrypt the volume key and activate the volume. Usecase: systemd-confext
for a central orchestrator to generate confext images securely that can only
be activated on one specific host (which can be used for installing a bunch
of creds in /etc/credstore/ for example). Extending on this: allow binding
LUKS2 TPM based encryption also to the TPM2 internal clock. Net result:
prepare a syscfg image that can only be activated on a specific host that
runs a specific software in a specific time window. syscfg would be
prepare a confext image that can only be activated on a specific host that
runs a specific software in a specific time window. confext would be
automatically invalidated outside of it.
* maybe add a "systemd-report" tool, that generates a TPM2-backed "report" of
@@ -538,17 +538,17 @@ Features:
this: have the report tool upload these reports every 3min somewhere. Then
have the orchestrator collect these reports centrally over a 3min time
window, and use them to determine what which node should now start/stop what,
and generate a small syscfg for each node, that uses Uphold= to pin services
on each node. The syscfg would be encrypted using the asymmetric encryption
and generate a small confext for each node, that uses Uphold= to pin services
on each node. The confext would be encrypted using the asymmetric encryption
proposed above, so that it can only be activated on the specific host, if the
software is in a good state, and within a specific time frame. Then run a
loop on each node that sends report to orchestrator and then sysupdate to
update syscfg. Orchestrator would be stateless, i.e. operate on desired
update confext. Orchestrator would be stateless, i.e. operate on desired
config and collected reports in the last 3min time window only, and thus can
be trivially scaled up since all instances of the orchestrator should come to
the same conclusions given the same inputs of reports/desired workload info.
Could also be used to deliver Wireguard secrets and thus to clients, thus
permitting zero-trust networking: secrets are rolled over via syscfg updates,
permitting zero-trust networking: secrets are rolled over via confext updates,
and via the time window TPM logic invalidated if node doesn't keep itself
updated, or becomes corrupted in some way.
@@ -597,7 +597,7 @@ Features:
keyring, so that the kernel does this validation for us for verity and kernel
modules
* for systemd-syscfg: add a tool that can generate suitable DDIs with verity +
* for systemd-confext: add a tool that can generate suitable DDIs with verity +
sig using squashfs-tools-ng's library. Maybe just systemd-repart called under
a new name with a built-in config?
@@ -914,12 +914,6 @@ Features:
* sysext: measure all activated sysext into a TPM PCR
* maybe add a "syscfg" concept, that is almost entirely identical to "sysext",
but operates on /etc/ instead of /usr/ and /opt/. Use case would be: trusted,
authenticated, atomic, additive configuration management primitive: drop in a
configuration bundle, and activate it, so that it is instantly visible,
comprehensively.
* systemd-dissect: show available versions inside of a disk image, i.e. if
multiple versions are around of the same resource, show which ones. (in other
words: show partition labels).

View File

@@ -328,7 +328,9 @@ the journal instead of only when logging in debug mode.
paths. Only "real" file systems and directories that only contain "real" file
systems as submounts should be used. Do not specify API file systems such as
`/proc/` or `/sys/` here, or hierarchies that have them as submounts. In
particular, do not specify the root directory `/` here.
particular, do not specify the root directory `/` here. Similarly,
`$SYSTEMD_CONFEXT_HIERARCHIES` works for confext images and supports the
systemd-confext multi-call functionality of sysext.
`systemd-tmpfiles`:

View File

@@ -442,6 +442,17 @@
</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>CONFEXT_LEVEL=</varname></term>
<listitem><para>Semantically the same as <varname>SYSEXT_LEVEL=</varname> but for confext images.
See <filename>/etc/extension-release.d/extension-release.<replaceable>IMAGE</replaceable></filename>
for more information.</para>
<para>Examples: <literal>CONFEXT_LEVEL=2</literal>, <literal>CONFEXT_LEVEL=15.14</literal>.
</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>SYSEXT_SCOPE=</varname></term>
<listitem><para>Takes a space-separated list of one or more of the strings
@@ -453,6 +464,12 @@
but not to initrd environments.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>CONFEXT_SCOPE=</varname></term>
<listitem><para>Semantically the same as <varname>SYSEXT_SCOPE=</varname> but for confext images.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>PORTABLE_PREFIXES=</varname></term>
<listitem><para>Takes a space-separated list of one or more valid prefix match strings for the

View File

@@ -1043,7 +1043,10 @@ manpages = [
'systemd-suspend-then-hibernate.service'],
''],
['systemd-sysctl.service', '8', ['systemd-sysctl'], ''],
['systemd-sysext', '8', ['systemd-sysext.service'], ''],
['systemd-sysext',
'8',
['systemd-confext', 'systemd-confext.service', 'systemd-sysext.service'],
''],
['systemd-system-update-generator', '8', [], ''],
['systemd-system.conf',
'5',

View File

@@ -19,6 +19,8 @@
<refnamediv>
<refname>systemd-sysext</refname>
<refname>systemd-sysext.service</refname>
<refname>systemd-confext</refname>
<refname>systemd-confext.service</refname>
<refpurpose>Activates System Extension Images</refpurpose>
</refnamediv>
@@ -31,6 +33,14 @@
<para><literallayout><filename>systemd-sysext.service</filename></literallayout></para>
<cmdsynopsis>
<command>systemd-confext</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="plain">COMMAND</arg>
</cmdsynopsis>
<para><literallayout><filename>systemd-confext.service</filename></literallayout></para>
</refsynopsisdiv>
<refsect1>
@@ -129,6 +139,29 @@
The <filename>extension-release</filename> file follows the same format and semantics, and carries the same
content, as the <filename>os-release</filename> file of the OS, but it describes the resources carried
in the extension image.</para>
<para>The <command>systemd-confext</command> concept follows the same principle as the
<citerefentry><refentrytitle>systemd-sysext</refentrytitle><manvolnum>1</manvolnum></citerefentry>
functionality but instead of working on <filename>/usr</filename> and <filename>/opt</filename>,
<command>confext</command> will extend only <filename>/etc</filename>. Files and directories contained
in the confext images outside of the <filename>/etc/</filename> hierarchy are <emphasis>not</emphasis>
merged, and hence have no effect when included in the image. Formats for these images are of the
same as sysext images.</para>
<para>Confexts are looked for in the directories <filename>/run/confexts/</filename>,
<filename>/var/lib/confexts/</filename>, <filename>/usr/lib/confexts/</filename> and
<filename>/usr/local/lib/confexts/</filename>. The first two listed directories are not suitable for
carrying large binary images, however are still useful for carrying symlinks to them. The primary place
for installing system extensions is <filename>/var/lib/confexts/</filename>. Any directories found in
these search directories are considered directory based confext images, any files with the
<filename>.raw</filename> suffix are considered disk image based confext images.</para>
<para>Again, just like sysext images, the confext images will contain a
<filename>/etc/extension-release.d/extension-release.<replaceable>$name</replaceable></filename>
file, which must match the image name (with the usual escape hatch of xattr), and again with content
being one or more of <varname>ID=</varname>, <varname>VERSION_ID=</varname>, and
<varname>CONFEXT_LEVEL</varname>. Confext images will then be checked and matched against the
base OS layer.</para>
</refsect1>
<refsect1>
@@ -153,20 +186,25 @@
<filename>/usr/</filename> as if it was installed in the OS image itself.) This case works regardless if
the underlying host <filename>/usr/</filename> is managed as immutable disk image or is a traditional
package manager controlled (i.e. writable) tree.</para>
</refsect1>
<para>For the confext case, the OSConfig project aims to perform runtime reconfiguration of OS services.
Sometimes, there is a need to swap certain configuration parameter values or restart only a specific
service without deployment of new code or a complete OS deployment. In other words, we want to be able
to tie the most frequently configured options to runtime updateable flags that can be changed without a
system reboot. This will help reduce servicing times when there is a need for changing the OS configuration.</para></refsect1>
<refsect1>
<title>Commands</title>
<para>The following commands are understood:</para>
<para>The following commands are understood by both the sysext and confext concepts:</para>
<variablelist>
<varlistentry>
<term><option>status</option></term>
<listitem><para>When invoked without any command verb, or when <option>status</option> is specified
the current merge status is shown, separately for both <filename>/usr/</filename> and
<filename>/opt/</filename>.</para></listitem>
the current merge status is shown, separately (for both <filename>/usr/</filename> and
<filename>/opt/</filename> of sysext and for <filename>/etc/</filename> of confext).</para></listitem>
</varlistentry>
<varlistentry>
@@ -174,14 +212,15 @@
<listitem><para>Merges all currently installed system extension images into
<filename>/usr/</filename> and <filename>/opt/</filename>, by overmounting these hierarchies with an
<literal>overlayfs</literal> file system combining the underlying hierarchies with those included in
the extension images. This command will fail if the hierarchies are already merged.</para></listitem>
the extension images. This command will fail if the hierarchies are already merged. For confext, the merge
happens into the <filename>/etc/</filename> directory instead.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>unmerge</option></term>
<listitem><para>Unmerges all currently installed system extension images from
<filename>/usr/</filename> and <filename>/opt/</filename>, by unmounting the
<literal>overlayfs</literal> file systems created by <option>merge</option>
<filename>/usr/</filename> and <filename>/opt/</filename> for sysext and <filename>/etc/</filename>,
for confext, by unmounting the <literal>overlayfs</literal> file systems created by <option>merge</option>
prior.</para></listitem>
</varlistentry>
@@ -191,11 +230,11 @@
mounted the existing <literal>overlayfs</literal> instance is unmounted temporarily, and then
replaced by a new version. This command is useful after installing/removing system extension images,
in order to update the <literal>overlayfs</literal> file system accordingly. If no system extensions
are installed when this command is executed, the equivalent of <option>unmerge</option> is
executed, without establishing any new <literal>overlayfs</literal> instance. Note that currently
there's a brief moment where neither the old nor the new <literal>overlayfs</literal> file system is
mounted. This implies that all resources supplied by a system extension will briefly disappear — even
if it exists continuously during the refresh operation.</para></listitem>
are installed when this command is executed, the equivalent of <option>unmerge</option> is executed,
without establishing any new <literal>overlayfs</literal> instance.
Note that currently there's a brief moment where neither the old nor the new <literal>overlayfs</literal>
file system is mounted. This implies that all resources supplied by a system extension will briefly
disappear — even if it exists continuously during the refresh operation.</para></listitem>
</varlistentry>
<varlistentry>
@@ -218,16 +257,17 @@
<listitem><para>Operate relative to the specified root directory, i.e. establish the
<literal>overlayfs</literal> mount not on the top-level host <filename>/usr/</filename> and
<filename>/opt/</filename> hierarchies, but below some specified root directory.</para></listitem>
<filename>/opt/</filename> hierarchies for sysext or <filename>/etc/</filename> for confext,
but below some specified root directory.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--force</option></term>
<listitem><para>When merging system extensions into <filename>/usr/</filename> and
<filename>/opt/</filename>, ignore version incompatibilities, i.e. force merging regardless of
whether the version information included in the extension images matches the host or
not.</para></listitem>
<filename>/opt/</filename> for sysext and <filename>/etc/</filename> for confext,
ignore version incompatibilities, i.e. force merging regardless of
whether the version information included in the images matches the host or not.</para></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="no-pager" />

View File

@@ -0,0 +1,85 @@
# systemd-confext(8) completion -*- shell-script -*-
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# systemd is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with systemd; If not, see <https://www.gnu.org/licenses/>.
__contains_word() {
local w word=$1; shift
for w in "$@"; do
[[ $w = "$word" ]] && return
done
}
_systemd-confext() {
local i verb comps
local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} words cword
local -A OPTS=(
[STANDALONE]='-h --help --version
--no-pager
--no-legend
--force'
[ARG]='--root
--json'
)
local -A VERBS=(
[STANDALONE]='status
merge
unmerge
refresh
list'
)
_init_completion || return
if __contains_word "$prev" ${OPTS[ARG]}; then
case $prev in
--root)
comps=$(compgen -A directory -- "$cur" )
compopt -o dirnames
;;
--json)
comps='pretty short off'
;;
esac
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
return 0
fi
if [[ "$cur" = -* ]]; then
COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") )
return 0
fi
for ((i=0; i < COMP_CWORD; i++)); do
if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} &&
! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then
verb=${COMP_WORDS[i]}
break
fi
done
if [[ -z ${verb-} ]]; then
comps=${VERBS[*]}
elif __contains_word "$verb" ${VERBS[STANDALONE]}; then
comps=''
fi
COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
return 0
}
complete -F _systemd-confext systemd-confext

View File

@@ -19,6 +19,21 @@
#include "utf8.h"
#include "xattr-util.h"
/* Helper struct for naming simplicity and reusability */
static const struct {
const char *release_file_directory;
const char *release_file_path_prefix;
} image_class_release_info[_IMAGE_CLASS_MAX] = {
[IMAGE_SYSEXT] = {
.release_file_directory = "/usr/lib/extension-release.d/",
.release_file_path_prefix = "/usr/lib/extension-release.d/extension-release.",
},
[IMAGE_CONFEXT] = {
.release_file_directory = "/etc/extension-release.d/",
.release_file_path_prefix = "/etc/extension-release.d/extension-release.",
}
};
bool image_name_is_valid(const char *s) {
if (!filename_is_valid(s))
return false;
@@ -36,10 +51,12 @@ bool image_name_is_valid(const char *s) {
return true;
}
int path_is_extension_tree(const char *path, const char *extension, bool relax_extension_release_check) {
int path_is_extension_tree(ImageClass image_class, const char *path, const char *extension, bool relax_extension_release_check) {
int r;
assert(path);
assert(image_class >= 0);
assert(image_class < _IMAGE_CLASS_MAX);
/* Does the path exist at all? If not, generate an error immediately. This is useful so that a missing root dir
* always results in -ENOENT, and we can properly distinguish the case where the whole root doesn't exist from
@@ -48,8 +65,9 @@ int path_is_extension_tree(const char *path, const char *extension, bool relax_e
return -errno;
/* We use /usr/lib/extension-release.d/extension-release[.NAME] as flag for something being a system extension,
* /etc/extension-release.d/extension-release[.NAME] as flag for something being a system configuration, and finally,
* and {/etc|/usr/lib}/os-release as a flag for something being an OS (when not an extension). */
r = open_extension_release(path, extension, relax_extension_release_check, NULL, NULL);
r = open_extension_release(path, image_class, extension, relax_extension_release_check, NULL, NULL);
if (r == -ENOENT) /* We got nothing */
return 0;
if (r < 0)
@@ -96,18 +114,21 @@ static int extension_release_strict_xattr_value(int extension_release_fd, const
return false;
}
int open_extension_release(const char *root, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd) {
int open_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd) {
_cleanup_free_ char *q = NULL;
int r, fd;
if (extension) {
assert(image_class >= 0);
assert(image_class < _IMAGE_CLASS_MAX);
const char *extension_full_path;
if (!image_name_is_valid(extension))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"The extension name %s is invalid.", extension);
extension_full_path = strjoina("/usr/lib/extension-release.d/extension-release.", extension);
extension_full_path = strjoina(image_class_release_info[image_class].release_file_path_prefix, extension);
r = chase(extension_full_path, root, CHASE_PREFIX_ROOT, ret_path ? &q : NULL, ret_fd ? &fd : NULL);
log_full_errno_zerook(LOG_DEBUG, MIN(r, 0), "Checking for %s: %m", extension_full_path);
@@ -120,10 +141,10 @@ int open_extension_release(const char *root, const char *extension, bool relax_e
_cleanup_free_ char *extension_release_dir_path = NULL;
_cleanup_closedir_ DIR *extension_release_dir = NULL;
r = chase_and_opendir("/usr/lib/extension-release.d/", root, CHASE_PREFIX_ROOT,
r = chase_and_opendir(image_class_release_info[image_class].release_file_directory, root, CHASE_PREFIX_ROOT,
&extension_release_dir_path, &extension_release_dir);
if (r < 0)
return log_debug_errno(r, "Cannot open %s/usr/lib/extension-release.d/, ignoring: %m", root);
return log_debug_errno(r, "Cannot open %s%s, ignoring: %m", root, image_class_release_info[image_class].release_file_directory);
r = -ENOENT;
FOREACH_DIRENT(de, extension_release_dir, return -errno) {
@@ -137,7 +158,7 @@ int open_extension_release(const char *root, const char *extension, bool relax_e
continue;
if (!image_name_is_valid(image_name)) {
log_debug("%s/%s is not a valid extension-release file name, ignoring.",
log_debug("%s/%s is not a valid release file name, ignoring.",
extension_release_dir_path, de->d_name);
continue;
}
@@ -149,7 +170,7 @@ int open_extension_release(const char *root, const char *extension, bool relax_e
O_PATH|O_CLOEXEC|O_NOFOLLOW);
if (extension_release_fd < 0)
return log_debug_errno(errno,
"Failed to open extension-release file %s/%s: %m",
"Failed to open release file %s/%s: %m",
extension_release_dir_path,
de->d_name);
@@ -219,16 +240,16 @@ int open_extension_release(const char *root, const char *extension, bool relax_e
return 0;
}
int fopen_extension_release(const char *root, const char *extension, bool relax_extension_release_check, char **ret_path, FILE **ret_file) {
int fopen_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, FILE **ret_file) {
_cleanup_free_ char *p = NULL;
_cleanup_close_ int fd = -EBADF;
FILE *f;
int r;
if (!ret_file)
return open_extension_release(root, extension, relax_extension_release_check, ret_path, NULL);
return open_extension_release(root, image_class, extension, relax_extension_release_check, ret_path, NULL);
r = open_extension_release(root, extension, relax_extension_release_check, ret_path ? &p : NULL, &fd);
r = open_extension_release(root, image_class, extension, relax_extension_release_check, ret_path ? &p : NULL, &fd);
if (r < 0)
return r;
@@ -243,24 +264,27 @@ int fopen_extension_release(const char *root, const char *extension, bool relax_
return 0;
}
static int parse_release_internal(const char *root, bool relax_extension_release_check, const char *extension, va_list ap) {
static int parse_release_internal(const char *root, ImageClass image_class, bool relax_extension_release_check, const char *extension, va_list ap) {
_cleanup_fclose_ FILE *f = NULL;
_cleanup_free_ char *p = NULL;
int r;
r = fopen_extension_release(root, extension, relax_extension_release_check, &p, &f);
r = fopen_extension_release(root, image_class, extension, relax_extension_release_check, &p, &f);
if (r < 0)
return r;
return parse_env_filev(f, p, ap);
}
int _parse_extension_release(const char *root, bool relax_extension_release_check, const char *extension, ...) {
int _parse_extension_release(const char *root, ImageClass image_class, bool relax_extension_release_check, const char *extension, ...) {
va_list ap;
int r;
assert(image_class >= 0);
assert(image_class < _IMAGE_CLASS_MAX);
va_start(ap, extension);
r = parse_release_internal(root, relax_extension_release_check, extension, ap);
r = parse_release_internal(root, image_class, relax_extension_release_check, extension, ap);
va_end(ap);
return r;
@@ -271,7 +295,7 @@ int _parse_os_release(const char *root, ...) {
int r;
va_start(ap, root);
r = parse_release_internal(root, /* relax_extension_release_check= */ false, NULL, ap);
r = parse_release_internal(root, -1, /* relax_extension_release_check= */ false, NULL, ap);
va_end(ap);
return r;
@@ -318,12 +342,15 @@ int load_os_release_pairs_with_prefix(const char *root, const char *prefix, char
return 0;
}
int load_extension_release_pairs(const char *root, const char *extension, bool relax_extension_release_check, char ***ret) {
int load_extension_release_pairs(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char ***ret) {
_cleanup_fclose_ FILE *f = NULL;
_cleanup_free_ char *p = NULL;
int r;
r = fopen_extension_release(root, extension, relax_extension_release_check, &p, &f);
assert(image_class >= 0);
assert(image_class < _IMAGE_CLASS_MAX);
r = fopen_extension_release(root, image_class, extension, relax_extension_release_check, &p, &f);
if (r < 0)
return r;

View File

@@ -5,33 +5,44 @@
#include <stdio.h>
#include "time-util.h"
typedef enum ImageClass {
IMAGE_MACHINE,
IMAGE_PORTABLE,
IMAGE_SYSEXT,
IMAGE_CONFEXT,
_IMAGE_CLASS_MAX,
_IMAGE_CLASS_INVALID = -EINVAL,
} ImageClass;
const char* image_class_to_string(ImageClass cl) _const_;
ImageClass image_class_from_string(const char *s) _pure_;
/* The *_extension_release flavours will look for /usr/lib/extension-release/extension-release.NAME
* in accordance with the OS extension specification, rather than for /usr/lib/ or /etc/os-release. */
bool image_name_is_valid(const char *s) _pure_;
int path_is_extension_tree(const char *path, const char *extension, bool relax_extension_release_check);
int path_is_extension_tree(ImageClass image_class, const char *path, const char *extension, bool relax_extension_release_check);
static inline int path_is_os_tree(const char *path) {
return path_is_extension_tree(path, NULL, false);
return path_is_extension_tree(IMAGE_SYSEXT, path, NULL, false);
}
int open_extension_release(const char *root, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd);
int open_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd);
static inline int open_os_release(const char *root, char **ret_path, int *ret_fd) {
return open_extension_release(root, NULL, false, ret_path, ret_fd);
return open_extension_release(root, IMAGE_SYSEXT, NULL, false, ret_path, ret_fd);
}
int fopen_extension_release(const char *root, const char *extension, bool relax_extension_release_check, char **ret_path, FILE **ret_file);
int fopen_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, FILE **ret_file);
static inline int fopen_os_release(const char *root, char **ret_path, FILE **ret_file) {
return fopen_extension_release(root, NULL, false, ret_path, ret_file);
return fopen_extension_release(root, IMAGE_SYSEXT, NULL, false, ret_path, ret_file);
}
int _parse_extension_release(const char *root, bool relax_extension_release_check, const char *extension, ...) _sentinel_;
int _parse_extension_release(const char *root, ImageClass image_class, bool relax_extension_release_check, const char *extension, ...) _sentinel_;
int _parse_os_release(const char *root, ...) _sentinel_;
#define parse_extension_release(root, relax_extension_release_check, extension, ...) _parse_extension_release(root, relax_extension_release_check, extension, __VA_ARGS__, NULL)
#define parse_extension_release(root, image_class, relax_extension_release_check, extension, ...) _parse_extension_release(root, image_class, relax_extension_release_check, extension, __VA_ARGS__, NULL)
#define parse_os_release(root, ...) _parse_os_release(root, __VA_ARGS__, NULL)
int load_extension_release_pairs(const char *root, const char *extension, bool relax_extension_release_check, char ***ret);
int load_extension_release_pairs(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char ***ret);
int load_os_release_pairs(const char *root, char ***ret);
int load_os_release_pairs_with_prefix(const char *root, const char *prefix, char ***ret);

View File

@@ -1423,7 +1423,7 @@ static int apply_one_mount(
if (isempty(host_os_release_id))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "'ID' field not found or empty in 'os-release' data of OS tree '%s': %m", empty_to_root(root_directory));
r = load_extension_release_pairs(mount_entry_source(m), extension_name, /* relax_extension_release_check= */ false, &extension_release);
r = load_extension_release_pairs(mount_entry_source(m), IMAGE_SYSEXT, extension_name, /* relax_extension_release_check= */ false, &extension_release);
if (r == -ENOENT && m->ignore)
return 0;
if (r < 0)
@@ -1435,7 +1435,8 @@ static int apply_one_mount(
host_os_release_version_id,
host_os_release_sysext_level,
/* host_sysext_scope */ NULL, /* Leave empty, we need to accept both system and portable */
extension_release);
extension_release,
IMAGE_SYSEXT);
if (r == 0)
return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "Directory %s extension-release metadata does not match the root's", extension_name);
if (r < 0)
@@ -2155,7 +2156,7 @@ int setup_namespace(
}
if (n_extension_images > 0 || !strv_isempty(extension_directories)) {
r = parse_env_extension_hierarchies(&hierarchies);
r = parse_env_extension_hierarchies(&hierarchies, "SYSTEMD_SYSEXT_HIERARCHIES");
if (r < 0)
return r;
}

View File

@@ -198,7 +198,7 @@ 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);
r = open_extension_release(where, image_name, relax_extension_release_check, &os_release_path, &os_release_fd);
r = open_extension_release(where, IMAGE_SYSEXT, image_name, relax_extension_release_check, &os_release_path, &os_release_fd);
} else {
os_release_id = "/etc/os-release";
r = open_os_release(where, &os_release_path, &os_release_fd);
@@ -607,7 +607,7 @@ static int extract_image_and_extensions(
return r;
if (validate_sysext) {
r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release);
r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release, IMAGE_SYSEXT);
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)
@@ -948,17 +948,17 @@ 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_EXTENSION] = { "SYSEXT_IMAGE_VERSION", "SYSEXT_VERSION_ID", "SYSEXT_BUILD_ID", NULL },
[IMAGE_SYSEXT] = { "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 },
[IMAGE_SYSEXT] = { "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(IN_SET(type, IMAGE_PORTABLE, IMAGE_SYSEXT));
assert(!strv_isempty((char *const *)field_ids[type]));
assert(!strv_isempty((char *const *)field_versions[type]));
assert(field_name);
@@ -1106,7 +1106,7 @@ static int install_chroot_dropin(
* still be able to identify what applies to what. */
r = append_release_log_fields(&text,
ordered_hashmap_get(extension_releases, ext->name),
IMAGE_EXTENSION,
IMAGE_SYSEXT,
"PORTABLE_EXTENSION_NAME_AND_VERSION");
if (r < 0)
return r;

View File

@@ -63,9 +63,14 @@ static const char* const image_search_path[_IMAGE_CLASS_MAX] = {
* because extension images are supposed to extend /usr/, so you get into recursive races, especially
* with directory-based extensions, as the kernel's OverlayFS explicitly checks for this and errors
* out with -ELOOP if it finds that a lowerdir= is a child of another lowerdir=. */
[IMAGE_EXTENSION] = "/etc/extensions\0" /* only place symlinks here */
"/run/extensions\0" /* and here too */
"/var/lib/extensions\0", /* the main place for images */
[IMAGE_SYSEXT] = "/etc/extensions\0" /* only place symlinks here */
"/run/extensions\0" /* and here too */
"/var/lib/extensions\0", /* the main place for images */
[IMAGE_CONFEXT] = "/run/confexts\0" /* only place symlinks here */
"/var/lib/confexts\0" /* the main place for images */
"/usr/local/lib/confexts\0"
"/usr/lib/confexts\0",
};
static Image *image_free(Image *i) {
@@ -1152,7 +1157,7 @@ int image_read_metadata(Image *i) {
_cleanup_free_ char *hostname = NULL;
_cleanup_free_ char *path = NULL;
if (i->class == IMAGE_EXTENSION) {
if (i->class == IMAGE_SYSEXT) {
r = extension_has_forbidden_content(i->path);
if (r < 0)
return r;
@@ -1190,7 +1195,7 @@ int image_read_metadata(Image *i) {
if (r < 0)
log_debug_errno(r, "Failed to read os-release in image, ignoring: %m");
r = load_extension_release_pairs(i->path, i->name, /* relax_extension_release_check= */ false, &extension_release);
r = load_extension_release_pairs(i->path, i->class, i->name, /* relax_extension_release_check= */ false, &extension_release);
if (r < 0)
log_debug_errno(r, "Failed to read extension-release in image, ignoring: %m");
@@ -1325,7 +1330,8 @@ DEFINE_STRING_TABLE_LOOKUP(image_type, ImageType);
static const char* const image_class_table[_IMAGE_CLASS_MAX] = {
[IMAGE_MACHINE] = "machine",
[IMAGE_PORTABLE] = "portable",
[IMAGE_EXTENSION] = "extension",
[IMAGE_SYSEXT] = "extension",
[IMAGE_CONFEXT] = "confext"
};
DEFINE_STRING_TABLE_LOOKUP(image_class, ImageClass);

View File

@@ -9,18 +9,11 @@
#include "hashmap.h"
#include "lock-util.h"
#include "macro.h"
#include "os-util.h"
#include "path-util.h"
#include "string-util.h"
#include "time-util.h"
typedef enum ImageClass {
IMAGE_MACHINE,
IMAGE_PORTABLE,
IMAGE_EXTENSION,
_IMAGE_CLASS_MAX,
_IMAGE_CLASS_INVALID = -EINVAL,
} ImageClass;
typedef enum ImageType {
IMAGE_DIRECTORY,
IMAGE_SUBVOLUME,
@@ -77,9 +70,6 @@ int image_read_only(Image *i, bool b);
const char* image_type_to_string(ImageType t) _const_;
ImageType image_type_from_string(const char *s) _pure_;
const char* image_class_to_string(ImageClass cl) _const_;
ImageClass image_class_from_string(const char *s) _pure_;
int image_path_lock(const char *path, int operation, LockFile *global, LockFile *local);
int image_name_lock(const char *name, int operation, LockFile *ret);

View File

@@ -1791,7 +1791,7 @@ int dissected_image_mount(
if (r < 0)
return r;
if (r == 0) {
r = path_is_extension_tree(where, m->image_name, FLAGS_SET(flags, DISSECT_IMAGE_RELAX_SYSEXT_CHECK));
r = path_is_extension_tree(IMAGE_SYSEXT, where, m->image_name, FLAGS_SET(flags, DISSECT_IMAGE_RELAX_SYSEXT_CHECK));
if (r < 0)
return r;
if (r > 0)
@@ -3054,7 +3054,7 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_
* 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, m->image_name, /* relax_extension_release_check= */ false, NULL, &fd);
r = open_extension_release(t, IMAGE_SYSEXT, m->image_name, /* relax_extension_release_check= */ false, NULL, &fd);
if (r < 0)
fd = r; /* Propagate the error. */
break;
@@ -3606,7 +3606,7 @@ int verity_dissect_and_mount(
assert(!isempty(required_host_os_release_id));
r = load_extension_release_pairs(dest, dissected_image->image_name, relax_extension_release_check, &extension_release);
r = load_extension_release_pairs(dest, IMAGE_SYSEXT, dissected_image->image_name, relax_extension_release_check, &extension_release);
if (r < 0)
return log_debug_errno(r, "Failed to parse image %s extension-release metadata: %m", dissected_image->image_name);
@@ -3616,7 +3616,8 @@ int verity_dissect_and_mount(
required_host_os_release_version_id,
required_host_os_release_sysext_level,
required_sysext_scope,
extension_release);
extension_release,
IMAGE_SYSEXT);
if (r == 0)
return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", dissected_image->image_name);
if (r < 0)

View File

@@ -13,39 +13,42 @@ int extension_release_validate(
const char *name,
const char *host_os_release_id,
const char *host_os_release_version_id,
const char *host_os_release_sysext_level,
const char *host_sysext_scope,
char **extension_release) {
const char *host_os_extension_release_level,
const char *host_extension_scope,
char **extension_release,
ImageClass image_class) {
const char *extension_release_id = NULL, *extension_release_sysext_level = NULL, *extension_architecture = NULL;
const char *extension_release_id = NULL, *extension_release_level = NULL, *extension_architecture = NULL;
const char *extension_level = image_class == IMAGE_CONFEXT ? "CONFEXT_LEVEL" : "SYSEXT_LEVEL";
const char *extension_scope = image_class == IMAGE_CONFEXT ? "CONFEXT_SCOPE" : "SYSEXT_SCOPE";
assert(name);
assert(!isempty(host_os_release_id));
/* Now that we can look into the extension image, let's see if the OS version is compatible */
/* Now that we can look into the extension/confext image, let's see if the OS version is compatible */
if (strv_isempty(extension_release)) {
log_debug("Extension '%s' carries no extension-release data, ignoring extension.", name);
log_debug("Extension '%s' carries no release data, ignoring.", name);
return 0;
}
if (host_sysext_scope) {
_cleanup_strv_free_ char **extension_sysext_scope_list = NULL;
const char *extension_sysext_scope;
if (host_extension_scope) {
_cleanup_strv_free_ char **scope_list = NULL;
const char *scope;
bool valid;
extension_sysext_scope = strv_env_pairs_get(extension_release, "SYSEXT_SCOPE");
if (extension_sysext_scope) {
extension_sysext_scope_list = strv_split(extension_sysext_scope, WHITESPACE);
if (!extension_sysext_scope_list)
scope = strv_env_pairs_get(extension_release, extension_scope);
if (scope) {
scope_list = strv_split(scope, WHITESPACE);
if (!scope_list)
return -ENOMEM;
}
/* by default extension are good for attachment in portable service and on the system */
/* By default extension are good for attachment in portable service and on the system */
valid = strv_contains(
extension_sysext_scope_list ?: STRV_MAKE("system", "portable"),
host_sysext_scope);
scope_list ?: STRV_MAKE("system", "portable"),
host_extension_scope);
if (!valid) {
log_debug("Extension '%s' is not suitable for scope %s, ignoring extension.", name, host_sysext_scope);
log_debug("Extension '%s' is not suitable for scope %s, ignoring.", name, host_extension_scope);
return 0;
}
}
@@ -54,21 +57,21 @@ int extension_release_validate(
* the future we could check if the kernel also supports 32 bit or binfmt has a translator set up for the architecture */
extension_architecture = strv_env_pairs_get(extension_release, "ARCHITECTURE");
if (!isempty(extension_architecture) && !streq(extension_architecture, "_any") &&
!streq(architecture_to_string(uname_architecture()), extension_architecture)) {
!streq(architecture_to_string(uname_architecture()), extension_architecture)) {
log_debug("Extension '%s' is for architecture '%s', but deployed on top of '%s'.",
name, extension_architecture, architecture_to_string(uname_architecture()));
name, extension_architecture, architecture_to_string(uname_architecture()));
return 0;
}
extension_release_id = strv_env_pairs_get(extension_release, "ID");
if (isempty(extension_release_id)) {
log_debug("Extension '%s' does not contain ID in extension-release but requested to match '%s' or be '_any'",
name, host_os_release_id);
log_debug("Extension '%s' does not contain ID in release file but requested to match '%s' or be '_any'",
name, host_os_release_id);
return 0;
}
/* A sysext with no host OS dependency (static binaries or scripts) can match
* '_any' host OS, and VERSION_ID or SYSEXT_LEVEL are not required anywhere */
/* A sysext(or confext) with no host OS dependency (static binaries or scripts) can match
* '_any' host OS, and VERSION_ID or SYSEXT_LEVEL(or CONFEXT_LEVEL) are not required anywhere */
if (streq(extension_release_id, "_any")) {
log_debug("Extension '%s' matches '_any' OS.", name);
return 1;
@@ -81,18 +84,18 @@ int extension_release_validate(
}
/* Rolling releases do not typically set VERSION_ID (eg: ArchLinux) */
if (isempty(host_os_release_version_id) && isempty(host_os_release_sysext_level)) {
if (isempty(host_os_release_version_id) && isempty(host_os_extension_release_level)) {
log_debug("No version info on the host (rolling release?), but ID in %s matched.", name);
return 1;
}
/* If the extension has a sysext API level declared, then it must match the host API
* level. Otherwise, compare OS version as a whole */
extension_release_sysext_level = strv_env_pairs_get(extension_release, "SYSEXT_LEVEL");
if (!isempty(host_os_release_sysext_level) && !isempty(extension_release_sysext_level)) {
if (!streq_ptr(host_os_release_sysext_level, extension_release_sysext_level)) {
log_debug("Extension '%s' is for sysext API level '%s', but running on sysext API level '%s'",
name, strna(extension_release_sysext_level), strna(host_os_release_sysext_level));
extension_release_level = strv_env_pairs_get(extension_release, extension_level);
if (!isempty(host_os_extension_release_level) && !isempty(extension_release_level)) {
if (!streq_ptr(host_os_extension_release_level, extension_release_level)) {
log_debug("Extension '%s' is for API level '%s', but running on API level '%s'",
name, strna(extension_release_level), strna(host_os_extension_release_level));
return 0;
}
} else if (!isempty(host_os_release_version_id)) {
@@ -100,7 +103,7 @@ int extension_release_validate(
extension_release_version_id = strv_env_pairs_get(extension_release, "VERSION_ID");
if (isempty(extension_release_version_id)) {
log_debug("Extension '%s' does not contain VERSION_ID in extension-release but requested to match '%s'",
log_debug("Extension '%s' does not contain VERSION_ID in release file but requested to match '%s'",
name, strna(host_os_release_version_id));
return 0;
}
@@ -110,7 +113,7 @@ int extension_release_validate(
name, strna(extension_release_version_id), strna(host_os_release_version_id));
return 0;
}
} else if (isempty(host_os_release_version_id) && isempty(host_os_release_sysext_level)) {
} else if (isempty(host_os_release_version_id) && isempty(host_os_extension_release_level)) {
/* Rolling releases do not typically set VERSION_ID (eg: ArchLinux) */
log_debug("No version info on the host (rolling release?), but ID in %s matched.", name);
return 1;
@@ -120,16 +123,21 @@ int extension_release_validate(
return 1;
}
int parse_env_extension_hierarchies(char ***ret_hierarchies) {
int parse_env_extension_hierarchies(char ***ret_hierarchies, const char *hierarchy_env) {
_cleanup_free_ char **l = NULL;
int r;
r = getenv_path_list("SYSTEMD_SYSEXT_HIERARCHIES", &l);
assert(hierarchy_env);
r = getenv_path_list(hierarchy_env, &l);
if (r == -ENXIO) {
/* Default when unset */
l = strv_new("/usr", "/opt");
if (!l)
return -ENOMEM;
if (streq(hierarchy_env, "SYSTEMD_CONFEXT_HIERARCHIES"))
/* Default for confext when unset */
l = strv_new("/etc");
else if (streq(hierarchy_env, "SYSTEMD_SYSEXT_HIERARCHIES"))
/* Default for sysext when unset */
l = strv_new("/usr", "/opt");
else
return -ENXIO;
} else if (r < 0)
return r;

View File

@@ -1,6 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "os-util.h"
/* Given an image name (for logging purposes), a set of os-release values from the host and a key-value pair
* vector of extension-release variables, check that the distro and (system extension level or distro
* version) match and return 1, and 0 otherwise. */
@@ -8,12 +10,13 @@ int extension_release_validate(
const char *name,
const char *host_os_release_id,
const char *host_os_release_version_id,
const char *host_os_release_sysext_level,
const char *host_sysext_scope,
char **extension_release);
const char *host_os_extension_release_level,
const char *host_extension_scope,
char **extension_release,
ImageClass image_class);
/* Parse SYSTEMD_SYSEXT_HIERARCHIES and if not set, return "/usr /opt" */
int parse_env_extension_hierarchies(char ***ret_hierarchies);
/* Parse hierarchy variables and if not set, return "/usr /opt" for sysext and "/etc" for confext */
int parse_env_extension_hierarchies(char ***ret_hierarchies, const char *hierarchy_env);
/* Insist that extension images do not overwrite the underlying OS release file (it's fine if they place one
* in /etc/os-release, i.e. where things don't matter, as they aren't merged.) */

View File

@@ -1,3 +1,7 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
systemd_sysext_sources = files('sysext.c')
meson.add_install_script(meson_make_symlink,
rootbindir / 'systemd-sysext',
rootbindir / 'systemd-confext')

View File

@@ -39,16 +39,49 @@
#include "user-util.h"
#include "verbs.h"
static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default */
static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default for sysext and /etc by default for confext */
static char *arg_root = NULL;
static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_force = false;
/* Is set to IMAGE_CONFEXT when systemd is called with the confext functionality instead of the default */
static ImageClass arg_image_class = IMAGE_SYSEXT;
STATIC_DESTRUCTOR_REGISTER(arg_hierarchies, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
/* Helper struct for naming simplicity and reusability */
static const struct {
const char *dot_directory_name;
const char *directory_name;
const char *short_identifier;
const char *short_identifier_plural;
const char *level_env;
const char *scope_env;
const char *name_env;
} image_class_info[_IMAGE_CLASS_MAX] = {
[IMAGE_SYSEXT] = {
.dot_directory_name = ".systemd-sysext",
.directory_name = "systemd-sysext",
.short_identifier = "sysext",
.short_identifier_plural = "extensions",
.level_env = "SYSEXT_LEVEL",
.scope_env = "SYSEXT_SCOPE",
.name_env = "SYSTEMD_SYSEXT_HIERARCHIES",
},
[IMAGE_CONFEXT] = {
.dot_directory_name = ".systemd-confext",
.directory_name = "systemd-confext",
.short_identifier = "confext",
.short_identifier_plural = "confexts",
.level_env = "CONFEXT_LEVEL",
.scope_env = "CONFEXT_SCOPE",
.name_env = "SYSTEMD_CONFEXT_HIERARCHIES",
}
};
static int is_our_mount_point(const char *p) {
_cleanup_free_ char *buf = NULL, *f = NULL;
struct stat st;
@@ -70,26 +103,26 @@ static int is_our_mount_point(const char *p) {
/* So we know now that it's a mount point. Now let's check if it's one of ours, so that we don't
* accidentally unmount the user's own /usr/ but just the mounts we established ourselves. We do this
* check by looking into the metadata directory we place in merged mounts: if the file
* .systemd-sysext/dev contains the major/minor device pair of the mount we have a good reason to
* ../dev contains the major/minor device pair of the mount we have a good reason to
* believe this is one of our mounts. This thorough check has the benefit that we aren't easily
* confused if people tar up one of our merged trees and untar them elsewhere where we might mistake
* them for a live sysext tree. */
f = path_join(p, ".systemd-sysext/dev");
f = path_join(p, image_class_info[arg_image_class].dot_directory_name, "dev");
if (!f)
return log_oom();
r = read_one_line_file(f, &buf);
if (r == -ENOENT) {
log_debug("Hierarchy '%s' does not carry a .systemd-sysext/dev file, not a sysext merged tree.", p);
log_debug("Hierarchy '%s' does not carry a %s/dev file, not a merged tree.", p, image_class_info[arg_image_class].dot_directory_name);
return false;
}
if (r < 0)
return log_error_errno(r, "Failed to determine whether hierarchy '%s' contains '.systemd-sysext/dev': %m", p);
return log_error_errno(r, "Failed to determine whether hierarchy '%s' contains '%s/dev': %m", p, image_class_info[arg_image_class].dot_directory_name);
r = parse_devnum(buf, &dev);
if (r < 0)
return log_error_errno(r, "Failed to parse device major/minor stored in '.systemd-sysext/dev' file on '%s': %m", p);
return log_error_errno(r, "Failed to parse device major/minor stored in '%s/dev' file on '%s': %m", image_class_info[arg_image_class].dot_directory_name, p);
if (lstat(p, &st) < 0)
return log_error_errno(r, "Failed to stat %s: %m", p);
@@ -205,7 +238,7 @@ static int verb_status(int argc, char **argv, void *userdata) {
continue;
}
f = path_join(*p, ".systemd-sysext/extensions");
f = path_join(*p, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural);
if (!f)
return log_oom();
@@ -272,7 +305,7 @@ static int mount_overlayfs(
}
/* Now mount the actual overlayfs */
r = mount_nofollow_verbose(LOG_ERR, "sysext", where, "overlay", MS_RDONLY, options);
r = mount_nofollow_verbose(LOG_ERR, image_class_info[arg_image_class].short_identifier, where, "overlay", MS_RDONLY, options);
if (r < 0)
return r;
@@ -315,7 +348,7 @@ static int merge_hierarchy(
/* Let's generate a metadata file that lists all extensions we took into account for this
* hierarchy. We include this in the final fs, to make things nicely discoverable and
* recognizable. */
f = path_join(meta_path, ".systemd-sysext/extensions");
f = path_join(meta_path, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural);
if (!f)
return log_oom();
@@ -383,12 +416,12 @@ static int merge_hierarchy(
/* Now we have mounted the new file system. Let's now figure out its .st_dev field, and make that
* available in the metadata directory. This is useful to detect whether the metadata dir actually
* belongs to the fs it is found on: if .st_dev of the top-level mount matches it, it's pretty likely
* we are looking at a live sysext tree, and not an unpacked tar or so of one. */
* we are looking at a live tree, and not an unpacked tar or so of one. */
if (stat(overlay_path, &st) < 0)
return log_error_errno(r, "Failed to stat mount '%s': %m", overlay_path);
free(f);
f = path_join(meta_path, ".systemd-sysext/dev");
f = path_join(meta_path, image_class_info[arg_image_class].dot_directory_name, "dev");
if (!f)
return log_oom();
@@ -410,7 +443,7 @@ static int strverscmp_improvedp(char *const* a, char *const* b) {
static int merge_subprocess(Hashmap *images, const char *workspace) {
_cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, *host_os_release_sysext_level = NULL,
*buf = NULL;
*host_os_release_confext_level = NULL, *buf = NULL;
_cleanup_strv_free_ char **extensions = NULL, **paths = NULL;
size_t n_extensions = 0;
unsigned n_ignored = 0;
@@ -437,11 +470,13 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
return r;
/* Acquire host OS release info, so that we can compare it with the extension's data */
char **host_os_release_level = (arg_image_class == IMAGE_CONFEXT) ? &host_os_release_confext_level : &host_os_release_sysext_level;
r = parse_os_release(
arg_root,
"ID", &host_os_release_id,
"VERSION_ID", &host_os_release_version_id,
"SYSEXT_LEVEL", &host_os_release_sysext_level);
image_class_info[arg_image_class].level_env,
host_os_release_level);
if (r < 0)
return log_error_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root));
if (isempty(host_os_release_id))
@@ -453,7 +488,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
HASHMAP_FOREACH(img, images) {
_cleanup_free_ char *p = NULL;
p = path_join(workspace, "extensions", img->name);
p = path_join(workspace, image_class_info[arg_image_class].short_identifier_plural, img->name);
if (!p)
return log_oom();
@@ -574,7 +609,8 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
host_os_release_version_id,
host_os_release_sysext_level,
in_initrd() ? "initrd" : "system",
img->extension_release);
img->extension_release,
arg_image_class);
if (r < 0)
return r;
if (r == 0) {
@@ -619,7 +655,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
assert_se(img = hashmap_get(images, extensions[n_extensions - 1 - k]));
p = path_join(workspace, "extensions", img->name);
p = path_join(workspace, image_class_info[arg_image_class].short_identifier_plural, img->name);
if (!p)
return log_oom();
@@ -695,7 +731,7 @@ static int merge(Hashmap *images) {
pid_t pid;
int r;
r = safe_fork("(sd-sysext)", FORK_DEATHSIG|FORK_LOG|FORK_NEW_MOUNTNS, &pid);
r = safe_fork("(sd-merge)", FORK_DEATHSIG|FORK_LOG|FORK_NEW_MOUNTNS, &pid);
if (r < 0)
return log_error_errno(r, "Failed to fork off child: %m");
if (r == 0) {
@@ -711,7 +747,7 @@ static int merge(Hashmap *images) {
_exit(r > 0 ? EXIT_SUCCESS : 123); /* 123 means: didn't find any extensions */
}
r = wait_for_terminate_and_check("(sd-sysext)", pid, WAIT_LOG_ABNORMAL);
r = wait_for_terminate_and_check("(sd-merge)", pid, WAIT_LOG_ABNORMAL);
if (r < 0)
return r;
@@ -729,9 +765,9 @@ static int image_discover_and_read_metadata(Hashmap **ret_images) {
if (!images)
return log_oom();
r = image_discover(IMAGE_EXTENSION, arg_root, images);
r = image_discover(arg_image_class, arg_root, images);
if (r < 0)
return log_error_errno(r, "Failed to discover extension images: %m");
return log_error_errno(r, "Failed to discover images: %m");
HASHMAP_FOREACH(img, images) {
r = image_read_metadata(img);
@@ -832,9 +868,9 @@ static int verb_list(int argc, char **argv, void *userdata) {
if (!images)
return log_oom();
r = image_discover(IMAGE_EXTENSION, arg_root, images);
r = image_discover(arg_image_class, arg_root, images);
if (r < 0)
return log_error_errno(r, "Failed to discover extension images: %m");
return log_error_errno(r, "Failed to discover images: %m");
if ((arg_json_format_flags & JSON_FORMAT_OFF) && hashmap_isempty(images)) {
log_info("No OS extensions found.");
@@ -870,11 +906,11 @@ static int verb_help(int argc, char **argv, void *userdata) {
return log_oom();
printf("%1$s [OPTIONS...] COMMAND\n"
"\n%5$sMerge extension images into /usr/ and /opt/ hierarchies.%6$s\n"
"\n%3$sCommands:%4$s\n"
"\n%5$sMerge extension images into /usr/ and /opt/ hierarchies for\n"
" sysext and into the /etc/ hierarchy for confext.%6$s\n"
" status Show current merge status (default)\n"
" merge Merge extensions into /usr/ and /opt/\n"
" unmerge Unmerge extensions from /usr/ and /opt/\n"
" merge Merge extensions into relevant hierarchies\n"
" unmerge Unmerge extensions from relevant hierarchies\n"
" refresh Unmerge/merge extensions again\n"
" list List installed extensions\n"
" -h --help Show this help\n"
@@ -986,6 +1022,7 @@ static int sysext_main(int argc, char *argv[]) {
static int run(int argc, char *argv[]) {
int r;
arg_image_class = invoked_as(argv, "systemd-confext") ? IMAGE_CONFEXT : IMAGE_SYSEXT;
log_setup();
@@ -996,9 +1033,9 @@ static int run(int argc, char *argv[]) {
/* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and
* /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline
* switch. */
r = parse_env_extension_hierarchies(&arg_hierarchies);
r = parse_env_extension_hierarchies(&arg_hierarchies, image_class_info[arg_image_class].name_env);
if (r < 0)
return log_error_errno(r, "Failed to parse $SYSTEMD_SYSEXT_HIERARCHIES environment variable: %m");
return log_error_errno(r, "Failed to parse environment variable: %m");
return sysext_main(argc, argv);
}

View File

@@ -1,13 +1,17 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <errno.h>
#include "fileio.h"
#include "fs-util.h"
#include "log.h"
#include "mkdir.h"
#include "os-util.h"
#include "path-util.h"
#include "rm-rf.h"
#include "string-util.h"
#include "strv.h"
#include "tests.h"
#include "tmpfile-util.h"
TEST(path_is_os_tree) {
assert_se(path_is_os_tree("/") > 0);
@@ -55,6 +59,48 @@ TEST(parse_os_release) {
assert_se(unsetenv("SYSTEMD_OS_RELEASE") == 0);
}
TEST(parse_extension_release) {
/* Let's assume that we have a valid extension image */
_cleanup_free_ char *id = NULL, *version_id = NULL, *foobar = NULL, *a = NULL, *b = NULL;
_cleanup_(rm_rf_physical_and_freep) char *tempdir = NULL;
int r = mkdtemp_malloc("/tmp/test-os-util.XXXXXX", &tempdir);
if (r < 0)
log_error_errno(r, "Failed to setup working directory: %m");
assert_se(a = path_join(tempdir, "/usr/lib/extension-release.d/extension-release.test"));
assert_se(mkdir_parents(a, 0777) >= 0);
r = write_string_file(a, "ID=the-id \n VERSION_ID=the-version-id", WRITE_STRING_FILE_CREATE);
if (r < 0)
log_error_errno(r, "Failed to write file: %m");
assert_se(parse_extension_release(tempdir, IMAGE_SYSEXT, false, "test", "ID", &id, "VERSION_ID", &version_id) == 0);
log_info("ID: %s VERSION_ID: %s", id, version_id);
assert_se(streq(id, "the-id"));
assert_se(streq(version_id, "the-version-id"));
assert_se(b = path_join(tempdir, "/etc/extension-release.d/extension-release.tester"));
assert_se(mkdir_parents(b, 0777) >= 0);
r = write_string_file(b, "ID=\"ignored\" \n ID=\"the-id\" \n VERSION_ID='the-version-id'", WRITE_STRING_FILE_CREATE);
if (r < 0)
log_error_errno(r, "Failed to write file: %m");
assert_se(parse_extension_release(tempdir, IMAGE_CONFEXT, false, "tester", "ID", &id, "VERSION_ID", &version_id) == 0);
log_info("ID: %s VERSION_ID: %s", id, version_id);
assert_se(streq(id, "the-id"));
assert_se(streq(version_id, "the-version-id"));
assert_se(parse_extension_release(tempdir, IMAGE_CONFEXT, false, "tester", "FOOBAR", &foobar) == 0);
log_info("FOOBAR: %s", strnull(foobar));
assert_se(foobar == NULL);
assert_se(parse_extension_release(tempdir, IMAGE_SYSEXT, false, "test", "FOOBAR", &foobar) == 0);
log_info("FOOBAR: %s", strnull(foobar));
assert_se(foobar == NULL);
}
TEST(load_os_release_pairs) {
_cleanup_(unlink_tempfilep) char tmpfile[] = "/tmp/test-os-util.XXXXXX";
assert_se(write_tmpfile(tmpfile,

View File

@@ -483,6 +483,17 @@ test ! -e "/dev/loop/by-ref/$name"
systemd-dissect --detach "${image}.raw"
(! systemd-dissect --detach "${image}.raw")
# check for confext functionality
mkdir -p /run/confexts/test/etc/extension-release.d
echo "ID=_any" >/run/confexts/test/etc/extension-release.d/extension-release.test
echo "ARCHITECTURE=_any" >>/run/confexts/test/etc/extension-release.d/extension-release.test
echo "MARKER_CONFEXT_123" >/run/confexts/test/etc/testfile
systemd-confext merge
grep -q -F "MARKER_CONFEXT_123" /etc/testfile
systemd-confext status
systemd-confext unmerge
rm -rf /run/confexts/
echo OK >/testok
exit 0

View File

@@ -138,6 +138,7 @@ units = [
['systemd-reboot.service', ''],
['systemd-rfkill.socket', 'ENABLE_RFKILL'],
['systemd-sysext.service', 'ENABLE_SYSEXT'],
['systemd-confext.service', 'ENABLE_SYSEXT'],
['systemd-sysupdate.timer', 'ENABLE_SYSUPDATE'],
['systemd-sysupdate-reboot.timer', 'ENABLE_SYSUPDATE'],
['systemd-sysusers.service', 'ENABLE_SYSUSERS',

Some files were not shown because too many files have changed in this diff Show More