diff --git a/TODO b/TODO
index b0e4665ad8..73edc66d78 100644
--- a/TODO
+++ b/TODO
@@ -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).
diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md
index 3ec5573ff9..f29ca9226b 100644
--- a/docs/ENVIRONMENT.md
+++ b/docs/ENVIRONMENT.md
@@ -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`:
diff --git a/man/os-release.xml b/man/os-release.xml
index e74f27b990..6cc786acf9 100644
--- a/man/os-release.xml
+++ b/man/os-release.xml
@@ -442,6 +442,17 @@
+
+ CONFEXT_LEVEL=
+
+ Semantically the same as SYSEXT_LEVEL= but for confext images.
+ See /etc/extension-release.d/extension-release.IMAGE
+ for more information.
+
+ Examples: CONFEXT_LEVEL=2, CONFEXT_LEVEL=15.14.
+
+
+
SYSEXT_SCOPE=Takes a space-separated list of one or more of the strings
@@ -453,6 +464,12 @@
but not to initrd environments.
+
+ CONFEXT_SCOPE=
+
+ Semantically the same as SYSEXT_SCOPE= but for confext images.
+
+
PORTABLE_PREFIXES=Takes a space-separated list of one or more valid prefix match strings for the
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 63a68c3211..b6c88db390 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -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',
diff --git a/man/systemd-sysext.xml b/man/systemd-sysext.xml
index 96e40ddf95..f3a12e0a1c 100644
--- a/man/systemd-sysext.xml
+++ b/man/systemd-sysext.xml
@@ -19,6 +19,8 @@
systemd-sysextsystemd-sysext.service
+ systemd-confext
+ systemd-confext.serviceActivates System Extension Images
@@ -31,6 +33,14 @@
systemd-sysext.service
+
+ systemd-confext
+ OPTIONS
+ COMMAND
+
+
+ systemd-confext.service
+
@@ -129,6 +139,29 @@
The extension-release file follows the same format and semantics, and carries the same
content, as the os-release file of the OS, but it describes the resources carried
in the extension image.
+
+ The systemd-confext concept follows the same principle as the
+ systemd-sysext1
+ functionality but instead of working on /usr and /opt,
+ confext will extend only /etc. Files and directories contained
+ in the confext images outside of the /etc/ hierarchy are not
+ merged, and hence have no effect when included in the image. Formats for these images are of the
+ same as sysext images.
+
+ Confexts are looked for in the directories /run/confexts/,
+ /var/lib/confexts/, /usr/lib/confexts/ and
+ /usr/local/lib/confexts/. 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 /var/lib/confexts/. Any directories found in
+ these search directories are considered directory based confext images, any files with the
+ .raw suffix are considered disk image based confext images.
+
+ Again, just like sysext images, the confext images will contain a
+ /etc/extension-release.d/extension-release.$name
+ file, which must match the image name (with the usual escape hatch of xattr), and again with content
+ being one or more of ID=, VERSION_ID=, and
+ CONFEXT_LEVEL. Confext images will then be checked and matched against the
+ base OS layer.
@@ -153,20 +186,25 @@
/usr/ as if it was installed in the OS image itself.) This case works regardless if
the underlying host /usr/ is managed as immutable disk image or is a traditional
package manager controlled (i.e. writable) tree.
-
+
+ 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.Commands
- The following commands are understood:
+ The following commands are understood by both the sysext and confext concepts:When invoked without any command verb, or when is specified
- the current merge status is shown, separately for both /usr/ and
- /opt/.
+ the current merge status is shown, separately (for both /usr/ and
+ /opt/ of sysext and for /etc/ of confext).
@@ -174,14 +212,15 @@
Merges all currently installed system extension images into
/usr/ and /opt/, by overmounting these hierarchies with an
overlayfs file system combining the underlying hierarchies with those included in
- the extension images. This command will fail if the hierarchies are already merged.
+ the extension images. This command will fail if the hierarchies are already merged. For confext, the merge
+ happens into the /etc/ directory instead.
Unmerges all currently installed system extension images from
- /usr/ and /opt/, by unmounting the
- overlayfs file systems created by
+ /usr/ and /opt/ for sysext and /etc/,
+ for confext, by unmounting the overlayfs file systems created by
prior.
@@ -191,11 +230,11 @@
mounted the existing overlayfs 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 overlayfs file system accordingly. If no system extensions
- are installed when this command is executed, the equivalent of is
- executed, without establishing any new overlayfs instance. Note that currently
- there's a brief moment where neither the old nor the new overlayfs 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.
+ are installed when this command is executed, the equivalent of is executed,
+ without establishing any new overlayfs instance.
+ Note that currently there's a brief moment where neither the old nor the new overlayfs
+ 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.
@@ -218,16 +257,17 @@
Operate relative to the specified root directory, i.e. establish the
overlayfs mount not on the top-level host /usr/ and
- /opt/ hierarchies, but below some specified root directory.
+ /opt/ hierarchies for sysext or /etc/ for confext,
+ but below some specified root directory.
When merging system extensions into /usr/ and
- /opt/, ignore version incompatibilities, i.e. force merging regardless of
- whether the version information included in the extension images matches the host or
- not.
+ /opt/ for sysext and /etc/ for confext,
+ ignore version incompatibilities, i.e. force merging regardless of
+ whether the version information included in the images matches the host or not.
diff --git a/shell-completion/bash/systemd-confext b/shell-completion/bash/systemd-confext
new file mode 100644
index 0000000000..c8eca3b2cb
--- /dev/null
+++ b/shell-completion/bash/systemd-confext
@@ -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 .
+
+__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
diff --git a/src/basic/os-util.c b/src/basic/os-util.c
index c8b23b10e4..8cb8d9302b 100644
--- a/src/basic/os-util.c
+++ b/src/basic/os-util.c
@@ -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;
diff --git a/src/basic/os-util.h b/src/basic/os-util.h
index 3bafeaeb92..2f641ba9cd 100644
--- a/src/basic/os-util.h
+++ b/src/basic/os-util.h
@@ -5,33 +5,44 @@
#include
#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);
diff --git a/src/core/namespace.c b/src/core/namespace.c
index 8b141a2484..a71beeb18b 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -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;
}
diff --git a/src/portable/portable.c b/src/portable/portable.c
index adfd846bab..f3fc06f6fd 100644
--- a/src/portable/portable.c
+++ b/src/portable/portable.c
@@ -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;
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index eed0a5629e..0343d2e20b 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -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);
diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h
index 3c6928619c..342b161577 100644
--- a/src/shared/discover-image.h
+++ b/src/shared/discover-image.h
@@ -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);
diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c
index 6000af0ce0..a68610b80e 100644
--- a/src/shared/dissect-image.c
+++ b/src/shared/dissect-image.c
@@ -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)
diff --git a/src/shared/extension-util.c b/src/shared/extension-util.c
index fa83f6b6fd..43a19bf262 100644
--- a/src/shared/extension-util.c
+++ b/src/shared/extension-util.c
@@ -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;
diff --git a/src/shared/extension-util.h b/src/shared/extension-util.h
index fba8acaf19..3cad219d51 100644
--- a/src/shared/extension-util.h
+++ b/src/shared/extension-util.h
@@ -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.) */
diff --git a/src/sysext/meson.build b/src/sysext/meson.build
index f159adb8cc..ede953712c 100644
--- a/src/sysext/meson.build
+++ b/src/sysext/meson.build
@@ -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')
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index 5632b72f3d..d235073743 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -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);
}
diff --git a/src/test/test-os-util.c b/src/test/test-os-util.c
index bc9e3ec91b..94680389fd 100644
--- a/src/test/test-os-util.c
+++ b/src/test/test-os-util.c
@@ -1,13 +1,17 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include
-
+#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,
diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh
index 546a915a2e..3af90a69c6 100755
--- a/test/units/testsuite-50.sh
+++ b/test/units/testsuite-50.sh
@@ -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
diff --git a/units/meson.build b/units/meson.build
index 06d68c1d6b..e6eb300661 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -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',
diff --git a/units/systemd-confext.service b/units/systemd-confext.service
new file mode 100644
index 0000000000..b9d0b21508
--- /dev/null
+++ b/units/systemd-confext.service
@@ -0,0 +1,33 @@
+# 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.
+
+[Unit]
+Description=Merge System Configuration Images into /etc/
+Documentation=man:systemd-confext.service(8)
+
+ConditionCapability=CAP_SYS_ADMIN
+ConditionDirectoryNotEmpty=|/run/confexts
+ConditionDirectoryNotEmpty=|/var/lib/confexts
+ConditionDirectoryNotEmpty=|/usr/local/lib/confexts
+ConditionDirectoryNotEmpty=|/usr/lib/confexts
+
+DefaultDependencies=no
+After=local-fs.target
+Before=sysinit.target systemd-tmpfiles-setup.service
+Conflicts=shutdown.target initrd-switch-root.target
+Before=shutdown.target initrd-switch-root.target
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=systemd-confext merge
+ExecStop=systemd-confext unmerge
+
+[Install]
+WantedBy=sysinit.target