mirror of
https://github.com/openzfs/zfs.git
synced 2026-07-16 17:27:25 +02:00
zfs bookmark: add recursive (-r) bookmark creation
"zfs snapshot -r" and "zfs destroy -r" already operate recursively, but there was no way to create bookmarks for a recursive snapshot in one step, so users had to script a loop over every dataset. Add a -r flag to "zfs bookmark" that, given a snapshot source, creates a bookmark of that snapshot on every descendant dataset that has it. Descendants that lack the snapshot (for example one created after the recursive snapshot) or that are still inconsistent are skipped instead of failing the whole request, mirroring how "zfs snapshot -r" gathers its targets. The pairs are submitted to lzc_bookmark() in a single call and any per-dataset failures are reported individually. The -r flag requires a snapshot source; a bookmark source is rejected. Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov> Signed-off-by: MorganaFuture <103630661+MorganaFuture@users.noreply.github.com> Closes #9460 Closes #18763
This commit is contained in:
+137
-43
@@ -420,7 +420,7 @@ get_usage(zfs_help_t idx)
|
||||
return (gettext("\tdiff [-FHth] <snapshot> "
|
||||
"[snapshot|filesystem]\n"));
|
||||
case HELP_BOOKMARK:
|
||||
return (gettext("\tbookmark <snapshot|bookmark> "
|
||||
return (gettext("\tbookmark [-r] <snapshot|bookmark> "
|
||||
"<newbookmark>\n"));
|
||||
case HELP_CHANNEL_PROGRAM:
|
||||
return (gettext("\tprogram [-jn] [-t <instruction limit>] "
|
||||
@@ -8214,11 +8214,92 @@ out:
|
||||
return (err != 0);
|
||||
}
|
||||
|
||||
typedef struct bookmark_cbdata {
|
||||
nvlist_t *cb_nvl;
|
||||
const char *cb_snapname; /* source snapshot name (after '@') */
|
||||
const char *cb_bookname; /* new bookmark name (after '#') */
|
||||
} bookmark_cbdata_t;
|
||||
|
||||
/*
|
||||
* zfs bookmark <fs@source>|<fs#source> <fs#bookmark>
|
||||
* Recursively gather "<dataset>#bookname" -> "<dataset>@snapname" pairs for
|
||||
* every descendant that actually has the source snapshot, mirroring the way
|
||||
* "zfs snapshot -r" collects its targets. Descendants that lack the snapshot
|
||||
* (or whose name is too long to form the pair) are skipped rather than failing
|
||||
* the whole request. Unlike snapshotting, bookmarking only needs the snapshot
|
||||
* to exist, so an inconsistent (e.g. mid-receive) dataset is not skipped.
|
||||
*/
|
||||
static int
|
||||
zfs_bookmark_cb(zfs_handle_t *zhp, void *arg)
|
||||
{
|
||||
bookmark_cbdata_t *cb = arg;
|
||||
char snap[ZFS_MAX_DATASET_NAME_LEN];
|
||||
char book[ZFS_MAX_DATASET_NAME_LEN];
|
||||
int rv = 0;
|
||||
int n;
|
||||
|
||||
n = snprintf(snap, sizeof (snap), "%s@%s", zfs_get_name(zhp),
|
||||
cb->cb_snapname);
|
||||
if (n >= 0 && (size_t)n < sizeof (snap) && lzc_exists(snap)) {
|
||||
n = snprintf(book, sizeof (book), "%s#%s",
|
||||
zfs_get_name(zhp), cb->cb_bookname);
|
||||
if (n >= 0 && (size_t)n < sizeof (book))
|
||||
fnvlist_add_string(cb->cb_nvl, book, snap);
|
||||
}
|
||||
|
||||
rv = zfs_iter_filesystems_v2(zhp, 0, zfs_bookmark_cb, cb);
|
||||
zfs_close(zhp);
|
||||
return (rv);
|
||||
}
|
||||
|
||||
static void
|
||||
zfs_bookmark_perror(const char *bookname, int err)
|
||||
{
|
||||
const char *err_msg = NULL;
|
||||
char errbuf[1024];
|
||||
|
||||
(void) snprintf(errbuf, sizeof (errbuf),
|
||||
dgettext(TEXT_DOMAIN, "cannot create bookmark '%s'"), bookname);
|
||||
|
||||
switch (err) {
|
||||
case EXDEV:
|
||||
err_msg = "bookmark is in a different pool";
|
||||
break;
|
||||
case ZFS_ERR_BOOKMARK_SOURCE_NOT_ANCESTOR:
|
||||
err_msg = "source is not an ancestor of the "
|
||||
"new bookmark's dataset";
|
||||
break;
|
||||
case EEXIST:
|
||||
err_msg = "bookmark exists";
|
||||
break;
|
||||
case EINVAL:
|
||||
err_msg = "invalid argument";
|
||||
break;
|
||||
case ENOTSUP:
|
||||
err_msg = "bookmark feature not enabled";
|
||||
break;
|
||||
case ENOSPC:
|
||||
err_msg = "out of space";
|
||||
break;
|
||||
case ENOENT:
|
||||
err_msg = "dataset does not exist";
|
||||
break;
|
||||
default:
|
||||
(void) zfs_standard_error(g_zfs, err, errbuf);
|
||||
break;
|
||||
}
|
||||
if (err_msg != NULL) {
|
||||
(void) fprintf(stderr, "%s: %s\n", errbuf,
|
||||
dgettext(TEXT_DOMAIN, err_msg));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* zfs bookmark [-r] <fs@source>|<fs#source> <fs#bookmark>
|
||||
*
|
||||
* Creates a bookmark with the given name from the source snapshot
|
||||
* or creates a copy of an existing source bookmark.
|
||||
* or creates a copy of an existing source bookmark. With -r, a bookmark
|
||||
* is created for the source snapshot of every descendant dataset that has
|
||||
* one.
|
||||
*/
|
||||
static int
|
||||
zfs_do_bookmark(int argc, char **argv)
|
||||
@@ -8227,12 +8308,17 @@ zfs_do_bookmark(int argc, char **argv)
|
||||
char expbuf[ZFS_MAX_DATASET_NAME_LEN];
|
||||
int source_type;
|
||||
nvlist_t *nvl;
|
||||
nvlist_t *errlist = NULL;
|
||||
boolean_t recursive = B_FALSE;
|
||||
int ret = 0;
|
||||
int c;
|
||||
|
||||
/* check options */
|
||||
while ((c = getopt(argc, argv, "")) != -1) {
|
||||
while ((c = getopt(argc, argv, "r")) != -1) {
|
||||
switch (c) {
|
||||
case 'r':
|
||||
recursive = B_TRUE;
|
||||
break;
|
||||
case '?':
|
||||
(void) fprintf(stderr,
|
||||
gettext("invalid option '%c'\n"), optopt);
|
||||
@@ -8310,6 +8396,12 @@ zfs_do_bookmark(int argc, char **argv)
|
||||
default: abort();
|
||||
}
|
||||
|
||||
if (recursive && source_type != ZFS_TYPE_SNAPSHOT) {
|
||||
(void) fprintf(stderr, gettext("recursive bookmarks (-r) can "
|
||||
"only be created from a snapshot source\n"));
|
||||
goto usage;
|
||||
}
|
||||
|
||||
/* test the source exists */
|
||||
zfs_handle_t *zhp;
|
||||
zhp = zfs_open(g_zfs, source, source_type);
|
||||
@@ -8318,51 +8410,53 @@ zfs_do_bookmark(int argc, char **argv)
|
||||
zfs_close(zhp);
|
||||
|
||||
nvl = fnvlist_alloc();
|
||||
fnvlist_add_string(nvl, bookname, source);
|
||||
ret = lzc_bookmark(nvl, NULL);
|
||||
fnvlist_free(nvl);
|
||||
|
||||
if (recursive) {
|
||||
bookmark_cbdata_t cb = { 0 };
|
||||
char dsname[ZFS_MAX_DATASET_NAME_LEN];
|
||||
|
||||
/* recurse from the dataset the source snapshot belongs to */
|
||||
(void) strlcpy(dsname, source, sizeof (dsname));
|
||||
*strchr(dsname, '@') = '\0';
|
||||
|
||||
cb.cb_nvl = nvl;
|
||||
cb.cb_snapname = strchr(source, '@') + 1;
|
||||
cb.cb_bookname = strchr(bookname, '#') + 1;
|
||||
|
||||
zhp = zfs_open(g_zfs, dsname,
|
||||
ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME);
|
||||
if (zhp == NULL) {
|
||||
fnvlist_free(nvl);
|
||||
goto usage;
|
||||
}
|
||||
/* zfs_bookmark_cb() closes zhp */
|
||||
if (zfs_bookmark_cb(zhp, &cb) != 0) {
|
||||
fnvlist_free(nvl);
|
||||
return (1);
|
||||
}
|
||||
} else {
|
||||
fnvlist_add_string(nvl, bookname, source);
|
||||
}
|
||||
|
||||
ret = lzc_bookmark(nvl, &errlist);
|
||||
|
||||
if (ret != 0) {
|
||||
const char *err_msg = NULL;
|
||||
char errbuf[1024];
|
||||
boolean_t reported = B_FALSE;
|
||||
|
||||
(void) snprintf(errbuf, sizeof (errbuf),
|
||||
dgettext(TEXT_DOMAIN,
|
||||
"cannot create bookmark '%s'"), bookname);
|
||||
|
||||
switch (ret) {
|
||||
case EXDEV:
|
||||
err_msg = "bookmark is in a different pool";
|
||||
break;
|
||||
case ZFS_ERR_BOOKMARK_SOURCE_NOT_ANCESTOR:
|
||||
err_msg = "source is not an ancestor of the "
|
||||
"new bookmark's dataset";
|
||||
break;
|
||||
case EEXIST:
|
||||
err_msg = "bookmark exists";
|
||||
break;
|
||||
case EINVAL:
|
||||
err_msg = "invalid argument";
|
||||
break;
|
||||
case ENOTSUP:
|
||||
err_msg = "bookmark feature not enabled";
|
||||
break;
|
||||
case ENOSPC:
|
||||
err_msg = "out of space";
|
||||
break;
|
||||
case ENOENT:
|
||||
err_msg = "dataset does not exist";
|
||||
break;
|
||||
default:
|
||||
(void) zfs_standard_error(g_zfs, ret, errbuf);
|
||||
break;
|
||||
}
|
||||
if (err_msg != NULL) {
|
||||
(void) fprintf(stderr, "%s: %s\n", errbuf,
|
||||
dgettext(TEXT_DOMAIN, err_msg));
|
||||
for (nvpair_t *pair = nvlist_next_nvpair(errlist, NULL);
|
||||
pair != NULL; pair = nvlist_next_nvpair(errlist, pair)) {
|
||||
zfs_bookmark_perror(nvpair_name(pair),
|
||||
fnvpair_value_int32(pair));
|
||||
reported = B_TRUE;
|
||||
}
|
||||
/* fall back to the overall error if none was itemized */
|
||||
if (!reported)
|
||||
zfs_bookmark_perror(bookname, ret);
|
||||
}
|
||||
|
||||
fnvlist_free(nvl);
|
||||
nvlist_free(errlist);
|
||||
|
||||
return (ret != 0);
|
||||
|
||||
usage:
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
.Sh SYNOPSIS
|
||||
.Nm zfs
|
||||
.Cm bookmark
|
||||
.Op Fl r
|
||||
.Ar snapshot Ns | Ns Ar bookmark
|
||||
.Ar newbookmark
|
||||
.
|
||||
@@ -49,6 +50,14 @@ Creates a new bookmark of the given snapshot or bookmark.
|
||||
Bookmarks mark the point in time when the snapshot was created, and can be used
|
||||
as the incremental source for a
|
||||
.Nm zfs Cm send .
|
||||
.Bl -tag -width Ds
|
||||
.It Fl r
|
||||
Recursively create bookmarks for the given source snapshot of all descendent
|
||||
datasets.
|
||||
The source must be a snapshot, and a bookmark is created only for the
|
||||
descendants that have a snapshot of that name; descendants without it are
|
||||
skipped.
|
||||
.El
|
||||
.Pp
|
||||
When creating a bookmark from an existing redaction bookmark, the resulting
|
||||
bookmark is
|
||||
|
||||
@@ -187,7 +187,7 @@ tests = ['zfs_001_neg', 'zfs_002_pos']
|
||||
tags = ['functional', 'cli_root', 'zfs']
|
||||
|
||||
[tests/functional/cli_root/zfs_bookmark]
|
||||
tests = ['zfs_bookmark_cliargs']
|
||||
tests = ['zfs_bookmark_cliargs', 'zfs_bookmark_recursive']
|
||||
tags = ['functional', 'cli_root', 'zfs_bookmark']
|
||||
|
||||
[tests/functional/cli_root/zfs_change-key]
|
||||
|
||||
@@ -704,6 +704,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
|
||||
functional/cli_root/zfs_bookmark/cleanup.ksh \
|
||||
functional/cli_root/zfs_bookmark/setup.ksh \
|
||||
functional/cli_root/zfs_bookmark/zfs_bookmark_cliargs.ksh \
|
||||
functional/cli_root/zfs_bookmark/zfs_bookmark_recursive.ksh \
|
||||
functional/cli_root/zfs_change-key/cleanup.ksh \
|
||||
functional/cli_root/zfs_change-key/setup.ksh \
|
||||
functional/cli_root/zfs_change-key/zfs_change-key_child.ksh \
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
#!/bin/ksh -p
|
||||
# SPDX-License-Identifier: CDDL-1.0
|
||||
#
|
||||
# CDDL HEADER START
|
||||
#
|
||||
# The contents of this file are subject to the terms of the
|
||||
# Common Development and Distribution License (the "License").
|
||||
# You may not use this file except in compliance with the License.
|
||||
#
|
||||
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
|
||||
# or https://opensource.org/licenses/CDDL-1.0.
|
||||
# See the License for the specific language governing permissions
|
||||
# and limitations under the License.
|
||||
#
|
||||
# When distributing Covered Code, include this CDDL HEADER in each
|
||||
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
|
||||
# If applicable, add the following below this CDDL HEADER, with the
|
||||
# fields enclosed by brackets "[]" replaced with your own identifying
|
||||
# information: Portions Copyright [yyyy] [name of copyright owner]
|
||||
#
|
||||
# CDDL HEADER END
|
||||
#
|
||||
|
||||
. $STF_SUITE/include/libtest.shlib
|
||||
|
||||
#
|
||||
# DESCRIPTION:
|
||||
# 'zfs bookmark -r' creates a bookmark for the source snapshot of every
|
||||
# descendant dataset that has it, and skips those that do not.
|
||||
#
|
||||
# STRATEGY:
|
||||
# 1. Recursively snapshot a dataset hierarchy.
|
||||
# 2. Verify 'zfs bookmark -r' creates a bookmark on every dataset in the
|
||||
# subtree, and not on a sibling outside of it.
|
||||
# 3. Verify a descendant created after the snapshot is skipped (not an error)
|
||||
# while the others still get a new bookmark.
|
||||
# 4. Verify 'zfs bookmark -r' rejects a bookmark source.
|
||||
#
|
||||
|
||||
verify_runnable "both"
|
||||
|
||||
typeset TESTSNAP="testsnap"
|
||||
typeset TESTBM="testbm"
|
||||
typeset TESTBM2="testbm2"
|
||||
|
||||
typeset ROOT="$TESTPOOL/$TESTFS"
|
||||
typeset -a SUBTREE=("$ROOT" "$ROOT/child" "$ROOT/recv")
|
||||
typeset OUTSIDE="$TESTPOOL/${TESTFS}_with_suffix"
|
||||
typeset LATE="$ROOT/late"
|
||||
|
||||
function cleanup
|
||||
{
|
||||
datasetexists "$LATE" && destroy_dataset "$LATE" "-r"
|
||||
for ds in "$ROOT" "$OUTSIDE"; do
|
||||
snapexists "$ds@$TESTSNAP" && destroy_dataset "$ds@$TESTSNAP" "-r"
|
||||
done
|
||||
}
|
||||
|
||||
log_onexit cleanup
|
||||
|
||||
log_assert "'zfs bookmark -r' bookmarks the source snapshot of every " \
|
||||
"descendant that has it"
|
||||
|
||||
# 1. Recursive snapshot of the subtree only (not the sibling).
|
||||
log_must zfs snapshot -r "$ROOT@$TESTSNAP"
|
||||
|
||||
# Give the sibling a snapshot of the same name so the scoping check below is
|
||||
# meaningful: it shares the snapshot name and a dataset-name prefix with $ROOT,
|
||||
# so it would be wrongly bookmarked by a name-prefix match rather than true
|
||||
# descendant iteration.
|
||||
log_must zfs snapshot "$OUTSIDE@$TESTSNAP"
|
||||
|
||||
# 2. Recursive bookmark across the subtree.
|
||||
log_must zfs bookmark -r "$ROOT@$TESTSNAP" "$ROOT#$TESTBM"
|
||||
for ds in "${SUBTREE[@]}"; do
|
||||
log_must eval "bkmarkexists $ds#$TESTBM"
|
||||
done
|
||||
# The sibling outside the subtree must not be bookmarked, even though it has
|
||||
# the same snapshot and a matching name prefix.
|
||||
log_mustnot eval "bkmarkexists $OUTSIDE#$TESTBM"
|
||||
|
||||
# 3. A dataset created after the snapshot has no source snapshot and must be
|
||||
# skipped without failing the request; the others get the new bookmark.
|
||||
log_must zfs create "$LATE"
|
||||
log_must zfs bookmark -r "$ROOT@$TESTSNAP" "$ROOT#$TESTBM2"
|
||||
for ds in "${SUBTREE[@]}"; do
|
||||
log_must eval "bkmarkexists $ds#$TESTBM2"
|
||||
done
|
||||
log_mustnot eval "bkmarkexists $LATE#$TESTBM2"
|
||||
|
||||
# 4. A bookmark source is not valid with -r.
|
||||
log_mustnot zfs bookmark -r "$ROOT#$TESTBM" "$ROOT#$TESTBM2"
|
||||
|
||||
log_pass "'zfs bookmark -r' creates recursive bookmarks as expected"
|
||||
Reference in New Issue
Block a user