From cb599f881a804eb3c9e3991e1e293d08bc7d9430 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Nov 2023 16:55:22 +0100 Subject: [PATCH 01/12] strv: add new strv_endswith() helper --- src/basic/strv.c | 10 ++++++++++ src/basic/strv.h | 2 ++ src/test/test-strv.c | 8 ++++++++ 3 files changed, 20 insertions(+) diff --git a/src/basic/strv.c b/src/basic/strv.c index c2109d35bc..43a4f569bd 100644 --- a/src/basic/strv.c +++ b/src/basic/strv.c @@ -905,3 +905,13 @@ int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const } DEFINE_HASH_OPS_FULL(string_strv_hash_ops, char, string_hash_func, string_compare_func, free, char*, strv_free); + +char* strv_endswith(const char *s, char **l) { + STRV_FOREACH(i, l) { + char *e = endswith(s, *i); + if (e) + return (char*) e; + } + + return NULL; +} diff --git a/src/basic/strv.h b/src/basic/strv.h index fec2616733..18df0f23f2 100644 --- a/src/basic/strv.h +++ b/src/basic/strv.h @@ -252,3 +252,5 @@ int _string_strv_hashmap_put(Hashmap **h, const char *key, const char *value HA int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const char *value HASHMAP_DEBUG_PARAMS); #define string_strv_hashmap_put(h, k, v) _string_strv_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS) #define string_strv_ordered_hashmap_put(h, k, v) _string_strv_ordered_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS) + +char* strv_endswith(const char *s, char **l); diff --git a/src/test/test-strv.c b/src/test/test-strv.c index cfd662b329..f4a45703d0 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -1006,4 +1006,12 @@ TEST(strv_find_first_field) { assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("i", "k", "l", "m", "d", "c", "a", "b"), haystack), "j")); } +TEST(strv_endswith) { + assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("xxx", "yyy", "ldo", "zzz")), "ldo")); + assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("xxx", "yyy", "zzz")), NULL)); + assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("waldo")), "waldo")); + assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("w", "o", "ldo")), "o")); + assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("knurz", "", "waldo")), "")); +} + DEFINE_TEST_MAIN(LOG_INFO); From 63566c6b6ffbb747727db4d6f78c28547430d54f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Mar 2023 14:01:02 +0100 Subject: [PATCH 02/12] string-util: add strrstr() helper --- src/basic/string-util.c | 23 +++++++++++++++++++++++ src/basic/string-util.h | 2 ++ src/test/test-string-util.c | 23 +++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 7329bfacdf..f6453990bd 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1519,3 +1519,26 @@ ssize_t strlevenshtein(const char *x, const char *y) { return t1[yl]; } + +char *strrstr(const char *haystack, const char *needle) { + const char *f = NULL; + size_t l; + + /* Like strstr() but returns the last rather than the first occurence of "needle" in "haystack". */ + + if (!haystack || !needle) + return NULL; + + l = strlen(needle); + + /* Special case: for the empty string we return the very last possible occurence, i.e. *after* the + * last char, not before. */ + if (l == 0) + return strchr(haystack, 0); + + for (const char *p = haystack; *p; p++) + if (strncmp(p, needle, l) == 0) + f = p; + + return (char*) f; +} diff --git a/src/basic/string-util.h b/src/basic/string-util.h index b6d8be3083..bf427cd7f7 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -322,3 +322,5 @@ static inline int strdup_or_null(const char *s, char **ret) { *ret = c; return 1; } + +char *strrstr(const char *haystack, const char *needle); diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c index a8fd45df73..e78e299ed2 100644 --- a/src/test/test-string-util.c +++ b/src/test/test-string-util.c @@ -1324,4 +1324,27 @@ TEST(strlevenshtein) { assert_se(strlevenshtein("sunday", "saturday") == 3); } +TEST(strrstr) { + assert_se(!strrstr(NULL, NULL)); + assert_se(!strrstr("foo", NULL)); + assert_se(!strrstr(NULL, "foo")); + + const char *p = "foo"; + assert_se(strrstr(p, "foo") == p); + assert_se(strrstr(p, "fo") == p); + assert_se(strrstr(p, "f") == p); + assert_se(strrstr(p, "oo") == p + 1); + assert_se(strrstr(p, "o") == p + 2); + assert_se(strrstr(p, "") == p + strlen(p)); + assert_se(!strrstr(p, "bar")); + + p = "xoxoxox"; + assert_se(strrstr(p, "") == p + strlen(p)); + assert_se(strrstr(p, "x") == p + 6); + assert_se(strrstr(p, "ox") == p + 5); + assert_se(strrstr(p, "xo") == p + 4); + assert_se(strrstr(p, "xox") == p + 4); + assert_se(!strrstr(p, "xx")); +} + DEFINE_TEST_MAIN(LOG_DEBUG); From cc03788086e4c02d8e1f44cc84e0536594cca230 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 20 Jun 2023 16:18:37 +0200 Subject: [PATCH 03/12] stat-util: add inode_type_from_string() helper --- src/basic/stat-util.c | 22 ++++++++++++++++++++++ src/basic/stat-util.h | 1 + src/test/test-stat-util.c | 15 +++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index 9bcd63d3e9..51715668fe 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -516,3 +516,25 @@ const char* inode_type_to_string(mode_t m) { return NULL; } + +mode_t inode_type_from_string(const char *s) { + if (!s) + return MODE_INVALID; + + if (streq(s, "reg")) + return S_IFREG; + if (streq(s, "dir")) + return S_IFDIR; + if (streq(s, "lnk")) + return S_IFLNK; + if (streq(s, "chr")) + return S_IFCHR; + if (streq(s, "blk")) + return S_IFBLK; + if (streq(s, "fifo")) + return S_IFIFO; + if (streq(s, "sock")) + return S_IFSOCK; + + return MODE_INVALID; +} diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h index ae0aaf8f51..cb736c36dd 100644 --- a/src/basic/stat-util.h +++ b/src/basic/stat-util.h @@ -114,3 +114,4 @@ int inode_compare_func(const struct stat *a, const struct stat *b); extern const struct hash_ops inode_hash_ops; const char* inode_type_to_string(mode_t m); +mode_t inode_type_from_string(const char *s); diff --git a/src/test/test-stat-util.c b/src/test/test-stat-util.c index 5aca207fa4..95137ffcf1 100644 --- a/src/test/test-stat-util.c +++ b/src/test/test-stat-util.c @@ -180,6 +180,21 @@ TEST(dir_is_empty) { assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ false) > 0); } +TEST(inode_type_from_string) { + static const mode_t types[] = { + S_IFREG, + S_IFDIR, + S_IFLNK, + S_IFCHR, + S_IFBLK, + S_IFIFO, + S_IFSOCK, + }; + + FOREACH_ARRAY(m, types, ELEMENTSOF(types)) + assert_se(inode_type_from_string(inode_type_to_string(*m)) == *m); +} + static int intro(void) { log_show_color(true); return EXIT_SUCCESS; From 76511c1bd32a262c76d462919083925c47cbd212 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Mar 2023 18:18:05 +0100 Subject: [PATCH 04/12] shared: add new "vpick" concept for ".v/" directories that contain versioned resources This adds a new concept for handling paths. At appropriate places, if a path such as /foo/bar/baz.v/ is specified, we'll automatically enumerate all entries in /foo/bar/baz.v/baz* and then do a version sort and pick the newest file. A slightly more complex syntax is available, too: /foo/bar/baz.v/quux___waldo if that's used, then we'll look for all files matching /foo/bar/baz.v/quux*waldo, and split out the middle, and version sort it, and pick the nwest. The ___ wildcard indicates both a version string, and if needed an architecture ID, in case per-arch entries shall be supported. This is a very simple way to maintain versioned resources in a dir, and make systemd's components automatically pick the newest. Example: /srv/myimages.v/foobar_1.32.65_x86-64.raw /srv/myimages.v/foobar_1.33.45_x86-64.raw /srv/myimages.v/foobar_1.31.5_x86-64.raw /srv/myimages.v/foobar_1.31.5_arm64.raw If now nspawn is invoked like this: systemd-nspawn --image=/srv/myimages.v/foobar___.raw Then it will automatically pick /srv/myimages.v/foobar_1.33.45_x86-64.raw as the version to boot on x86-64, and /srv/myimages.v/foobar_1.31.5_arm64.raw on arm64. This commit only adds the basic implementation for picking files from a dir, but no hook-up anywhere. --- src/shared/meson.build | 1 + src/shared/vpick.c | 694 +++++++++++++++++++++++++++++++++++++++++ src/shared/vpick.h | 59 ++++ src/test/meson.build | 1 + src/test/test-vpick.c | 171 ++++++++++ 5 files changed, 926 insertions(+) create mode 100644 src/shared/vpick.c create mode 100644 src/shared/vpick.h create mode 100644 src/test/test-vpick.c diff --git a/src/shared/meson.build b/src/shared/meson.build index b2cee6fa2c..1b95430f88 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -189,6 +189,7 @@ shared_sources = files( 'verbs.c', 'vlan-util.c', 'volatile-util.c', + 'vpick.c', 'wall.c', 'watchdog.c', 'web-util.c', diff --git a/src/shared/vpick.c b/src/shared/vpick.c new file mode 100644 index 0000000000..4a4d1eb408 --- /dev/null +++ b/src/shared/vpick.c @@ -0,0 +1,694 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "architecture.h" +#include "chase.h" +#include "fd-util.h" +#include "fs-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "recurse-dir.h" +#include "vpick.h" + +void pick_result_done(PickResult *p) { + assert(p); + + free(p->path); + safe_close(p->fd); + free(p->version); + + *p = PICK_RESULT_NULL; +} + +static int format_fname( + const PickFilter *filter, + PickFlags flags, + char **ret) { + + _cleanup_free_ char *fn = NULL; + int r; + + assert(filter); + assert(ret); + + if (FLAGS_SET(flags, PICK_TRIES) || !filter->version) /* Underspecified? */ + return -ENOEXEC; + + /* The format for names we match goes like this: + * + * + * or: + * _ + * or: + * __ + * or: + * _ + * + * (Note that basename can be empty, in which case the leading "_" is suppressed) + * + * Examples: foo.raw, foo_1.3-7.raw, foo_1.3-7_x86-64.raw, foo_x86-64.raw + * + * Why use "_" as separator here? Primarily because it is not used by Semver 2.0. In RPM it is used + * for "unsortable" versions, i.e. doesn't show up in "sortable" versions, which we matter for this + * usecase here. In Debian the underscore is not allowed (and it uses it itself for separating + * fields). + * + * This is very close to Debian's way to name packages, but allows arbitrary suffixes, and makes the + * architecture field redundant. + * + * Compare with RPM's "NEVRA" concept. Here we have "BVAS" (basename, version, architecture, suffix). + */ + + if (filter->basename) { + fn = strdup(filter->basename); + if (!fn) + return -ENOMEM; + } + + if (filter->version) { + if (isempty(fn)) { + r = free_and_strdup(&fn, filter->version); + if (r < 0) + return r; + } else if (!strextend(&fn, "_", filter->version)) + return -ENOMEM; + } + + if (FLAGS_SET(flags, PICK_ARCHITECTURE) && filter->architecture >= 0) { + const char *as = ASSERT_PTR(architecture_to_string(filter->architecture)); + if (isempty(fn)) { + r = free_and_strdup(&fn, as); + if (r < 0) + return r; + } else if (!strextend(&fn, "_", as)) + return -ENOMEM; + } + + if (filter->suffix && !strextend(&fn, filter->suffix)) + return -ENOMEM; + + if (!filename_is_valid(fn)) + return -EINVAL; + + *ret = TAKE_PTR(fn); + return 0; +} + +static int errno_from_mode(uint32_t type_mask, mode_t found) { + /* Returns the most appropriate error code if we are lookging for an inode of type of those in the + * 'type_mask' but found 'found' instead. + * + * type_mask is a mask of 1U << DT_REG, 1U << DT_DIR, … flags, while found is a S_IFREG, S_IFDIR, … + * mode value. */ + + if (type_mask == 0) /* type doesn't matter */ + return 0; + + if (FLAGS_SET(type_mask, UINT32_C(1) << IFTODT(found))) + return 0; + + if (type_mask == (UINT32_C(1) << DT_BLK)) + return -ENOTBLK; + if (type_mask == (UINT32_C(1) << DT_DIR)) + return -ENOTDIR; + if (type_mask == (UINT32_C(1) << DT_SOCK)) + return -ENOTSOCK; + + if (S_ISLNK(found)) + return -ELOOP; + if (S_ISDIR(found)) + return -EISDIR; + + return -EBADF; +} + +static int pin_choice( + const char *toplevel_path, + int toplevel_fd, + const char *inode_path, + int _inode_fd, /* we always take ownership of the fd, even on failure */ + unsigned tries_left, + unsigned tries_done, + const PickFilter *filter, + PickFlags flags, + PickResult *ret) { + + _cleanup_close_ int inode_fd = TAKE_FD(_inode_fd); + _cleanup_free_ char *resolved_path = NULL; + int r; + + assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD); + assert(inode_path); + assert(filter); + + toplevel_path = strempty(toplevel_path); + + if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) { + r = chaseat(toplevel_fd, + inode_path, + CHASE_AT_RESOLVE_IN_ROOT, + FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : 0, + inode_fd < 0 ? &inode_fd : NULL); + if (r < 0) + return r; + + if (resolved_path) + inode_path = resolved_path; + } + + struct stat st; + if (fstat(inode_fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat discovered inode '%s/%s': %m", toplevel_path, inode_path); + + if (filter->type_mask != 0 && + !FLAGS_SET(filter->type_mask, UINT32_C(1) << IFTODT(st.st_mode))) + return log_debug_errno( + SYNTHETIC_ERRNO(errno_from_mode(filter->type_mask, st.st_mode)), + "Inode '%s/%s' has wrong type, found '%s'.", + toplevel_path, inode_path, + inode_type_to_string(st.st_mode)); + + _cleanup_(pick_result_done) PickResult result = { + .fd = TAKE_FD(inode_fd), + .st = st, + .architecture = filter->architecture, + .tries_left = tries_left, + .tries_done = tries_done, + }; + + result.path = strdup(inode_path); + if (!result.path) + return log_oom_debug(); + + if (filter->version) { + result.version = strdup(filter->version); + if (!result.version) + return log_oom_debug(); + } + + *ret = TAKE_PICK_RESULT(result); + return 1; +} + +static int parse_tries(const char *s, unsigned *ret_tries_left, unsigned *ret_tries_done) { + unsigned left, done; + size_t n; + + assert(s); + assert(ret_tries_left); + assert(ret_tries_done); + + if (s[0] != '+') + goto nomatch; + + s++; + + n = strspn(s, DIGITS); + if (n == 0) + goto nomatch; + + if (s[n] == 0) { + if (safe_atou(s, &left) < 0) + goto nomatch; + + done = 0; + } else if (s[n] == '-') { + _cleanup_free_ char *c = NULL; + + c = strndup(s, n); + if (!c) + return -ENOMEM; + + if (safe_atou(c, &left) < 0) + goto nomatch; + + s += n + 1; + + if (!in_charset(s, DIGITS)) + goto nomatch; + + if (safe_atou(s, &done) < 0) + goto nomatch; + } else + goto nomatch; + + *ret_tries_left = left; + *ret_tries_done = done; + return 1; + +nomatch: + *ret_tries_left = *ret_tries_done = UINT_MAX; + return 0; +} + +static int make_choice( + const char *toplevel_path, + int toplevel_fd, + const char *inode_path, + int _inode_fd, /* we always take ownership of the fd, even on failure */ + const PickFilter *filter, + PickFlags flags, + PickResult *ret) { + + static const Architecture local_architectures[] = { + /* In order of preference */ + native_architecture(), +#ifdef ARCHITECTURE_SECONDARY + ARCHITECTURE_SECONDARY, +#endif + _ARCHITECTURE_INVALID, /* accept any arch, as last resort */ + }; + + _cleanup_free_ DirectoryEntries *de = NULL; + _cleanup_free_ char *best_version = NULL, *best_filename = NULL, *p = NULL, *j = NULL; + _cleanup_close_ int dir_fd = -EBADF, object_fd = -EBADF, inode_fd = TAKE_FD(_inode_fd); + const Architecture *architectures; + unsigned best_tries_left = UINT_MAX, best_tries_done = UINT_MAX; + size_t n_architectures, best_architecture_index = SIZE_MAX; + int r; + + assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD); + assert(inode_path); + assert(filter); + + toplevel_path = strempty(toplevel_path); + + if (inode_fd < 0) { + r = chaseat(toplevel_fd, inode_path, CHASE_AT_RESOLVE_IN_ROOT, NULL, &inode_fd); + if (r < 0) + return r; + } + + /* Maybe the filter is fully specified? Then we can generate the file name directly */ + r = format_fname(filter, flags, &j); + if (r >= 0) { + _cleanup_free_ char *object_path = NULL; + + /* Yay! This worked! */ + p = path_join(inode_path, j); + if (!p) + return log_oom_debug(); + + r = chaseat(toplevel_fd, p, CHASE_AT_RESOLVE_IN_ROOT, &object_path, &object_fd); + if (r < 0) { + if (r != -ENOENT) + return log_debug_errno(r, "Failed to open '%s/%s': %m", toplevel_path, p); + + *ret = PICK_RESULT_NULL; + return 0; + } + + return pin_choice( + toplevel_path, + toplevel_fd, + FLAGS_SET(flags, PICK_RESOLVE) ? object_path : p, + TAKE_FD(object_fd), /* unconditionally pass ownership of the fd */ + /* tries_left= */ UINT_MAX, + /* tries_done= */ UINT_MAX, + filter, + flags & ~PICK_RESOLVE, + ret); + + } else if (r != -ENOEXEC) + return log_debug_errno(r, "Failed to format file name: %m"); + + /* Underspecified, so we do our enumeration dance */ + + /* Convert O_PATH to a regular directory fd */ + dir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (dir_fd < 0) + return log_debug_errno(dir_fd, "Failed to reopen '%s/%s' as directory: %m", toplevel_path, inode_path); + + r = readdir_all(dir_fd, 0, &de); + if (r < 0) + return log_debug_errno(r, "Failed to read directory '%s/%s': %m", toplevel_path, inode_path); + + if (filter->architecture < 0) { + architectures = local_architectures; + n_architectures = ELEMENTSOF(local_architectures); + } else { + architectures = &filter->architecture; + n_architectures = 1; + } + + FOREACH_ARRAY(entry, de->entries, de->n_entries) { + unsigned found_tries_done = UINT_MAX, found_tries_left = UINT_MAX; + _cleanup_free_ char *chopped = NULL; + size_t found_architecture_index = SIZE_MAX; + const char *e; + + if (!isempty(filter->basename)) { + e = startswith((*entry)->d_name, filter->basename); + if (!e) + continue; + + if (e[0] != '_') + continue; + + e++; + } else + e = (*entry)->d_name; + + if (!isempty(filter->suffix)) { + const char *sfx; + + sfx = endswith(e, filter->suffix); + if (!sfx) + continue; + + chopped = strndup(e, sfx - e); + if (!chopped) + return log_oom_debug(); + + e = chopped; + } + + if (FLAGS_SET(flags, PICK_TRIES)) { + char *plus = strrchr(e, '+'); + if (plus) { + r = parse_tries(plus, &found_tries_left, &found_tries_done); + if (r < 0) + return r; + if (r > 0) /* Found and parsed, now chop off */ + *plus = 0; + } + } + + if (FLAGS_SET(flags, PICK_ARCHITECTURE)) { + char *underscore = strrchr(e, '_'); + Architecture a; + + a = underscore ? architecture_from_string(underscore + 1) : _ARCHITECTURE_INVALID; + + for (size_t i = 0; i < n_architectures; i++) + if (architectures[i] == a) { + found_architecture_index = i; + break; + } + + if (found_architecture_index == SIZE_MAX) { /* No matching arch found */ + log_debug("Found entry with architecture '%s' which is not what we are looking for, ignoring entry.", a < 0 ? "any" : architecture_to_string(a)); + continue; + } + + /* Chop off architecture from string */ + if (underscore) + *underscore = 0; + } + + if (!version_is_valid(e)) { + log_debug("Version string '%s' of entry '%s' is invalid, ignoring entry.", e, (*entry)->d_name); + continue; + } + + if (filter->version && !streq(filter->version, e)) { + log_debug("Found entry with version string '%s', but was looking for '%s', ignoring entry.", e, filter->version); + continue; + } + + if (best_filename) { /* Already found one matching entry? Then figure out the better one */ + int d = 0; + + /* First, prefer entries with tries left over those without */ + if (FLAGS_SET(flags, PICK_TRIES)) + d = CMP(found_tries_left != 0, best_tries_left != 0); + + /* Second, prefer newer versions */ + if (d == 0) + d = strverscmp_improved(e, best_version); + + /* Third, prefer native architectures over secondary architectures */ + if (d == 0 && + FLAGS_SET(flags, PICK_ARCHITECTURE) && + found_architecture_index != SIZE_MAX && best_architecture_index != SIZE_MAX) + d = -CMP(found_architecture_index, best_architecture_index); + + /* Fourth, prefer entries with more tries left */ + if (FLAGS_SET(flags, PICK_TRIES)) { + if (d == 0) + d = CMP(found_tries_left, best_tries_left); + + /* Fifth, prefer entries with fewer attempts done so far */ + if (d == 0) + d = -CMP(found_tries_done, best_tries_done); + } + + /* Last, just compare the filenames as strings */ + if (d == 0) + d = strcmp((*entry)->d_name, best_filename); + + if (d < 0) { + log_debug("Found entry '%s' but previously found entry '%s' matches better, hence skipping entry.", (*entry)->d_name, best_filename); + continue; + } + } + + r = free_and_strdup_warn(&best_version, e); + if (r < 0) + return r; + + r = free_and_strdup_warn(&best_filename, (*entry)->d_name); + if (r < 0) + return r; + + best_architecture_index = found_architecture_index; + best_tries_left = found_tries_left; + best_tries_done = found_tries_done; + } + + if (!best_filename) { /* Everything was good, but we didn't find any suitable entry */ + *ret = PICK_RESULT_NULL; + return 0; + } + + p = path_join(inode_path, best_filename); + if (!p) + return log_oom_debug(); + + object_fd = openat(dir_fd, best_filename, O_CLOEXEC|O_PATH); + if (object_fd < 0) + return log_debug_errno(errno, "Failed to open '%s/%s': %m", toplevel_path, p); + + return pin_choice( + toplevel_path, + toplevel_fd, + p, + TAKE_FD(object_fd), + best_tries_left, + best_tries_done, + &(const PickFilter) { + .type_mask = filter->type_mask, + .basename = filter->basename, + .version = empty_to_null(best_version), + .architecture = best_architecture_index != SIZE_MAX ? architectures[best_architecture_index] : _ARCHITECTURE_INVALID, + .suffix = filter->suffix, + }, + flags, + ret); +} + +int path_pick(const char *toplevel_path, + int toplevel_fd, + const char *path, + const PickFilter *filter, + PickFlags flags, + PickResult *ret) { + + _cleanup_free_ char *filter_bname = NULL, *dir = NULL, *parent = NULL, *fname = NULL; + const char *filter_suffix, *enumeration_path; + uint32_t filter_type_mask; + int r; + + assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD); + assert(path); + + toplevel_path = strempty(toplevel_path); + + /* Given a path, resolve .v/ subdir logic (if used!), and returns the choice made. This supports + * three ways to be called: + * + * • with a path referring a directory of any name, and filter→basename *explicitly* specified, in + * which case we'll use a pattern "_*" on the directory's files. + * + * • with no filter→basename explicitly specified and a path referring to a directory named in format + * ".v" . In this case the filter basename to search for inside the dir + * is derived from the directory name. Example: "/foo/bar/baz.suffix.v" → we'll search for + * "/foo/bar/baz.suffix.v/baz_*.suffix". + * + * • with a path whose penultimate component ends in ".v/". In this case the final component of the + * path refers to the pattern. Example: "/foo/bar/baz.v/waldo__.suffix" → we'll search for + * "/foo/bar/baz.v/waldo_*.suffix". + */ + + /* Explicit basename specified, then shortcut things and do .v mode regardless of the path name. */ + if (filter->basename) + return make_choice( + toplevel_path, + toplevel_fd, + path, + /* inode_fd= */ -EBADF, + filter, + flags, + ret); + + r = path_extract_filename(path, &fname); + if (r < 0) { + if (r != -EADDRNOTAVAIL) /* root dir or "." */ + return r; + + /* If there's no path element we can derive a pattern off, the don't */ + goto bypass; + } + + /* Remember if the path ends in a dash suffix */ + bool slash_suffix = r == O_DIRECTORY; + + const char *e = endswith(fname, ".v"); + if (e) { + /* So a path in the form /foo/bar/baz.v is specified. In this case our search basename is + * "baz", possibly with a suffix chopped off if there's one specified. */ + filter_bname = strndup(fname, e - fname); + if (!filter_bname) + return -ENOMEM; + + if (filter->suffix) { + /* Chop off suffix, if specified */ + char *f = endswith(filter_bname, filter->suffix); + if (f) + *f = 0; + } + + filter_suffix = filter->suffix; + filter_type_mask = filter->type_mask; + + enumeration_path = path; + } else { + /* The path does not end in '.v', hence see if the last element is a pattern. */ + + char *wildcard = strrstr(fname, "___"); + if (!wildcard) + goto bypass; /* Not a pattern, then bypass */ + + /* We found the '___' wildcard, hence evertyhing after it is our filter suffix, and + * evertyhing before is our filter basename */ + *wildcard = 0; + filter_suffix = empty_to_null(wildcard + 3); + + filter_bname = TAKE_PTR(fname); + + r = path_extract_directory(path, &dir); + if (r < 0) { + if (!IN_SET(r, -EDESTADDRREQ, -EADDRNOTAVAIL)) /* only filename specified (no dir), or root or "." */ + return r; + + goto bypass; /* No dir extractable, can't check if parent ends in ".v" */ + } + + r = path_extract_filename(dir, &parent); + if (r < 0) { + if (r != -EADDRNOTAVAIL) /* root dir or "." */ + return r; + + goto bypass; /* Cannot extract fname from parent dir, can't check if it ends in ".v" */ + } + + e = endswith(parent, ".v"); + if (!e) + goto bypass; /* Doesn't end in ".v", shortcut */ + + filter_type_mask = filter->type_mask; + if (slash_suffix) { + /* If the pattern is suffixed by a / then we are looking for directories apparently. */ + if (filter_type_mask != 0 && !FLAGS_SET(filter_type_mask, UINT32_C(1) << DT_DIR)) + return log_debug_errno(SYNTHETIC_ERRNO(errno_from_mode(filter_type_mask, S_IFDIR)), + "Specified pattern ends in '/', but not looking for directories, refusing."); + filter_type_mask = UINT32_C(1) << DT_DIR; + } + + enumeration_path = dir; + } + + return make_choice( + toplevel_path, + toplevel_fd, + enumeration_path, + /* inode_fd= */ -EBADF, + &(const PickFilter) { + .type_mask = filter_type_mask, + .basename = filter_bname, + .version = filter->version, + .architecture = filter->architecture, + .suffix = filter_suffix, + }, + flags, + ret); + +bypass: + /* Don't make any choice, but just use the passed path literally */ + return pin_choice( + toplevel_path, + toplevel_fd, + path, + /* inode_fd= */ -EBADF, + /* tries_left= */ UINT_MAX, + /* tries_done= */ UINT_MAX, + filter, + flags, + ret); +} + +int path_pick_update_warn( + char **path, + const PickFilter *filter, + PickFlags flags, + PickResult *ret_result) { + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + int r; + + assert(path); + assert(*path); + + /* This updates the first argument if needed! */ + + r = path_pick(/* toplevel_path= */ NULL, + /* toplevel_fd= */ AT_FDCWD, + *path, + filter, + flags, + &result); + if (r == -ENOENT) { + log_debug("Path '%s' doesn't exist, leaving as is.", *path); + *ret_result = PICK_RESULT_NULL; + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to pick version on path '%s': %m", *path); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entries in versioned directory '%s' found.", *path); + + log_debug("Resolved versioned directory pattern '%s' to file '%s' as version '%s'.", result.path, *path, strna(result.version)); + + if (ret_result) { + r = free_and_strdup_warn(path, result.path); + if (r < 0) + return r; + + *ret_result = TAKE_PICK_RESULT(result); + } else + free_and_replace(*path, result.path); + + return 1; +} + +const PickFilter pick_filter_image_raw = { + .type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK), + .architecture = _ARCHITECTURE_INVALID, + .suffix = ".raw", +}; + +const PickFilter pick_filter_image_dir = { + .type_mask = UINT32_C(1) << DT_DIR, + .architecture = _ARCHITECTURE_INVALID, +}; diff --git a/src/shared/vpick.h b/src/shared/vpick.h new file mode 100644 index 0000000000..0e0d41e5db --- /dev/null +++ b/src/shared/vpick.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "architecture.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 */ +} PickFlags; + +typedef struct PickFilter { + uint32_t type_mask; /* A mask of 1U << DT_REG, 1U << DT_DIR, … */ + const char *basename; /* Can be overriden by search pattern */ + const char *version; + Architecture architecture; + const char *suffix; /* Can be overriden by search pattern */ +} PickFilter; + +typedef struct PickResult { + char *path; + int fd; /* O_PATH */ + struct stat st; + char *version; + Architecture architecture; + unsigned tries_left; + unsigned tries_done; +} PickResult; + +#define PICK_RESULT_NULL \ + (const PickResult) { \ + .fd = -EBADF, \ + .st.st_mode = MODE_INVALID, \ + .architecture = _ARCHITECTURE_INVALID, \ + .tries_left = UINT_MAX, \ + .tries_done = UINT_MAX, \ + } + +#define TAKE_PICK_RESULT(ptr) TAKE_GENERIC(ptr, PickResult, PICK_RESULT_NULL) + +void pick_result_done(PickResult *p); + +int path_pick(const char *toplevel_path, + int toplevel_fd, + const char *path, + const PickFilter *filter, + PickFlags flags, + PickResult *ret); + +int path_pick_update_warn( + char **path, + const PickFilter *filter, + PickFlags flags, + PickResult *ret); + +extern const PickFilter pick_filter_image_raw; +extern const PickFilter pick_filter_image_dir; diff --git a/src/test/meson.build b/src/test/meson.build index aec125d483..3439f585ee 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -178,6 +178,7 @@ simple_tests += files( 'test-user-util.c', 'test-utf8.c', 'test-verbs.c', + 'test-vpick.c', 'test-web-util.c', 'test-xattr-util.c', 'test-xml.c', diff --git a/src/test/test-vpick.c b/src/test/test-vpick.c new file mode 100644 index 0000000000..1a288e8ba1 --- /dev/null +++ b/src/test/test-vpick.c @@ -0,0 +1,171 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "mkdir.h" +#include "path-util.h" +#include "rm-rf.h" +#include "tests.h" +#include "tmpfile-util.h" +#include "vpick.h" + +TEST(path_pick) { + _cleanup_(rm_rf_physical_and_freep) char *p = NULL; + _cleanup_close_ int dfd = -EBADF, sub_dfd = -EBADF; + + dfd = mkdtemp_open(NULL, O_DIRECTORY|O_CLOEXEC, &p); + assert(dfd >= 0); + + sub_dfd = open_mkdir_at(dfd, "foo.v", O_CLOEXEC, 0777); + assert(sub_dfd >= 0); + + assert_se(write_string_file_at(sub_dfd, "foo_5.5.raw", "5.5", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "foo_55.raw", "55", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "foo_5.raw", "5", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "foo_5_ia64.raw", "5", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "foo_7.raw", "7", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "foo_7_x86-64.raw", "7 64bit", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "foo_55_x86-64.raw", "55 64bit", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "foo_55_x86.raw", "55 32bit", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "foo_99_x86.raw", "99 32bit", WRITE_STRING_FILE_CREATE) >= 0); + + /* Let's add an entry for sparc (which is a valid arch, but almost certainly not what we test + * on). This entry should hence always be ignored */ + if (native_architecture() != ARCHITECTURE_SPARC) + assert_se(write_string_file_at(sub_dfd, "foo_100_sparc.raw", "100 sparc", WRITE_STRING_FILE_CREATE) >= 0); + + assert_se(write_string_file_at(sub_dfd, "quux_1_s390.raw", "waldo1", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "quux_2_s390+4-6.raw", "waldo2", WRITE_STRING_FILE_CREATE) >= 0); + assert_se(write_string_file_at(sub_dfd, "quux_3_s390+0-10.raw", "waldo3", WRITE_STRING_FILE_CREATE) >= 0); + + _cleanup_free_ char *pp = NULL; + pp = path_join(p, "foo.v"); + assert_se(pp); + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + + PickFilter filter = { + .architecture = _ARCHITECTURE_INVALID, + .suffix = ".raw", + }; + + if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) { + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(streq_ptr(result.version, "99")); + assert_se(result.architecture == ARCHITECTURE_X86); + assert_se(endswith(result.path, "/foo_99_x86.raw")); + + pick_result_done(&result); + } + + filter.architecture = ARCHITECTURE_X86_64; + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(streq_ptr(result.version, "55")); + assert_se(result.architecture == ARCHITECTURE_X86_64); + assert_se(endswith(result.path, "/foo_55_x86-64.raw")); + pick_result_done(&result); + + filter.architecture = ARCHITECTURE_IA64; + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(streq_ptr(result.version, "5")); + assert_se(result.architecture == ARCHITECTURE_IA64); + assert_se(endswith(result.path, "/foo_5_ia64.raw")); + pick_result_done(&result); + + filter.architecture = _ARCHITECTURE_INVALID; + filter.version = "5"; + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(streq_ptr(result.version, "5")); + if (native_architecture() != ARCHITECTURE_IA64) { + assert_se(result.architecture == _ARCHITECTURE_INVALID); + assert_se(endswith(result.path, "/foo_5.raw")); + } + pick_result_done(&result); + + filter.architecture = ARCHITECTURE_IA64; + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(streq_ptr(result.version, "5")); + assert_se(result.architecture == ARCHITECTURE_IA64); + assert_se(endswith(result.path, "/foo_5_ia64.raw")); + pick_result_done(&result); + + filter.architecture = ARCHITECTURE_CRIS; + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) == 0); + assert_se(result.st.st_mode == MODE_INVALID); + assert_se(!result.version); + assert_se(result.architecture < 0); + assert_se(!result.path); + + assert_se(unlinkat(sub_dfd, "foo_99_x86.raw", 0) >= 0); + + filter.architecture = _ARCHITECTURE_INVALID; + filter.version = NULL; + if (IN_SET(native_architecture(), ARCHITECTURE_X86_64, ARCHITECTURE_X86)) { + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(streq_ptr(result.version, "55")); + + if (native_architecture() == ARCHITECTURE_X86_64) { + assert_se(result.architecture == ARCHITECTURE_X86_64); + assert_se(endswith(result.path, "/foo_55_x86-64.raw")); + } else { + assert_se(result.architecture == ARCHITECTURE_X86); + assert_se(endswith(result.path, "/foo_55_x86.raw")); + } + pick_result_done(&result); + } + + /* Test explicit patterns in last component of path not being .v */ + free(pp); + pp = path_join(p, "foo.v/foo___.raw"); + assert_se(pp); + + if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) { + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(streq_ptr(result.version, "55")); + assert_se(result.architecture == native_architecture()); + assert_se(endswith(result.path, ".raw")); + assert_se(strrstr(result.path, "/foo_55_x86")); + pick_result_done(&result); + } + + /* Specify an explicit path */ + free(pp); + pp = path_join(p, "foo.v/foo_5.raw"); + assert_se(pp); + + filter.type_mask = UINT32_C(1) << DT_DIR; + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) == -ENOTDIR); + + filter.type_mask = UINT32_C(1) << DT_REG; + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(!result.version); + assert_se(result.architecture == _ARCHITECTURE_INVALID); + assert_se(path_equal(result.path, pp)); + pick_result_done(&result); + + free(pp); + pp = path_join(p, "foo.v"); + assert_se(pp); + + filter.architecture = ARCHITECTURE_S390; + filter.basename = "quux"; + + assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0); + assert_se(S_ISREG(result.st.st_mode)); + assert_se(streq_ptr(result.version, "2")); + assert_se(result.tries_left == 4); + assert_se(result.tries_done == 6); + assert_se(endswith(result.path, "quux_2_s390+4-6.raw")); + assert_se(result.architecture == ARCHITECTURE_S390); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); From 9e61ed11156d423dc171a3377119b39b1cc4a8d6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Mar 2023 18:26:45 +0100 Subject: [PATCH 05/12] vpick: add new tool "systemd-vpick" which exposes vpick on the command line Usecase: $ du $(systemd-vpick /srv/myimages.v/foo___.raw) In order to determine size of newest image in /srv/myimages.v/ --- meson.build | 1 + src/vpick/meson.build | 9 ++ src/vpick/vpick-tool.c | 350 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+) create mode 100644 src/vpick/meson.build create mode 100644 src/vpick/vpick-tool.c diff --git a/meson.build b/meson.build index d1433e3dbc..d2d255391d 100644 --- a/meson.build +++ b/meson.build @@ -2230,6 +2230,7 @@ subdir('src/vconsole') subdir('src/veritysetup') subdir('src/vmspawn') subdir('src/volatile-root') +subdir('src/vpick') subdir('src/xdg-autostart-generator') subdir('src/systemd') diff --git a/src/vpick/meson.build b/src/vpick/meson.build new file mode 100644 index 0000000000..a8c14cb584 --- /dev/null +++ b/src/vpick/meson.build @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + executable_template + { + 'name' : 'systemd-vpick', + 'public' : true, + 'sources' : files('vpick-tool.c'), + }, +] diff --git a/src/vpick/vpick-tool.c b/src/vpick/vpick-tool.c new file mode 100644 index 0000000000..0ae8fe322d --- /dev/null +++ b/src/vpick/vpick-tool.c @@ -0,0 +1,350 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "architecture.h" +#include "build.h" +#include "format-table.h" +#include "fs-util.h" +#include "main-func.h" +#include "path-util.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "stat-util.h" +#include "string-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "vpick.h" + +static char *arg_filter_basename = NULL; +static char *arg_filter_version = NULL; +static Architecture arg_filter_architecture = _ARCHITECTURE_INVALID; +static char *arg_filter_suffix = NULL; +static uint32_t arg_filter_type_mask = 0; +static enum { + PRINT_PATH, + PRINT_FILENAME, + PRINT_VERSION, + PRINT_TYPE, + PRINT_ARCHITECTURE, + PRINT_TRIES, + PRINT_ALL, + _PRINT_INVALID = -EINVAL, +} arg_print = _PRINT_INVALID; +static PickFlags arg_flags = PICK_ARCHITECTURE|PICK_TRIES; + +STATIC_DESTRUCTOR_REGISTER(arg_filter_basename, freep); +STATIC_DESTRUCTOR_REGISTER(arg_filter_version, freep); +STATIC_DESTRUCTOR_REGISTER(arg_filter_suffix, freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-vpick", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] PATH...\n" + "\n%5$sPick entry from versioned directory.%6$s\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + "\n%3$sLookup Keys:%4$s\n" + " -B --basename=BASENAME\n" + " Look for specified basename\n" + " -V VERSION Look for specified version\n" + " -A ARCH Look for specified architecture\n" + " -S --suffix=SUFFIX Look for specified suffix\n" + " -t --type=TYPE Look for specified inode type\n" + "\n%3$sOutput:%4$s\n" + " -p --print=filename Print selected filename rather than path\n" + " -p --print=version Print selected version rather than path\n" + " -p --print=type Print selected inode type rather than path\n" + " -p --print=arch Print selected architecture rather than path\n" + " -p --print=tries Print selected tries left/tries done rather than path\n" + " -p --print=all Print all of the above\n" + " --resolve=yes Canonicalize the result path\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), ansi_normal(), + ansi_highlight(), ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_RESOLVE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "basename", required_argument, NULL, 'B' }, + { "suffix", required_argument, NULL, 'S' }, + { "type", required_argument, NULL, 't' }, + { "print", required_argument, NULL, 'p' }, + { "resolve", required_argument, NULL, ARG_RESOLVE }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hB:V:A:S:t:p:", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case 'B': + if (!filename_part_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid basename string: %s", optarg); + + r = free_and_strdup_warn(&arg_filter_basename, optarg); + if (r < 0) + return r; + + break; + + case 'V': + if (!version_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid version string: %s", optarg); + + r = free_and_strdup_warn(&arg_filter_version, optarg); + if (r < 0) + return r; + + break; + + case 'A': + if (streq(optarg, "native")) + arg_filter_architecture = native_architecture(); + else if (streq(optarg, "secondary")) { +#ifdef ARCHITECTURE_SECONDARY + arg_filter_architecture = ARCHITECTURE_SECONDARY; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Local architecture has no secondary architecture."); +#endif + } else if (streq(optarg, "uname")) + arg_filter_architecture = uname_architecture(); + else if (streq(optarg, "auto")) + arg_filter_architecture = _ARCHITECTURE_INVALID; + else { + arg_filter_architecture = architecture_from_string(optarg); + if (arg_filter_architecture < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown architecture: %s", optarg); + } + break; + + case 'S': + if (!filename_part_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid suffix string: %s", optarg); + + r = free_and_strdup_warn(&arg_filter_suffix, optarg); + if (r < 0) + return r; + + break; + + case 't': + if (isempty(optarg)) + arg_filter_type_mask = 0; + else { + mode_t m; + + m = inode_type_from_string(optarg); + if (m == MODE_INVALID) + return log_error_errno(m, "Unknown inode type: %s", optarg); + + arg_filter_type_mask |= UINT32_C(1) << IFTODT(m); + } + + break; + + case 'p': + if (streq(optarg, "path")) + arg_print = PRINT_PATH; + else if (streq(optarg, "filename")) + arg_print = PRINT_FILENAME; + else if (streq(optarg, "version")) + arg_print = PRINT_VERSION; + else if (streq(optarg, "type")) + arg_print = PRINT_TYPE; + else if (STR_IN_SET(optarg, "arch", "architecture")) + arg_print = PRINT_ARCHITECTURE; + else if (streq(optarg, "tries")) + arg_print = PRINT_TRIES; + else if (streq(optarg, "all")) + arg_print = PRINT_ALL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown --print= argument: %s", optarg); + + break; + + case ARG_RESOLVE: + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --resolve= value: %m"); + + SET_FLAG(arg_flags, PICK_RESOLVE, r); + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + if (arg_print < 0) + arg_print = PRINT_PATH; + + return 1; +} + +static int run(int argc, char *argv[]) { + int r; + + log_show_color(true); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (optind >= argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path to resolve must be specified."); + + for (int i = optind; i < argc; i++) { + _cleanup_free_ char *p = NULL; + r = path_make_absolute_cwd(argv[i], &p); + if (r < 0) + return log_error_errno(r, "Failed to make path '%s' absolute: %m", argv[i]); + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + r = path_pick(/* toplevel_path= */ NULL, + /* toplevel_fd= */ AT_FDCWD, + p, + &(PickFilter) { + .basename = arg_filter_basename, + .version = arg_filter_version, + .architecture = arg_filter_architecture, + .suffix = arg_filter_suffix, + .type_mask = arg_filter_type_mask, + }, + arg_flags, + &result); + if (r < 0) + return log_error_errno(r, "Failed to pick version for '%s': %m", p); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No matching version for '%s' found.", p); + + switch (arg_print) { + + case PRINT_PATH: + fputs(result.path, stdout); + if (result.st.st_mode != MODE_INVALID && S_ISDIR(result.st.st_mode) && !endswith(result.path, "/")) + fputc('/', stdout); + fputc('\n', stdout); + break; + + case PRINT_FILENAME: { + _cleanup_free_ char *fname = NULL; + + r = path_extract_filename(result.path, &fname); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from path '%s': %m", result.path); + + puts(fname); + break; + } + + case PRINT_VERSION: + if (!result.version) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No version information discovered."); + + puts(result.version); + break; + + case PRINT_TYPE: + if (result.st.st_mode == MODE_INVALID) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No inode type information discovered."); + + puts(inode_type_to_string(result.st.st_mode)); + break; + + case PRINT_ARCHITECTURE: + if (result.architecture < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No architecture information discovered."); + + puts(architecture_to_string(result.architecture)); + break; + + case PRINT_TRIES: + if (result.tries_left == UINT_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No tries left/tries done information discovered."); + + printf("+%u-%u", result.tries_left, result.tries_done); + break; + + case PRINT_ALL: { + _cleanup_(table_unrefp) Table *t = NULL; + + t = table_new_vertical(); + if (!t) + return log_oom(); + + table_set_ersatz_string(t, TABLE_ERSATZ_NA); + + r = table_add_many( + t, + TABLE_FIELD, "Path", + TABLE_PATH, result.path, + TABLE_FIELD, "Version", + TABLE_STRING, result.version, + TABLE_FIELD, "Type", + TABLE_STRING, result.st.st_mode == MODE_INVALID ? NULL : inode_type_to_string(result.st.st_mode), + TABLE_FIELD, "Architecture", + TABLE_STRING, result.architecture < 0 ? NULL : architecture_to_string(result.architecture)); + if (r < 0) + return table_log_add_error(r); + + if (result.tries_left != UINT_MAX) { + r = table_add_many( + t, + TABLE_FIELD, "Tries left", + TABLE_UINT, result.tries_left, + TABLE_FIELD, "Tries done", + TABLE_UINT, result.tries_done); + if (r < 0) + return table_log_add_error(r); + } + + r = table_print(t, stdout); + if (r < 0) + return table_log_print_error(r); + + break; + } + + default: + assert_not_reached(); + } + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); From 300a03bec3fa6285c2803545b41268d978a64ea8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Mar 2023 18:25:37 +0100 Subject: [PATCH 06/12] nspawn: hook up --image=/--directory=/--template= with vpick logic --- src/nspawn/nspawn.c | 69 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 4d3783e00a..9e53c51f1a 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -112,6 +112,7 @@ #include "umask-util.h" #include "unit-name.h" #include "user-util.h" +#include "vpick.h" /* The notify socket inside the container it can use to talk to nspawn using the sd_notify(3) protocol */ #define NSPAWN_NOTIFY_SOCKET_PATH "/run/host/notify" @@ -2911,14 +2912,72 @@ static int on_request_stop(sd_bus_message *m, void *userdata, sd_bus_error *erro return 0; } +static int pick_paths(void) { + int r; + + if (arg_directory) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + PickFilter filter = pick_filter_image_dir; + + filter.architecture = arg_architecture; + + r = path_pick_update_warn( + &arg_directory, + &filter, + PICK_ARCHITECTURE|PICK_TRIES, + &result); + if (r < 0) { + /* Accept ENOENT here so that the --template= logic can work */ + if (r != -ENOENT) + return r; + } else + arg_architecture = result.architecture; + } + + if (arg_image) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + PickFilter filter = pick_filter_image_raw; + + filter.architecture = arg_architecture; + + r = path_pick_update_warn( + &arg_image, + &filter, + PICK_ARCHITECTURE|PICK_TRIES, + &result); + if (r < 0) + return r; + + arg_architecture = result.architecture; + } + + if (arg_template) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + PickFilter filter = pick_filter_image_dir; + + filter.architecture = arg_architecture; + + r = path_pick_update_warn( + &arg_template, + &filter, + PICK_ARCHITECTURE, + &result); + if (r < 0) + return r; + + arg_architecture = result.architecture; + } + + return 0; +} + static int determine_names(void) { int r; if (arg_template && !arg_directory && arg_machine) { - /* If --template= was specified then we should not - * search for a machine, but instead create a new one - * in /var/lib/machine. */ + /* If --template= was specified then we should not search for a machine, but instead create a + * new one in /var/lib/machine. */ arg_directory = path_join("/var/lib/machines", arg_machine); if (!arg_directory) @@ -5406,6 +5465,10 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; + r = pick_paths(); + if (r < 0) + goto finish; + r = determine_names(); if (r < 0) goto finish; From d7688568191077b36c71e8ad5b5356695c69b406 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 20 Jun 2023 22:05:53 +0200 Subject: [PATCH 07/12] dissect: port to vpick for selecting image --- src/dissect/dissect.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 6a8193f618..10af26888e 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -48,6 +48,7 @@ #include "tmpfile-util.h" #include "uid-alloc-range.h" #include "user-util.h" +#include "vpick.h" static enum { ACTION_DISSECT, @@ -1817,6 +1818,16 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; + if (arg_image) { + r = path_pick_update_warn( + &arg_image, + &pick_filter_image_raw, + PICK_ARCHITECTURE|PICK_TRIES, + /* ret_result= */ NULL); + if (r < 0) + return r; + } + switch (arg_action) { case ACTION_UMOUNT: return action_umount(arg_path); From 0cb110231edf6982019e7e6ded43416325c55c6c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Nov 2023 18:01:19 +0100 Subject: [PATCH 08/12] execute: teach RootDirectory= and RootImage= the new vpick logic --- src/core/exec-invoke.c | 154 ++++++++++++++++++++++++++++++++++------- 1 file changed, 128 insertions(+), 26 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index f76db133b1..3b3250f32e 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -59,6 +59,7 @@ #include "strv.h" #include "terminal-util.h" #include "utmp-wtmp.h" +#include "vpick.h" #define IDLE_TIMEOUT_USEC (5*USEC_PER_SEC) #define IDLE_TIMEOUT2_USEC (1*USEC_PER_SEC) @@ -2859,13 +2860,33 @@ static bool insist_on_sandboxing( return false; } -static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) { +static int setup_ephemeral( + const ExecContext *context, + ExecRuntime *runtime, + char **root_image, /* both input and output! modified if ephemeral logic enabled */ + char **root_directory) { /* ditto */ + _cleanup_close_ int fd = -EBADF; + _cleanup_free_ char *new_root = NULL; int r; + assert(context); + assert(root_image); + assert(root_directory); + + if (!*root_image && !*root_directory) + return 0; + if (!runtime || !runtime->ephemeral_copy) return 0; + assert(runtime->ephemeral_storage_socket[0] >= 0); + assert(runtime->ephemeral_storage_socket[1] >= 0); + + new_root = strdup(runtime->ephemeral_copy); + if (!new_root) + return log_oom_debug(); + r = posix_lock(runtime->ephemeral_storage_socket[0], LOCK_EX); if (r < 0) return log_debug_errno(r, "Failed to lock ephemeral storage socket: %m"); @@ -2876,28 +2897,23 @@ static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) { if (fd >= 0) /* We got an fd! That means ephemeral has already been set up, so nothing to do here. */ return 0; - if (fd != -EAGAIN) return log_debug_errno(fd, "Failed to receive file descriptor queued on ephemeral storage socket: %m"); - log_debug("Making ephemeral snapshot of %s to %s", - context->root_image ?: context->root_directory, runtime->ephemeral_copy); + if (*root_image) { + log_debug("Making ephemeral copy of %s to %s", *root_image, new_root); - if (context->root_image) - fd = copy_file(context->root_image, runtime->ephemeral_copy, O_EXCL, 0600, - COPY_LOCK_BSD|COPY_REFLINK|COPY_CRTIME); - else - fd = btrfs_subvol_snapshot_at(AT_FDCWD, context->root_directory, - AT_FDCWD, runtime->ephemeral_copy, - BTRFS_SNAPSHOT_FALLBACK_COPY | - BTRFS_SNAPSHOT_FALLBACK_DIRECTORY | - BTRFS_SNAPSHOT_RECURSIVE | - BTRFS_SNAPSHOT_LOCK_BSD); - if (fd < 0) - return log_debug_errno(fd, "Failed to snapshot %s to %s: %m", - context->root_image ?: context->root_directory, runtime->ephemeral_copy); + fd = copy_file(*root_image, + new_root, + O_EXCL, + 0600, + COPY_LOCK_BSD| + COPY_REFLINK| + COPY_CRTIME); + if (fd < 0) + return log_debug_errno(fd, "Failed to copy image %s to %s: %m", + *root_image, new_root); - if (context->root_image) { /* A root image might be subject to lots of random writes so let's try to disable COW on it * which tends to not perform well in combination with lots of random writes. * @@ -2906,13 +2922,35 @@ static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) { */ r = chattr_fd(fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL); if (r < 0) - log_debug_errno(fd, "Failed to disable copy-on-write for %s, ignoring: %m", runtime->ephemeral_copy); + log_debug_errno(fd, "Failed to disable copy-on-write for %s, ignoring: %m", new_root); + } else { + assert(*root_directory); + + log_debug("Making ephemeral snapshot of %s to %s", *root_directory, new_root); + + fd = btrfs_subvol_snapshot_at( + AT_FDCWD, *root_directory, + AT_FDCWD, new_root, + BTRFS_SNAPSHOT_FALLBACK_COPY | + BTRFS_SNAPSHOT_FALLBACK_DIRECTORY | + BTRFS_SNAPSHOT_RECURSIVE | + BTRFS_SNAPSHOT_LOCK_BSD); + if (fd < 0) + return log_debug_errno(fd, "Failed to snapshot directory %s to %s: %m", + *root_directory, new_root); } r = send_one_fd(runtime->ephemeral_storage_socket[1], fd, MSG_DONTWAIT); if (r < 0) return log_debug_errno(r, "Failed to queue file descriptor on ephemeral storage socket: %m"); + if (*root_image) + free_and_replace(*root_image, new_root); + else { + assert(*root_directory); + free_and_replace(*root_directory, new_root); + } + return 1; } @@ -2972,6 +3010,63 @@ static int verity_settings_prepare( return 0; } +static int pick_versions( + const ExecContext *context, + const ExecParameters *params, + char **ret_root_image, + char **ret_root_directory) { + + int r; + + assert(context); + assert(params); + assert(ret_root_image); + assert(ret_root_directory); + + if (context->root_image) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + + r = path_pick(/* toplevel_path= */ NULL, + /* toplevel_fd= */ AT_FDCWD, + context->root_image, + &pick_filter_image_raw, + PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, + &result); + if (r < 0) + return r; + + if (!result.path) + return log_exec_debug_errno(context, params, SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_image); + + *ret_root_image = TAKE_PTR(result.path); + *ret_root_directory = NULL; + return r; + } + + if (context->root_directory) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + + r = path_pick(/* toplevel_path= */ NULL, + /* toplevel_fd= */ AT_FDCWD, + context->root_directory, + &pick_filter_image_dir, + PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, + &result); + if (r < 0) + return r; + + if (!result.path) + return log_exec_debug_errno(context, params, SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_directory); + + *ret_root_image = NULL; + *ret_root_directory = TAKE_PTR(result.path); + return r; + } + + *ret_root_image = *ret_root_directory = NULL; + return 0; +} + static int apply_mount_namespace( ExecCommandFlags command_flags, const ExecContext *context, @@ -2984,8 +3079,8 @@ static int apply_mount_namespace( _cleanup_strv_free_ char **empty_directories = NULL, **symlinks = NULL, **read_write_paths_cleanup = NULL; _cleanup_free_ char *creds_path = NULL, *incoming_dir = NULL, *propagate_dir = NULL, - *extension_dir = NULL, *host_os_release_stage = NULL; - const char *root_dir = NULL, *root_image = NULL, *tmp_dir = NULL, *var_tmp_dir = NULL; + *extension_dir = NULL, *host_os_release_stage = NULL, *root_image = NULL, *root_dir = NULL; + const char *tmp_dir = NULL, *var_tmp_dir = NULL; char **read_write_paths; bool needs_sandboxing, setup_os_release_symlink; BindMount *bind_mounts = NULL; @@ -2997,14 +3092,21 @@ static int apply_mount_namespace( CLEANUP_ARRAY(bind_mounts, n_bind_mounts, bind_mount_free_many); if (params->flags & EXEC_APPLY_CHROOT) { - r = setup_ephemeral(context, runtime); + r = pick_versions( + context, + params, + &root_image, + &root_dir); if (r < 0) return r; - if (context->root_image) - root_image = (runtime ? runtime->ephemeral_copy : NULL) ?: context->root_image; - else - root_dir = (runtime ? runtime->ephemeral_copy : NULL) ?: context->root_directory; + r = setup_ephemeral( + context, + runtime, + &root_image, + &root_dir); + if (r < 0) + return r; } r = compile_bind_mounts(context, params, &bind_mounts, &n_bind_mounts, &empty_directories); From a5ecdf7c6bad80a8a8b1f89d439471c17f3e95aa Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 Nov 2023 18:36:24 +0100 Subject: [PATCH 09/12] discover-image: add support for vpick --- src/shared/discover-image.c | 329 +++++++++++++++++++++++++++++------- 1 file changed, 272 insertions(+), 57 deletions(-) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 3baa84c8bd..e7e3a4d71c 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -45,6 +45,7 @@ #include "strv.h" #include "time-util.h" #include "utf8.h" +#include "vpick.h" #include "xattr-util.h" static const char* const image_search_path[_IMAGE_CLASS_MAX] = { @@ -215,40 +216,60 @@ static int image_new( return 0; } -static int extract_pretty( +static int extract_image_basename( const char *path, - const char *class_suffix, - const char *format_suffix, - char **ret) { + const char *class_suffix, /* e.g. ".sysext" (this is an optional suffix) */ + char **format_suffixes, /* e.g. ".raw" (one of these will be required) */ + char **ret_basename, + char **ret_suffix) { - _cleanup_free_ char *name = NULL; + _cleanup_free_ char *name = NULL, *suffix = NULL; int r; assert(path); - assert(ret); r = path_extract_filename(path, &name); if (r < 0) return r; - if (format_suffix) { - char *e = endswith(name, format_suffix); + if (format_suffixes) { + char *e = strv_endswith(name, format_suffixes); if (!e) /* Format suffix is required */ return -EINVAL; + if (ret_suffix) { + suffix = strdup(e); + if (!suffix) + return -ENOMEM; + } + *e = 0; } if (class_suffix) { char *e = endswith(name, class_suffix); - if (e) /* Class suffix is optional */ + if (e) { /* Class suffix is optional */ + if (ret_suffix) { + _cleanup_free_ char *j = strjoin(e, suffix); + if (!j) + return -ENOMEM; + + free_and_replace(suffix, j); + } + *e = 0; + } } if (!image_name_is_valid(name)) return -EINVAL; - *ret = TAKE_PTR(name); + if (ret_suffix) + *ret_suffix = TAKE_PTR(suffix); + + if (ret_basename) + *ret_basename = TAKE_PTR(name); + return 0; } @@ -303,7 +324,12 @@ static int image_make( return 0; if (!pretty) { - r = extract_pretty(filename, image_class_suffix_to_string(c), NULL, &pretty_buffer); + r = extract_image_basename( + filename, + image_class_suffix_to_string(c), + /* format_suffix= */ NULL, + &pretty_buffer, + /* ret_suffix= */ NULL); if (r < 0) return r; @@ -390,7 +416,12 @@ static int image_make( (void) fd_getcrtime_at(dfd, filename, AT_SYMLINK_FOLLOW, &crtime); if (!pretty) { - r = extract_pretty(filename, image_class_suffix_to_string(c), ".raw", &pretty_buffer); + r = extract_image_basename( + filename, + image_class_suffix_to_string(c), + STRV_MAKE(".raw"), + &pretty_buffer, + /* ret_suffix= */ NULL); if (r < 0) return r; @@ -424,7 +455,12 @@ static int image_make( return 0; if (!pretty) { - r = extract_pretty(filename, NULL, NULL, &pretty_buffer); + r = extract_image_basename( + filename, + /* class_suffix= */ NULL, + /* format_suffix= */ NULL, + &pretty_buffer, + /* ret_suffix= */ NULL); if (r < 0) return r; @@ -488,6 +524,37 @@ static const char *pick_image_search_path(ImageClass class) { return in_initrd() && image_search_path_initrd[class] ? image_search_path_initrd[class] : image_search_path[class]; } +static char **make_possible_filenames(ImageClass class, const char *image_name) { + _cleanup_strv_free_ char **l = NULL; + + assert(image_name); + + FOREACH_STRING(v_suffix, "", ".v") + FOREACH_STRING(format_suffix, "", ".raw") { + _cleanup_free_ char *j = NULL; + const char *class_suffix; + + class_suffix = image_class_suffix_to_string(class); + if (class_suffix) { + j = strjoin(image_name, class_suffix, format_suffix, v_suffix); + if (!j) + return NULL; + + if (strv_consume(&l, TAKE_PTR(j)) < 0) + return NULL; + } + + j = strjoin(image_name, format_suffix, v_suffix); + if (!j) + return NULL; + + if (strv_consume(&l, TAKE_PTR(j)) < 0) + return NULL; + } + + return TAKE_PTR(l); +} + int image_find(ImageClass class, const char *name, const char *root, @@ -503,6 +570,10 @@ int image_find(ImageClass class, if (!image_name_is_valid(name)) return -ENOENT; + _cleanup_strv_free_ char **names = make_possible_filenames(class, name); + if (!names) + return -ENOMEM; + NULSTR_FOREACH(path, pick_image_search_path(class)) { _cleanup_free_ char *resolved = NULL; _cleanup_closedir_ DIR *d = NULL; @@ -519,43 +590,97 @@ int image_find(ImageClass class, * to symlink block devices into the search path. (For now, we disable that when operating * relative to some root directory.) */ flags = root ? AT_SYMLINK_NOFOLLOW : 0; - if (fstatat(dirfd(d), name, &st, flags) < 0) { - _cleanup_free_ char *raw = NULL; - if (errno != ENOENT) - return -errno; + STRV_FOREACH(n, names) { + _cleanup_free_ char *fname_buf = NULL; + const char *fname = *n; - raw = strjoin(name, ".raw"); - if (!raw) - return -ENOMEM; + if (fstatat(dirfd(d), fname, &st, flags) < 0) { + if (errno != ENOENT) + return -errno; - if (fstatat(dirfd(d), raw, &st, flags) < 0) { - if (errno == ENOENT) - continue; - - return -errno; + continue; /* Vanished while we were looking at it */ } - if (!S_ISREG(st.st_mode)) + if (endswith(fname, ".raw")) { + if (!S_ISREG(st.st_mode)) { + log_debug("Ignoring non-regular file '%s' with .raw suffix.", fname); + continue; + } + + } else if (endswith(fname, ".v")) { + + if (!S_ISDIR(st.st_mode)) { + log_debug("Ignoring non-directory file '%s' with .v suffix.", fname); + continue; + } + + _cleanup_free_ char *suffix = NULL; + suffix = strdup(ASSERT_PTR(startswith(fname, name))); + if (!suffix) + return -ENOMEM; + + *ASSERT_PTR(endswith(suffix, ".v")) = 0; + + _cleanup_free_ char *vp = path_join(resolved, fname); + if (!vp) + return -ENOMEM; + + PickFilter filter = { + .type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR), + .basename = name, + .architecture = _ARCHITECTURE_INVALID, + .suffix = suffix, + }; + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + r = path_pick(root, + /* toplevel_fd= */ AT_FDCWD, + vp, + &filter, + PICK_ARCHITECTURE|PICK_TRIES, + &result); + if (r < 0) { + log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp); + continue; + } + if (!result.path) { + log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp); + continue; + } + + /* Refresh the stat data for the discovered target */ + st = result.st; + + _cleanup_free_ char *bn = NULL; + r = path_extract_filename(result.path, &bn); + if (r < 0) { + log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path); + continue; + } + + fname_buf = path_join(fname, bn); + if (!fname_buf) + return log_oom(); + + fname = fname_buf; + + } else if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode)) { + log_debug("Ignoring non-directory and non-block device file '%s' without suffix.", fname); continue; + } - r = image_make(class, name, dirfd(d), resolved, raw, &st, ret); - - } else { - if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode)) + r = image_make(class, name, dirfd(d), resolved, fname, &st, ret); + if (IN_SET(r, -ENOENT, -EMEDIUMTYPE)) continue; + if (r < 0) + return r; - r = image_make(class, name, dirfd(d), resolved, name, &st, ret); + if (ret) + (*ret)->discoverable = true; + + return 1; } - if (IN_SET(r, -ENOENT, -EMEDIUMTYPE)) - continue; - if (r < 0) - return r; - - if (ret) - (*ret)->discoverable = true; - - return 1; } if (class == IMAGE_MACHINE && streq(name, ".host")) { @@ -566,7 +691,7 @@ int image_find(ImageClass class, if (ret) (*ret)->discoverable = true; - return r; + return 1; } return -ENOENT; @@ -613,43 +738,133 @@ int image_discover( return r; FOREACH_DIRENT_ALL(de, d, return -errno) { + _cleanup_free_ char *pretty = NULL, *fname_buf = NULL; _cleanup_(image_unrefp) Image *image = NULL; - _cleanup_free_ char *pretty = NULL; + const char *fname = de->d_name; struct stat st; int flags; - if (dot_or_dot_dot(de->d_name)) + if (dot_or_dot_dot(fname)) continue; /* As mentioned above, we follow symlinks on this fstatat(), because we want to * permit people to symlink block devices into the search path. */ flags = root ? AT_SYMLINK_NOFOLLOW : 0; - if (fstatat(dirfd(d), de->d_name, &st, flags) < 0) { + if (fstatat(dirfd(d), fname, &st, flags) < 0) { if (errno == ENOENT) continue; return -errno; } - if (S_ISREG(st.st_mode)) - r = extract_pretty(de->d_name, image_class_suffix_to_string(class), ".raw", &pretty); - else if (S_ISDIR(st.st_mode)) - r = extract_pretty(de->d_name, image_class_suffix_to_string(class), NULL, &pretty); - else if (S_ISBLK(st.st_mode)) - r = extract_pretty(de->d_name, NULL, NULL, &pretty); - else { - log_debug("Skipping directory entry '%s', which is neither regular file, directory nor block device.", de->d_name); - continue; - } - if (r < 0) { - log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", de->d_name); + if (S_ISREG(st.st_mode)) { + r = extract_image_basename( + fname, + image_class_suffix_to_string(class), + STRV_MAKE(".raw"), + &pretty, + /* suffix= */ NULL); + if (r < 0) { + log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname); + continue; + } + } else if (S_ISDIR(st.st_mode)) { + const char *v; + + v = endswith(fname, ".v"); + if (v) { + _cleanup_free_ char *suffix = NULL, *nov = NULL; + + nov = strndup(fname, v - fname); /* Chop off the .v */ + if (!nov) + return -ENOMEM; + + r = extract_image_basename( + nov, + image_class_suffix_to_string(class), + STRV_MAKE(".raw", ""), + &pretty, + &suffix); + if (r < 0) { + log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like a versioned image.", fname); + continue; + } + + _cleanup_free_ char *vp = path_join(resolved, fname); + if (!vp) + return -ENOMEM; + + PickFilter filter = { + .type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR), + .basename = pretty, + .architecture = _ARCHITECTURE_INVALID, + .suffix = suffix, + }; + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + r = path_pick(root, + /* toplevel_fd= */ AT_FDCWD, + vp, + &filter, + PICK_ARCHITECTURE|PICK_TRIES, + &result); + if (r < 0) { + log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp); + continue; + } + if (!result.path) { + log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp); + continue; + } + + /* Refresh the stat data for the discovered target */ + st = result.st; + + _cleanup_free_ char *bn = NULL; + r = path_extract_filename(result.path, &bn); + if (r < 0) { + log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path); + continue; + } + + fname_buf = path_join(fname, bn); + if (!fname_buf) + return log_oom(); + + fname = fname_buf; + } else { + r = extract_image_basename( + fname, + image_class_suffix_to_string(class), + /* format_suffix= */ NULL, + &pretty, + /* ret_suffix= */ NULL); + if (r < 0) { + log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname); + continue; + } + } + + } else if (S_ISBLK(st.st_mode)) { + r = extract_image_basename( + fname, + /* class_suffix= */ NULL, + /* format_suffix= */ NULL, + &pretty, + /* ret_v_suffix= */ NULL); + if (r < 0) { + log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname); + continue; + } + } else { + log_debug("Skipping directory entry '%s', which is neither regular file, directory nor block device.", fname); continue; } if (hashmap_contains(h, pretty)) continue; - r = image_make(class, pretty, dirfd(d), resolved, de->d_name, &st, &image); + r = image_make(class, pretty, dirfd(d), resolved, fname, &st, &image); if (IN_SET(r, -ENOENT, -EMEDIUMTYPE)) continue; if (r < 0) From 7d93e4af8088fae7b50eb638c6e297fb8371e307 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Nov 2023 11:31:02 +0100 Subject: [PATCH 10/12] man: document the new vpick concept --- man/rules/meson.build | 2 + man/systemd-dissect.xml | 5 + man/systemd-nspawn.xml | 26 +++--- man/systemd-vpick.xml | 198 ++++++++++++++++++++++++++++++++++++++++ man/systemd.exec.xml | 8 ++ man/systemd.v.xml | 163 +++++++++++++++++++++++++++++++++ 6 files changed, 391 insertions(+), 11 deletions(-) create mode 100644 man/systemd-vpick.xml create mode 100644 man/systemd.v.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index c99f79eba8..3592b862f7 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1131,6 +1131,7 @@ manpages = [ 'HAVE_LIBCRYPTSETUP'], ['systemd-vmspawn', '1', [], 'ENABLE_VMSPAWN'], ['systemd-volatile-root.service', '8', ['systemd-volatile-root'], ''], + ['systemd-vpick', '1', [], ''], ['systemd-xdg-autostart-generator', '8', [], 'ENABLE_XDG_AUTOSTART'], ['systemd', '1', ['init'], ''], ['systemd.automount', '5', [], ''], @@ -1165,6 +1166,7 @@ manpages = [ ['systemd.time', '7', [], ''], ['systemd.timer', '5', [], ''], ['systemd.unit', '5', [], ''], + ['systemd.v', '7', [], ''], ['sysupdate.d', '5', [], 'ENABLE_SYSUPDATE'], ['sysusers.d', '5', [], 'ENABLE_SYSUSERS'], ['telinit', '8', [], 'HAVE_SYSV_COMPAT'], diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml index b238a9f7d0..85166f23d3 100644 --- a/man/systemd-dissect.xml +++ b/man/systemd-dissect.xml @@ -120,6 +120,10 @@ mounted directly by mount and fstab5. For details see below. + + In place of the image path a .v/ versioned directory may be specified, see + systemd.v7 for + details. @@ -543,6 +547,7 @@ systemd1 systemd-nspawn1 systemd.exec5 + systemd.v7 Discoverable Partitions Specification mount8 umount8 diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index e721f0e5ab..1a9fb09aae 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -209,21 +209,21 @@ - Directory to use as file system root for the - container. + Directory to use as file system root for the container. - If neither , nor - is specified the directory is - determined by searching for a directory named the same as the - machine name specified with . See + If neither , nor is specified the + directory is determined by searching for a directory named the same as the machine name specified + with . See machinectl1 section "Files and Directories" for the precise search path. - If neither , - , nor - are specified, the current directory will - be used. May not be specified together with - . + In place of the directory path a .v/ versioned directory may be specified, see + systemd.v7 for + details. + + If neither , , nor + are specified, the current directory will be used. May not be specified + together with . @@ -317,6 +317,10 @@ Any other partitions, such as foreign partitions or swap partitions are not mounted. May not be specified together with , . + In place of the image path a .v/ versioned directory may be specified, see + systemd.v7 for + details. + diff --git a/man/systemd-vpick.xml b/man/systemd-vpick.xml new file mode 100644 index 0000000000..95f946a84c --- /dev/null +++ b/man/systemd-vpick.xml @@ -0,0 +1,198 @@ + + + + + + + + systemd-vpick + systemd + + + + systemd-vpick + 1 + + + + systemd-vpick + Resolve paths to .v/ versioned directories + + + + + systemd-vpick OPTIONS PATH + + + + + Description + + systemd-vpick resolves a file system path referencing a .v/ + versioned directory to a path to the newest (by version) file contained therein. This tool provides a + command line interface for the + systemd.v7 + logic. + + The tool expects a path to a .v/ directory as argument (either directly, or with + a triple underscore pattern as final component). It then determines the newest file contained in that + directory, and writes its path to standard output. + + Unless the triple underscore pattern is passed as last component of the path, it is typically + necessary to at least specify the switch to configure the file suffix to look + for. + + If the specified path does not reference a .v/ path (i.e. neither the final + component ends in .v, nor the penultimate does or the final one does contain a triple + underscore) it specified path is written unmodified to standard output. + + + + Options + + The following options are understood: + + + + + + + Overrides the "basename" of the files to look for, i.e. the part to the left of the + variable part of the filenames. Normally this is derived automatically from the filename of the + .v component of the specified path, or from the triple underscore pattern in the + last component of the specified path. + + + + + + + + Explicitly configures the version to select. If specified, a filename with the + specified version string will be looked for, instead of the newest version + available. + + + + + + + + Explicitly configures the architecture to select. If specified, a filename with the + specified architecture identifier will be looked for. If not specified only filenames with a locally + supported architecture are considered, or those without any architecture identifier. + + + + + + + + + Configures the suffix of the filenames to consider. For the .v/ + logic it is necessary to specify the suffix to look for, and the .v/ component + must also carry the suffix immediately before .v in its name. + + + + + + + + + Configures the inode type to look for in the .v/ directory. Takes + one of reg, dir, sock, + fifo, blk, chr, lnk as + argument, each identifying an inode type. See inode7 for + details about inode types. If this option is used inodes not matching the specified type are filtered + and not taken into consideration. + + + + + + + + + Configures what precisely to write to standard output. If not specified prints the + full, resolved path of the newest matching file in the .v/ directory. This switch can be set to one of the following: + + + If set to filename, will print only the filename instead of the full path of the resolved file. + If set to version, will print only the version of the resolved file. + If set to type, will print only the inode type of the resolved + file (i.e. a string such as reg for regular files, or dir for + directories). + If set to arch, will print only the architecture of the resolved + file. + If set to tries, will print only the tries left/tries done of the + resolved file. + If set to all, will print all of the above in a simple tabular + output. + + + + + + + + + Takes a boolean argument. If true the path to the versioned file is fully + canonicalized (i.e. symlinks resolved, and redundant path components removed) before it is shown. If + false (the default) this is not done, and the path is shown without canonicalization. + + + + + + + + + + + Examples + + Use a command like the following to automatically pick the newest raw disk image from a + .v/ directory: + + $ systemd-vpick --suffix=.raw --type=reg /var/lib/machines/quux.raw.v/ + + This will enumerate all regular files matching + /var/lib/machines/quux.raw.v/quux*.raw, filter and sort them according to the rules + described in + systemd.v7, and then + write the path to the newest (by version) file to standard output. + + Use a command like the following to automatically pick the newest OS directory tree from a + .v/ directory: + + $ systemd-vpick --type=dir /var/lib/machines/waldo.v/ + + This will enumerate all directory inodes matching + /var/lib/machines/waldo.v/waldo*, filter and sort them according to the rules + described in + systemd.v7, and then + write the path to the newest (by version) directory to standard output. + + For further examples see + systemd.v7. + + + + Exit status + + On success, 0 is returned, a non-zero failure code + otherwise. + + + + See Also + + systemd1 + systemd.v7 + + + diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index e2b2910084..42e6ff8fd7 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -155,6 +155,10 @@ BindReadOnlyPaths=/dev/log /run/systemd/journal/socket /run/systemd/journal/stdout + In place of the directory path a .v/ versioned directory may be specified, + see systemd.v7 for + details. + @@ -191,6 +195,10 @@ systemd-soft-reboot.service8), in case the service is configured to survive it. + In place of the image path a .v/ versioned directory may be specified, see + systemd.v7 for + details. + diff --git a/man/systemd.v.xml b/man/systemd.v.xml new file mode 100644 index 0000000000..6cc6122a34 --- /dev/null +++ b/man/systemd.v.xml @@ -0,0 +1,163 @@ + + + + + + + + systemd.v + systemd + + + + systemd.v + 7 + + + + systemd.v + Directory with Versioned Resources + + + + Description + + In various places systemd components accept paths whose trailing components have the + .v/ suffix, pointing to a directory. These components will then automatically look for + suitable files inside the directory, do a version comparison and open the newest file found (by + version). Specifically, two expressions are supported: + + + + When looking for files with a suffix .SUFFIX, and a path + PATH/NAME.SUFFIX.v/ + is specified, then all files + PATH/NAME.SUFFIX.v/NAME_*.SUFFIX + are enumerated, filtered, sorted and the newest file used. The primary sorting key is the + variable part, here indicated by the wildcard + *. + + When a path + PATH.v/NAME___.SUFFIX + is specified (i.e. the penultimate component of the path ends in .v and the final + component contains a triple underscore), then all files + PATH.v/NAME_*.SUFFIX + are enumerated, filtered, sorted and the newest file used (again, by the variable + part, here indicated by the wildcard *). + + + To illustrate this in an example, consider a directory /var/lib/machines/mymachine.raw.v/, which is populated with three files: + + + mymachine_7.5.13.raw + mymachine_7.5.14.raw + mymachine_7.6.0.raw + + + Invoke a tool such as systemd-nspawn1 with a command line like the following: + + # systemd-nspawn --image=/var/lib/machines/mymachine.raw.v --boot + + Then this would automatically be resolved to the equivalent of: + + # systemd-nspawn --image=/var/lib/machines/mymachine.raw.v/mymachine_7.6.0.raw --boot + + Much of systemd's functionality that expects a path to a disk image or OS directory hierarchy + support the .v/ versioned directory mechanism, for example + systemd-nspawn1, + systemd-dissect1 or + the RootDirectory=/RootImage= settings of service files (see + systemd.exec5). + + Use the + systemd-vpick1 tool to + resolve .v/ paths from the command line, for example for usage in shell + scripts. + + + + Filtering and Sorting + + The variable part of the filenames in the .v/ directories are filtered and + compared primarily with a version comparison, implementing Version Format + Specification. However, additional rules apply: + + + If the variable part is suffixed by one or two integer values ("tries left" and "tries + done") in the formats +LEFT or + +LEFT-DONE, then these + indicate usage attempt counters. The idea is that each time before a file is attempted to be used, its + "tries left" counter is decreased, and the "tries done" counter increased (simply by renaming the + file). When the file is successfully used (which for example could mean for an OS image: successfully + booted) the counters are removed from the file name, indicating that the file has been validated to + work correctly. This mechanism mirrors the boot assessment counters defined by Automatic Boot Assessment. Any filenames + with no boot counters or with a non-zero "tries left" counter are sorted before filenames with a zero + "tries left" counter. + + Preceeding the use counters (if they are specified), an optional CPU architecture + identifier may be specified in the filename (separated from the version with an underscore), as defined + in the architecture vocabulary of the ConditionArchitecture= unit file setting, as + documented in + systemd.unit5. Files + whose name indicates an architecture not supported locally are filtered and not considered for the + version comparison. + + The rest of the variable part is the version string. + + + Or in other words, the files in the .v/ directories should follow one of these + naming structures: + + + NAME_VERSION.SUFFIX + NAME_VERSION_ARCHITECTURE.SUFFIX + NAME_VERSION+LEFT.SUFFIX + NAME_VERSION+LEFT-DONE.SUFFIX + NAME_VERSION_ARCHITECTURE+LEFT.SUFFIX + NAME_VERSION_ARCHITECTURE+LEFT-DONE.SUFFIX + + + + + Example + + Here's a more comprehensive example, further extending the one described above. Consider a + directory /var/lib/machines/mymachine.raw.v/, which is populated with the following + files: + + + mymachine_7.5.13.raw + mymachine_7.5.14_x86-64.raw + mymachine_7.6.0_arm64.raw + mymachine_7.7.0_x86-64+0-5.raw + + + Now invoke the following command on an x86-64 machine: + + $ systemd-vpick --suffix=.raw /var/lib/machines/mymachine.raw.v/ + + This would resolve the specified path to + /var/lib/machines/mymachine.raw.v/mymachine_7.5.14_x86-64.raw. Explanation: even + though mymachine_7.7.0_x86-64+0-5.raw has the newest version, it is not preferred + because its tries left counter is zero. And even though mymachine_7.6.0_arm64.raw + has the second newest version it is also not considered, in this case because we operate on an x86_64 + system and the image is intended for arm64 CPUs. Finally, the mymachine_7.5.13.raw + image is not considered because it is older than mymachine_7.5.14_x86-64.raw. + + + + See Also + + systemd1 + systemd-vpick1 + systemd-nspawn1 + systemd-dissect1 + systemd.exec5 + systemd-sysupdate1 + + + + From 0345366ac3af33dbd0cf01e65ea77c30e5a55e4d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 16 Nov 2023 16:40:54 +0100 Subject: [PATCH 11/12] tests: add integration tests for vpick logic --- test/units/testsuite-50.sh | 14 ++++ test/units/testsuite-74.vpick.sh | 116 +++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100755 test/units/testsuite-74.vpick.sh diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index 28218ab6d7..b5d01bfc8e 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -493,6 +493,20 @@ systemd-sysext unmerge rm -rf /run/extensions/app-reject rm /var/lib/extensions/app-nodistro.raw +# Some super basic test that RootImage= works with .v/ dirs +VBASE="vtest$RANDOM" +VDIR="/tmp/${VBASE}.v" +mkdir "$VDIR" + +ln -s "${image}.raw" "$VDIR/${VBASE}_33.raw" +ln -s "${image}.raw" "$VDIR/${VBASE}_34.raw" +ln -s "${image}.raw" "$VDIR/${VBASE}_35.raw" + +systemd-run -P -p RootImage="$VDIR" cat /usr/lib/os-release | grep -q -F "MARKER=1" + +rm "$VDIR/${VBASE}_33.raw" "$VDIR/${VBASE}_34.raw" "$VDIR/${VBASE}_35.raw" +rmdir "$VDIR" + mkdir -p /run/machines /run/portables /run/extensions touch /run/machines/a.raw /run/portables/b.raw /run/extensions/c.raw diff --git a/test/units/testsuite-74.vpick.sh b/test/units/testsuite-74.vpick.sh new file mode 100755 index 0000000000..400097ffeb --- /dev/null +++ b/test/units/testsuite-74.vpick.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +at_exit() { + set +e + rm -rf /var/lib/machines/mymachine.raw.v + rm -rf /var/lib/machines/mytree.v + rm -rf /var/lib/machines/testroot.v + umount -l /tmp/dotvroot + rmdir /tmp/dotvroot +} + +trap at_exit EXIT + +mkdir -p /var/lib/machines/mymachine.raw.v + +touch /var/lib/machines/mymachine.raw.v/mymachine_7.5.13.raw +touch /var/lib/machines/mymachine.raw.v/mymachine_7.5.14_x86-64.raw +touch /var/lib/machines/mymachine.raw.v/mymachine_7.6.0_arm64.raw +touch /var/lib/machines/mymachine.raw.v/mymachine_7.7.0_x86-64+0-5.raw + +mkdir -p /var/lib/machines/mytree.v + +mkdir /var/lib/machines/mytree.v/mytree_33.4 +mkdir /var/lib/machines/mytree.v/mytree_33.5 +mkdir /var/lib/machines/mytree.v/mytree_36.0+0-5 +mkdir /var/lib/machines/mytree.v/mytree_37.0_arm64+2-3 +mkdir /var/lib/machines/mytree.v/mytree_38.0_arm64+0-5 + +ARCH="$(busctl get-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager Architecture | cut -d\" -f 2)" + +export SYSTEMD_LOG_LEVEL=debug + +if [ "$ARCH" = "x86-64" ] ; then + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.5.14_x86-64.raw" + + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.5.13)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.5.13.raw" + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.5.14)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.5.14_x86-64.raw" + (! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.6.0) + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.7.0)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.7.0_x86-64+0-5.raw" + + systemd-dissect --discover | grep "/var/lib/machines/mymachine.raw.v/mymachine_7.5.14_x86-64.raw" +elif [ "$ARCH" = "arm64" ] ; then + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.6.0_arm64.raw" + + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.5.13)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.5.13.raw" + (! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.5.14) + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.6.0)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.6.0_arm64.raw" + (! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.7.0) + + systemd-dissect --discover | grep "/var/lib/machines/mymachine.raw.v/mymachine_7.6.0_arm64.raw" +else + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.5.13.raw" + + test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.5.13)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.5.13.raw" + (! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.5.14) + (! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.6.0) + (! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -V 7.7.0) + + systemd-dissect --discover | grep "/var/lib/machines/mymachine.raw.v/mymachine_7.5.13.raw" +fi + +test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A x86-64)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.5.14_x86-64.raw" +test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.6.0_arm64.raw" +(! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A ia64) + +test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64 -p version)" = "7.6.0" +test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64 -p type)" = "reg" +test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64 -p filename)" = "mymachine_7.6.0_arm64.raw" +test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64 -p arch)" = "arm64" + +test "$(systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64 -t reg)" = "/var/lib/machines/mymachine.raw.v/mymachine_7.6.0_arm64.raw" +(! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64 -t dir) +(! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64 -t fifo) +(! systemd-vpick /var/lib/machines/mymachine.raw.v --suffix=.raw -A arm64 -t sock) + +if [ "$ARCH" != "arm64" ] ; then + test "$(systemd-vpick /var/lib/machines/mytree.v)" = "/var/lib/machines/mytree.v/mytree_33.5/" + test "$(systemd-vpick /var/lib/machines/mytree.v --type=dir)" = "/var/lib/machines/mytree.v/mytree_33.5/" +else + test "$(systemd-vpick /var/lib/machines/mytree.v)" = "/var/lib/machines/mytree.v/mytree_37.0_arm64+2-3/" + test "$(systemd-vpick /var/lib/machines/mytree.v --type=dir)" = "/var/lib/machines/mytree.v/mytree_37.0_arm64+2-3/" +fi + +(! systemd-vpick /var/lib/machines/mytree.v --type=reg) + +mkdir /tmp/dotvroot +mount --bind / /tmp/dotvroot + +mkdir /var/lib/machines/testroot.v +mkdir /var/lib/machines/testroot.v/testroot_32 +ln -s /tmp/dotvroot /var/lib/machines/testroot.v/testroot_33 +mkdir /var/lib/machines/testroot.v/testroot_34 + +ls -l /var/lib/machines/testroot.v + +test "$(systemd-vpick /var/lib/machines/testroot.v)" = /var/lib/machines/testroot.v/testroot_34/ +test "$(systemd-vpick --resolve=yes /var/lib/machines/testroot.v)" = /var/lib/machines/testroot.v/testroot_34/ +(! systemd-run --wait -p RootDirectory=/var/lib/machines/testroot.v /bin/true) + +find /var/lib/machines/testroot.v/testroot_34 +rm -rf /var/lib/machines/testroot.v/testroot_34 +test "$(systemd-vpick /var/lib/machines/testroot.v)" = /var/lib/machines/testroot.v/testroot_33/ +test "$(systemd-vpick --resolve=yes /var/lib/machines/testroot.v)" = /tmp/dotvroot/ +systemd-run --wait -p RootDirectory=/var/lib/machines/testroot.v /bin/true + +rm /var/lib/machines/testroot.v/testroot_33 +test "$(systemd-vpick /var/lib/machines/testroot.v)" = /var/lib/machines/testroot.v/testroot_32/ +test "$(systemd-vpick --resolve=yes /var/lib/machines/testroot.v)" = /var/lib/machines/testroot.v/testroot_32/ +(! systemd-run --wait -p RootDirectory=/var/lib/machines/testroot.v /bin/true) + +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 /bin/true) From 97c493f2140b207ace89e9e028949ceb254fbfc6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 3 Mar 2023 18:55:18 +0100 Subject: [PATCH 12/12] update TODO --- TODO | 42 +++++++++++------------------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/TODO b/TODO index c4e09cbc44..ee6bfc304b 100644 --- a/TODO +++ b/TODO @@ -473,6 +473,17 @@ Features: line, and then generate a mount unit for it using a udev generated symlink based on lo_file_name. +* teach systemd-nspawn the boot assessment logic: hook up vpick's try counters + with success notifications from nspawn payloads. When this is enabled, + automatically support reverting back to older OS versin images if newer ones + fail to boot. + +* implement new "systemd-fsrebind" tool that works like gpt-auto-generator but + looks at a root dir and then applies vpick on various dirs/images to pick a + root tree, a /usr/ tree, a /home/, a /srv/, a /var/ tree and so on. Dirs + could also be btrfs subvols (combine with btrfs auto-snapshort approach for + creating versions like these automatically). + * remove tomoyo support, it's obsolete and unmaintained apparently * In .socket units, add ConnectStream=, ConnectDatagram=, @@ -704,17 +715,6 @@ Features: * automatic boot assessment: add one more default success check that just waits for a bit after boot, and blesses the boot if the system stayed up that long. -* implement concept of "versioned" resources inside a dir, and write a spec for - it. Make all tools in systemd, in particular - RootImage=/RootDirectory=/--image=/--directory= implement this. Idea: - directories ending in ".v/" indicate a directory with versioned resources in - them. Versioned resources inside a .v dir are always named in the pattern - _[+[-]]. - -* add support for using this .v/ logic on the root fs itself: in the initrd, - after mounting the rootfs, look for root-.v/ in the root fs, and then - apply the logic, moving the switch root logic there. - * systemd-repart: add support for generating ISO9660 images * systemd-repart: in addition to the existing "factory reset" mode (which @@ -1170,26 +1170,6 @@ Features: passwords, not just the first. i.e. if there are multiple defined, prefer unlocked over locked and prefer non-empty over empty. -* maybe add a tool inspired by the GPT auto discovery spec that runs in the - initrd and rearranges the rootfs hierarchy via bind mounts, if - enabled. Specifically in some top-level dir /@auto/ it will look for - dirs/symlinks/subvolumes that are named after their purpose, and optionally - encode a version as well as assessment counters, and then mount them into the - file system tree to boot into, similar to how we do that for the gpt auto - logic. Maybe then bind mount the original root into /.superior or something - like that (so that update tools can look there). Further discussion in this - thread: - https://lists.freedesktop.org/archives/systemd-devel/2021-November/047059.html - The GPT dissection logic should automatically enable this tool whenever we - detect a specially marked root fs (i.e introduce a new generic root gpt type - for this, that is arch independent). The also implement this in the image - dissection logic, so that nspawn/RootImage= and so on grok it. Maybe make - generic enough so that it can also work for ostrees arrangements. - -* if a path ending in ".auto.d/" is set for RootDirectory=/RootImage= then do a - strverscmp() of everything inside that dir and use that. i.e. implement very - simple version control. Also use this in systemd-nspawn --image= and so on. - * homed: while a home dir is not activated generate slightly different NSS records for it, that reports the home dir as "/" and the shell as some binary provided by us. Then, when an SSH login happens and SSH permits it our binary