diff --git a/man/systemd-vpick.xml b/man/systemd-vpick.xml
index 010701637e6..a0a6df04b65 100644
--- a/man/systemd-vpick.xml
+++ b/man/systemd-vpick.xml
@@ -147,6 +147,18 @@
+
+
+
+ Takes a boolean argument. If true (the default), entries of the
+ .v/ directory whose user.uapi.match_… extended attributes do
+ not match the local system are skipped, see
+ systemd.v7 for
+ details. If false, these extended attributes are ignored.
+
+
+
+
diff --git a/man/systemd.v.xml b/man/systemd.v.xml
index 9bdf775378a..bb345fd697f 100644
--- a/man/systemd.v.xml
+++ b/man/systemd.v.xml
@@ -106,6 +106,26 @@
version comparison.
The rest of the variable part is the version string.
+
+ Entries may be restricted to specific systems via the extended attributes
+ user.uapi.match_hostname, user.uapi.match_machine_id,
+ user.uapi.match_machine_tags, user.uapi.match_image_id,
+ user.uapi.match_image_version, user.uapi.match_os_id and
+ user.uapi.match_os_version, which are matched against the hostname, the machine
+ ID, the machine tags (see TAGS= in
+ machine-info5),
+ and the IMAGE_ID=, IMAGE_VERSION=, ID= and
+ VERSION_ID= fields of
+ os-release5 of
+ the local system. Entries carrying any of these extended attributes that do not match the local
+ system (or that cannot be parsed) are filtered and not considered for the version comparison. The
+ syntax of these extended attributes is described in the documentation of the
+ MatchXAttr= setting in
+ sysupdate.d5:
+ systemd-sysupdate sets them on the files it installs, if they are defined for
+ them in the UAPI.16 manifest the files are downloaded from. Note that entries that are explicitly
+ selected by their full path (i.e. without making use of the .v/ logic) are not
+ filtered this way.
Or in other words, the files in the .v/ directories should follow one of these
diff --git a/shell-completion/bash/systemd-vpick b/shell-completion/bash/systemd-vpick
index 7c67c0f09e8..bc5087beeab 100644
--- a/shell-completion/bash/systemd-vpick
+++ b/shell-completion/bash/systemd-vpick
@@ -30,17 +30,23 @@ _systemd-vpick() {
local -A OPTS=(
[STANDALONE]='--help -h --version'
- [ARG]='-B --baseline -V -A -S --suffix --type -p --print --resolve'
+ [ARG]='-B --basename -V -A -S --suffix -t --type -p --print --resolve --match-xattr'
)
_init_completion || return
if __contains_word "$prev" ${OPTS[ARG]}; then
case $prev in
+ -A)
+ comps="native secondary uname auto $(systemd-analyze --no-legend --no-pager architectures 2>/dev/null | { while read -r a _; do echo " $a"; done; })"
+ ;;
+ -t|--type)
+ comps='reg dir lnk chr blk fifo sock'
+ ;;
-p|--print)
comps='filename version type arch tries all'
;;
- --resolve)
+ --resolve|--match-xattr)
comps='yes no'
;;
esac
diff --git a/shell-completion/zsh/_systemd-vpick b/shell-completion/zsh/_systemd-vpick
new file mode 100644
index 00000000000..ec105385a0e
--- /dev/null
+++ b/shell-completion/zsh/_systemd-vpick
@@ -0,0 +1,22 @@
+#compdef systemd-vpick
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+(( $+functions[_systemd-vpick_architectures] )) ||
+_systemd-vpick_architectures() {
+ local -a _architectures
+ _architectures=( native secondary uname auto $(systemd-analyze --quiet --no-pager --no-legend architectures 2>/dev/null | { while read -r a _; do echo "$a"; done; }) )
+ _describe -t architectures 'architecture' _architectures
+}
+
+_arguments \
+ '(- *)'{-h,--help}'[Show help text]' \
+ '(- *)--version[Show package version]' \
+ '(-B --basename)'{-B+,--basename=}'[Look for specified basename]:basename' \
+ '-V+[Look for specified version]:version' \
+ '-A+[Look for specified architecture]:architecture:_systemd-vpick_architectures' \
+ '(-S --suffix)'{-S+,--suffix=}'[Look for specified suffix]:suffix' \
+ '*'{-t+,--type=}'[Look for specified inode type]:type:(reg dir lnk chr blk fifo sock)' \
+ "--match-xattr=[Skip entries whose user.uapi.match_* xattrs don't match the system]:boolean:(yes no)" \
+ '(-p --print)'{-p+,--print=}'[Print selected WHAT rather than path]:what:(filename version type arch tries all)' \
+ '--resolve=[Canonicalize the result path]:boolean:(yes no)' \
+ '*:path:_files'
diff --git a/shell-completion/zsh/meson.build b/shell-completion/zsh/meson.build
index cd1e1a7a482..8a02fd46461 100644
--- a/shell-completion/zsh/meson.build
+++ b/shell-completion/zsh/meson.build
@@ -64,6 +64,7 @@ foreach item : [
['_systemd-sysupdate', 'ENABLE_SYSUPDATE'],
['_systemd-tmpfiles', 'ENABLE_TMPFILES'],
['_systemd-vmspawn', 'ENABLE_VMSPAWN'],
+ ['_systemd-vpick', ''],
['_timedatectl', 'ENABLE_TIMEDATED'],
['_udevadm', ''],
['_updatectl', 'ENABLE_SYSUPDATED'],
diff --git a/src/bootctl/bootctl-link.c b/src/bootctl/bootctl-link.c
index d5dec50e546..dbb4cfe0e23 100644
--- a/src/bootctl/bootctl-link.c
+++ b/src/bootctl/bootctl-link.c
@@ -1194,6 +1194,7 @@ static int link_context_add_extras(LinkContext *c, int uki_dir_fd, Set **seen) {
de->d_name,
&filter,
/* n_filters= */ 1,
+ /* match_root= */ c->root,
/* flags= */ 0,
&pick);
if (r == -ENOENT || r == 0) {
@@ -1249,6 +1250,7 @@ static int link_context_find_kernel(LinkContext *c, int uki_dir_fd) {
"kernel.efi.v",
&pick_filter_uki,
/* n_filters= */ 1,
+ /* match_root= */ c->root,
/* flags= */ 0,
&pick);
if (r == -ENOENT || r == 0)
diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c
index 2e3d106ff43..58ef65cbdee 100644
--- a/src/core/exec-invoke.c
+++ b/src/core/exec-invoke.c
@@ -3983,6 +3983,7 @@ static int pin_rootfs(
context->root_image,
pick_filter_image_raw,
ELEMENTSOF(pick_filter_image_raw),
+ /* match_root= */ NULL,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0) {
@@ -4025,6 +4026,7 @@ static int pin_rootfs(
context->root_directory,
pick_filter_image_dir,
ELEMENTSOF(pick_filter_image_dir),
+ /* match_root= */ NULL,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0) {
@@ -4055,6 +4057,7 @@ static int pin_rootfs(
context->root_mstack,
pick_filter_image_mstack,
/* n_filters= */ 1,
+ /* match_root= */ NULL,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0) {
diff --git a/src/core/namespace.c b/src/core/namespace.c
index 628e85594db..704b6d0a21a 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -570,6 +570,7 @@ static int append_extensions(
m->source,
pick_filter_image_raw,
ELEMENTSOF(pick_filter_image_raw),
+ /* match_root= */ NULL,
PICK_ARCHITECTURE|PICK_TRIES,
&result);
if (r == -ENOENT && m->ignore_enoent)
@@ -643,6 +644,7 @@ static int append_extensions(
e,
pick_filter_image_dir,
ELEMENTSOF(pick_filter_image_dir),
+ /* match_root= */ NULL,
PICK_ARCHITECTURE|PICK_TRIES,
&result);
if (r == -ENOENT && ignore_enoent)
diff --git a/src/portable/portable.c b/src/portable/portable.c
index 878cf219068..27f0566c179 100644
--- a/src/portable/portable.c
+++ b/src/portable/portable.c
@@ -836,6 +836,7 @@ static int extract_image_and_extensions(
name_or_path,
pick_filter_image_any,
ELEMENTSOF(pick_filter_image_any),
+ /* match_root= */ NULL,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0)
@@ -876,6 +877,7 @@ static int extract_image_and_extensions(
*p,
pick_filter_image_any,
ELEMENTSOF(pick_filter_image_any),
+ /* match_root= */ NULL,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&ext_result);
if (r < 0)
@@ -2224,6 +2226,7 @@ static int marker_matches_images(const char *marker, const char *name_or_path, c
*image_name_or_path,
pick_filter_image_any,
ELEMENTSOF(pick_filter_image_any),
+ /* match_root= */ NULL,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0 && r != -ENOENT)
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index 08a4dce4873..d9585af2a04 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -962,6 +962,7 @@ int image_find(RuntimeScope scope,
fname_path, /* This has to be the unresolved entry with the .v suffix */
&filter,
/* n_filters= */ 1,
+ /* match_root= */ root,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0) {
@@ -1184,6 +1185,7 @@ int image_discover(
fname_path, /* This has to be the unresolved entry with the .v suffix */
&filter,
/* n_filters= */ 1,
+ /* match_root= */ root,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0) {
diff --git a/src/shared/mstack.c b/src/shared/mstack.c
index c2c1d393526..179c158870a 100644
--- a/src/shared/mstack.c
+++ b/src/shared/mstack.c
@@ -139,7 +139,7 @@ static int mstack_load_one(MStack *mstack, const char *dir, int dir_fd, const ch
};
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
- r = path_pick(dir, dir_fd, dir_fd, fname, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE, &result);
+ r = path_pick(dir, dir_fd, dir_fd, fname, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE, &result);
if (r < 0)
return log_debug_errno(r, "Failed to resolve '%s' directory: %m", fname);
if (r == 0)
diff --git a/src/shared/vpick.c b/src/shared/vpick.c
index d842c166e8c..8855d807b3a 100644
--- a/src/shared/vpick.c
+++ b/src/shared/vpick.c
@@ -6,7 +6,9 @@
#include "architecture.h"
#include "bitfield.h"
#include "chase.h"
+#include "errno-util.h"
#include "fd-util.h"
+#include "host-properties.h"
#include "log.h"
#include "parse-util.h"
#include "path-util.h"
@@ -14,6 +16,7 @@
#include "stat-util.h"
#include "string-util.h"
#include "vpick.h"
+#include "xattr-match.h"
void pick_result_done(PickResult *p) {
assert(p);
@@ -184,6 +187,7 @@ static int pin_choice(
unsigned tries_left,
unsigned tries_done,
const PickFilter *filter,
+ const XAttrMatchFilter *xattr_filter, /* NULL to ignore "user.uapi.match_…" xattrs */
PickFlags flags,
PickResult *ret) {
@@ -224,6 +228,32 @@ static int pin_choice(
return 0;
}
+ if (xattr_filter) {
+ _cleanup_(xattr_match_lists_done) XAttrMatchLists lists = {};
+ r = xattr_match_lists_read(inode_fd, "", xattr_filter->mask, &lists);
+ if (ERRNO_IS_NEG_PRIVILEGE(r)) {
+ /* Unlike fstat() on the O_PATH fd, reading user xattrs requires read access to the
+ * inode. Don't let a single inaccessible entry fail the whole operation. */
+ log_debug_errno(r, "Not allowed to read 'user.uapi.match_*' extended attributes of '%s/%s', ignoring entry: %m",
+ empty_to_root(root_path), skip_leading_slash(inode_path));
+ *ret = PICK_RESULT_NULL;
+ return 0;
+ }
+ if (r < 0)
+ return log_debug_errno(r, "Failed to read 'user.uapi.match_*' extended attributes of '%s/%s': %m",
+ empty_to_root(root_path), skip_leading_slash(inode_path));
+
+ r = xattr_match_filter_test_lists(xattr_filter, lists, inode_path);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ log_debug("Inode '%s/%s' is not suitable for this system, ignoring.",
+ empty_to_root(root_path), skip_leading_slash(inode_path));
+ *ret = PICK_RESULT_NULL;
+ return 0;
+ }
+ }
+
_cleanup_(pick_result_done) PickResult result = {
.fd = TAKE_FD(inode_fd),
.st = st,
@@ -319,6 +349,7 @@ static int make_choice(
const char *inode_path,
int _inode_fd, /* we always take ownership of the fd, even on failure */
const PickFilter *filter,
+ const XAttrMatchFilter *xattr_filter,
PickFlags flags,
PickResult *ret) {
@@ -367,6 +398,7 @@ static int make_choice(
/* tries_left= */ UINT_MAX,
/* tries_done= */ UINT_MAX,
filter,
+ xattr_filter,
flags & ~PICK_RESOLVE,
ret);
@@ -476,6 +508,7 @@ static int make_choice(
.architecture = a,
.suffix = filter->suffix,
},
+ xattr_filter,
flags,
&found);
if (r == 0)
@@ -512,6 +545,7 @@ static int path_pick_one(
int dir_fd,
const char *path,
const PickFilter *filter,
+ const XAttrMatchFilter *xattr_filter,
PickFlags flags,
PickResult *ret) {
@@ -551,6 +585,7 @@ static int path_pick_one(
path,
/* inode_fd= */ -EBADF,
filter,
+ xattr_filter,
flags,
ret);
@@ -644,11 +679,13 @@ static int path_pick_one(
.architecture = filter->architecture,
.suffix = filter_suffix,
},
+ xattr_filter,
flags,
ret);
bypass:
- /* Don't make any choice, but just use the passed path literally */
+ /* Don't make any choice, but just use the passed path literally. Since the path was explicitly
+ * specified we also don't check the "user.uapi.match_…" xattrs here. */
return pin_choice(
root_path,
root_fd,
@@ -658,6 +695,7 @@ bypass:
/* tries_left= */ UINT_MAX,
/* tries_done= */ UINT_MAX,
filter,
+ /* xattr_filter= */ NULL,
flags,
ret);
}
@@ -668,6 +706,7 @@ int path_pick(const char *root_path,
const char *path,
const PickFilter filters[],
size_t n_filters,
+ const char *match_root,
PickFlags flags,
PickResult *ret) {
@@ -680,11 +719,31 @@ int path_pick(const char *root_path,
assert(filters || n_filters == 0);
assert(ret);
+ /* Entries in .v/ directories may be restricted to specific systems via "user.uapi.match_…" xattrs
+ * (as set by systemd-sysupdate, see MatchXAttr= in sysupdate.d(5)). The properties of the system
+ * are only loaded once an entry actually carries one of them, from match_root (or the host if NULL).
+ * Note that this is deliberately separate from root_path: the directory we resolve paths in is not
+ * necessarily the root of the system the entries are going to be used on. */
+ _cleanup_(host_properties_done) HostProperties host = {};
+ const XAttrMatchFilter xattr_filter = {
+ .mask = XATTR_MATCH_MASK_ALL,
+ .host = &host,
+ .root = empty_to_null(match_root),
+ };
+
/* Iterate through all filters and pick the best result */
for (size_t i = 0; i < n_filters; i++) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
- r = path_pick_one(root_path, root_fd, dir_fd, path, &filters[i], flags, &result);
+ r = path_pick_one(
+ root_path,
+ root_fd,
+ dir_fd,
+ path,
+ &filters[i],
+ FLAGS_SET(flags, PICK_IGNORE_XATTR_MATCH) ? NULL : &xattr_filter,
+ flags,
+ &result);
if (r < 0)
return r;
if (r == 0)
@@ -736,6 +795,7 @@ int path_pick_update_warn(
*path,
filters,
n_filters,
+ /* match_root= */ NULL,
flags,
&result);
if (r == -ENOENT) {
diff --git a/src/shared/vpick.h b/src/shared/vpick.h
index 3d9647fc32e..cc015a827aa 100644
--- a/src/shared/vpick.h
+++ b/src/shared/vpick.h
@@ -7,9 +7,10 @@
#include "forward.h"
typedef enum PickFlags {
- PICK_ARCHITECTURE = 1 << 0, /* Look for an architecture suffix */
- PICK_TRIES = 1 << 1, /* Look for tries left/tries done counters */
- PICK_RESOLVE = 1 << 2, /* Return the fully resolved (chased) path, rather than the path to the entry */
+ PICK_ARCHITECTURE = 1 << 0, /* Look for an architecture suffix */
+ PICK_TRIES = 1 << 1, /* Look for tries left/tries done counters */
+ PICK_RESOLVE = 1 << 2, /* Return the fully resolved (chased) path, rather than the path to the entry */
+ PICK_IGNORE_XATTR_MATCH = 1 << 3, /* Don't skip entries whose "user.uapi.match_…" xattrs don't match the system */
} PickFlags;
typedef struct PickFilter {
@@ -51,6 +52,7 @@ int path_pick(const char *root_path,
const char *path,
const PickFilter filters[],
size_t n_filters,
+ const char *match_root,
PickFlags flags,
PickResult *ret);
diff --git a/src/test/test-vpick.c b/src/test/test-vpick.c
index 8e9fce19ba2..fb118a6a775 100644
--- a/src/test/test-vpick.c
+++ b/src/test/test-vpick.c
@@ -2,6 +2,7 @@
#include
+#include "errno-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
@@ -10,6 +11,7 @@
#include "tests.h"
#include "tmpfile-util.h"
#include "vpick.h"
+#include "xattr-util.h"
TEST(path_pick) {
_cleanup_(rm_rf_physical_and_freep) char *p = NULL;
@@ -48,7 +50,7 @@ TEST(path_pick) {
};
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "99");
ASSERT_EQ(result.architecture, ARCHITECTURE_X86);
@@ -58,7 +60,7 @@ TEST(path_pick) {
}
filter.architecture = ARCHITECTURE_X86_64;
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "55");
ASSERT_EQ(result.architecture, ARCHITECTURE_X86_64);
@@ -66,7 +68,7 @@ TEST(path_pick) {
pick_result_done(&result);
filter.architecture = ARCHITECTURE_IA64;
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "5");
ASSERT_EQ(result.architecture, ARCHITECTURE_IA64);
@@ -75,7 +77,7 @@ TEST(path_pick) {
filter.architecture = _ARCHITECTURE_INVALID;
filter.version = "5";
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "5");
if (native_architecture() != ARCHITECTURE_IA64) {
@@ -85,7 +87,7 @@ TEST(path_pick) {
pick_result_done(&result);
filter.architecture = ARCHITECTURE_IA64;
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "5");
ASSERT_EQ(result.architecture, ARCHITECTURE_IA64);
@@ -93,7 +95,7 @@ TEST(path_pick) {
pick_result_done(&result);
filter.architecture = ARCHITECTURE_CRIS;
- ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_EQ(result.st.st_mode, MODE_INVALID);
ASSERT_NULL(result.version);
ASSERT_LT(result.architecture, 0);
@@ -104,7 +106,7 @@ TEST(path_pick) {
filter.architecture = _ARCHITECTURE_INVALID;
filter.version = NULL;
if (IN_SET(native_architecture(), ARCHITECTURE_X86_64, ARCHITECTURE_X86)) {
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "55");
@@ -123,7 +125,7 @@ TEST(path_pick) {
pp = ASSERT_NOT_NULL(path_join(p, "foo.v/foo___.raw"));
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "55");
ASSERT_EQ(result.architecture, native_architecture());
@@ -137,10 +139,10 @@ TEST(path_pick) {
pp = ASSERT_NOT_NULL(path_join(p, "foo.v/foo_5.raw"));
filter.type_mask = UINT32_C(1) << DT_DIR;
- ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
filter.type_mask = UINT32_C(1) << DT_REG;
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_NULL(result.version);
ASSERT_EQ(result.architecture, _ARCHITECTURE_INVALID);
@@ -153,7 +155,7 @@ TEST(path_pick) {
filter.architecture = ARCHITECTURE_S390;
filter.basename = "quux";
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "2");
ASSERT_EQ(result.tries_left, 4U);
@@ -209,7 +211,7 @@ TEST(pick_filter_image_any) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
/* Test pick_filter_image_any: should pick the highest version, which is the directory test_5 */
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "5");
ASSERT_TRUE(endswith(result.path, "/test_5"));
@@ -219,14 +221,14 @@ TEST(pick_filter_image_any) {
ASSERT_OK(unlinkat(sub_dfd, "test_4", AT_REMOVEDIR));
ASSERT_OK(unlinkat(sub_dfd, "test_5", AT_REMOVEDIR));
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "3");
ASSERT_TRUE(endswith(result.path, "/test_3.raw"));
pick_result_done(&result);
/* Verify that pick_filter_image_raw only matches .raw files */
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "3");
ASSERT_TRUE(endswith(result.path, "/test_3.raw"));
@@ -239,12 +241,12 @@ TEST(pick_filter_image_any) {
ASSERT_OK(unlinkat(sub_dfd, "test_3.raw", 0));
/* Now only test_10.txt, test_11.img, and test_12 remain - none should match */
- ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
/* But if we add a directory, it should be picked */
ASSERT_OK(mkdirat(sub_dfd, "test_6", 0755));
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "6");
ASSERT_TRUE(endswith(result.path, "/test_6"));
@@ -263,7 +265,7 @@ TEST(pick_filter_image_any) {
pick_result_done(&result);
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "2");
ASSERT_TRUE(endswith(result.path, "/myimage_2"));
@@ -273,7 +275,7 @@ TEST(pick_filter_image_any) {
ASSERT_OK(unlinkat(sub_dfd, "myimage_1", AT_REMOVEDIR));
ASSERT_OK(unlinkat(sub_dfd, "myimage_2", AT_REMOVEDIR));
- ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
}
TEST(path_pick_resolve) {
@@ -294,26 +296,26 @@ TEST(path_pick_resolve) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
/* Test without PICK_RESOLVE - should return the symlink path */
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
ASSERT_STREQ(result.version, "2");
ASSERT_TRUE(endswith(result.path, "/resolve_2.raw"));
pick_result_done(&result);
/* Test with PICK_RESOLVE - should return the resolved (target) path */
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_RESOLVE, &result));
ASSERT_STREQ(result.version, "2");
ASSERT_TRUE(endswith(result.path, "/target_file.raw"));
pick_result_done(&result);
/* Test pick_filter_image_dir without PICK_RESOLVE */
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "1");
ASSERT_TRUE(endswith(result.path, "/resolve_1"));
pick_result_done(&result);
/* Test pick_filter_image_dir with PICK_RESOLVE */
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_RESOLVE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "1");
ASSERT_TRUE(endswith(result.path, "/target_dir"));
@@ -323,12 +325,12 @@ TEST(path_pick_resolve) {
ASSERT_OK(symlinkat("target_file.raw", dfd, "intermediate_link.raw"));
ASSERT_OK(symlinkat("../intermediate_link.raw", sub_dfd, "resolve_3.raw"));
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), /* match_root= */ NULL, PICK_ARCHITECTURE, &result));
ASSERT_STREQ(result.version, "3");
ASSERT_TRUE(endswith(result.path, "/resolve_3.raw"));
pick_result_done(&result);
- ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_RESOLVE, &result));
ASSERT_STREQ(result.version, "3");
/* The chain should be fully resolved to target_file.raw */
ASSERT_TRUE(endswith(result.path, "/target_file.raw"));
@@ -404,4 +406,72 @@ TEST(pick_result_compare) {
a.version = b.version = NULL;
}
+TEST(path_pick_xattr_match) {
+ _cleanup_(rm_rf_physical_and_freep) char *p = NULL;
+ _cleanup_close_ int dfd = -EBADF, sub_dfd = -EBADF;
+ int r;
+
+ dfd = ASSERT_OK(mkdtemp_open(/* template= */ NULL, O_DIRECTORY, &p));
+ sub_dfd = ASSERT_OK(open_mkdir_at(dfd, "foo.v", /* flags= */ 0, 0777));
+
+ ASSERT_OK(write_string_file_at(sub_dfd, "foo_1.raw", "1", WRITE_STRING_FILE_CREATE));
+ ASSERT_OK(write_string_file_at(sub_dfd, "foo_2.raw", "2", WRITE_STRING_FILE_CREATE));
+ ASSERT_OK(write_string_file_at(sub_dfd, "foo_3.raw", "3", WRITE_STRING_FILE_CREATE));
+
+ /* The newest one is restricted to a machine ID no system has, the second newest to any hostname */
+ r = xsetxattr_strv(sub_dfd, "foo_3.raw", /* at_flags= */ 0, "user.uapi.match_machine_id", STRV_MAKE("00000000000000000000000000000000"));
+ if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
+ return (void) log_tests_skipped_errno(r, "User extended attributes not supported");
+ ASSERT_OK(r);
+ ASSERT_OK(xsetxattr_strv(sub_dfd, "foo_2.raw", /* at_flags= */ 0, "user.uapi.match_hostname", STRV_MAKE("*")));
+
+ _cleanup_free_ char *pp = ASSERT_NOT_NULL(path_join(p, "foo.v"));
+ _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
+ PickFilter filter = {
+ .architecture = _ARCHITECTURE_INVALID,
+ .suffix = ".raw",
+ };
+
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_STREQ(result.version, "2");
+ pick_result_done(&result);
+
+ /* Also when the version is fully specified */
+ filter.version = "3";
+ ASSERT_OK_ZERO(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, /* flags= */ 0, &result));
+ filter.version = NULL;
+
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES|PICK_IGNORE_XATTR_MATCH, &result));
+ ASSERT_STREQ(result.version, "3");
+ pick_result_done(&result);
+
+ /* An invalid extended attribute disqualifies an entry too */
+ ASSERT_OK(xsetxattr_strv(sub_dfd, "foo_2.raw", /* at_flags= */ 0, "user.uapi.match_hostname", STRV_MAKE("")));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_STREQ(result.version, "1");
+ pick_result_done(&result);
+
+ /* The host properties are loaded from match_root if specified, not from the host */
+ _cleanup_(rm_rf_physical_and_freep) char *mr = NULL;
+ _cleanup_close_ int mr_fd = ASSERT_OK(mkdtemp_open(/* template= */ NULL, O_DIRECTORY, &mr));
+ ASSERT_OK(write_string_file_at(mr_fd, "etc/hostname", "vpick-test-host", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755));
+ ASSERT_OK(write_string_file_at(sub_dfd, "foo_4.raw", "4", WRITE_STRING_FILE_CREATE));
+ ASSERT_OK(xsetxattr_strv(sub_dfd, "foo_4.raw", /* at_flags= */ 0, "user.uapi.match_hostname", STRV_MAKE("vpick-test-host")));
+
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, mr, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_STREQ(result.version, "4");
+ pick_result_done(&result);
+
+ /* An empty /etc/hostname in match_root means no hostname, which doesn't match, but isn't an error */
+ ASSERT_OK(write_string_file_at(mr_fd, "etc/hostname", "# nothing", WRITE_STRING_FILE_TRUNCATE));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, mr, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_STREQ(result.version, "1");
+ pick_result_done(&result);
+
+ /* A path that doesn't make use of .v/ is taken literally */
+ _cleanup_free_ char *literal = ASSERT_NOT_NULL(path_join(p, "foo.v/foo_3.raw"));
+ ASSERT_OK_POSITIVE(path_pick(/* root_path= */ NULL, AT_FDCWD, AT_FDCWD, literal, &filter, /* n_filters= */ 1, /* match_root= */ NULL, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_TRUE(endswith(result.path, "/foo_3.raw"));
+}
+
DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/vpick/vpick-tool.c b/src/vpick/vpick-tool.c
index 5e6e8687960..46f3d90a19f 100644
--- a/src/vpick/vpick-tool.c
+++ b/src/vpick/vpick-tool.c
@@ -10,6 +10,7 @@
#include "format-table.h"
#include "log.h"
#include "main-func.h"
+#include "parse-argument.h"
#include "parse-util.h"
#include "path-util.h"
#include "stat-util.h"
@@ -140,6 +141,14 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
break;
+ OPTION_LONG("match-xattr", "BOOL", "Skip entries whose user.uapi.match_* xattrs don't match the system (default: yes)"):
+ r = parse_boolean_argument("--match-xattr=", opts.arg, /* ret= */ NULL);
+ if (r < 0)
+ return r;
+
+ SET_FLAG(arg_flags, PICK_IGNORE_XATTR_MATCH, !r);
+ break;
+
OPTION_GROUP("Output"): {}
OPTION_COMMON_HELP:
@@ -223,6 +232,7 @@ static int run(int argc, char *argv[]) {
.type_mask = arg_filter_type_mask,
},
/* n_filters= */ 1,
+ /* match_root= */ NULL,
arg_flags,
&result);
if (r < 0)
diff --git a/test/units/TEST-74-AUX-UTILS.vpick.sh b/test/units/TEST-74-AUX-UTILS.vpick.sh
index 4c78b91002c..065f4df67a6 100755
--- a/test/units/TEST-74-AUX-UTILS.vpick.sh
+++ b/test/units/TEST-74-AUX-UTILS.vpick.sh
@@ -8,6 +8,7 @@ at_exit() {
rm -rf /var/lib/machines/mymachine.raw.v
rm -rf /var/lib/machines/mytree.v
rm -rf /var/lib/machines/testroot.v
+ rm -rf /var/lib/machines/xattr.raw.v
umount -l /tmp/dotvroot
rmdir /tmp/dotvroot
}
@@ -114,3 +115,16 @@ test "$(systemd-vpick --resolve=yes /var/lib/machines/testroot.v)" = /var/lib/ma
rm -rf /var/lib/machines/testroot.v/testroot_32
(! systemd-vpick /var/lib/machines/testroot.v)
(! systemd-run --wait -p RootDirectory=/var/lib/machines/testroot.v true)
+
+# Entries may be restricted to specific systems via "user.uapi.match_…" extended attributes
+mkdir /var/lib/machines/xattr.raw.v
+touch /var/lib/machines/xattr.raw.v/xattr_1.raw /var/lib/machines/xattr.raw.v/xattr_2.raw
+setfattr -n user.uapi.match_machine_id -v 00000000000000000000000000000000 /var/lib/machines/xattr.raw.v/xattr_2.raw
+test "$(systemd-vpick --suffix=.raw /var/lib/machines/xattr.raw.v)" = /var/lib/machines/xattr.raw.v/xattr_1.raw
+test "$(systemd-vpick --suffix=.raw --match-xattr=no /var/lib/machines/xattr.raw.v)" = /var/lib/machines/xattr.raw.v/xattr_2.raw
+setfattr -n user.uapi.match_machine_id -v "$(