diff --git a/cmd/zfs/zfs_main.c b/cmd/zfs/zfs_main.c index b9c3a5cf3..718ceea50 100644 --- a/cmd/zfs/zfs_main.c +++ b/cmd/zfs/zfs_main.c @@ -288,9 +288,9 @@ get_usage(zfs_help_t idx) case HELP_ROLLBACK: return (gettext("\trollback [-rRf] \n")); case HELP_SEND: - return (gettext("\tsend [-DnPpRvLecr] [-[i|I] snapshot] " + return (gettext("\tsend [-DnPpRvLecwb] [-[i|I] snapshot] " "\n" - "\tsend [-Lecr] [-i snapshot|bookmark] " + "\tsend [-nvPLecw] [-i snapshot|bookmark] " "\n" "\tsend [-nvPe] -t \n")); case HELP_SET: @@ -3944,11 +3944,12 @@ zfs_do_send(int argc, char **argv) {"resume", required_argument, NULL, 't'}, {"compressed", no_argument, NULL, 'c'}, {"raw", no_argument, NULL, 'w'}, + {"backup", no_argument, NULL, 'b'}, {0, 0, 0, 0} }; /* check options */ - while ((c = getopt_long(argc, argv, ":i:I:RDpvnPLet:cw", long_options, + while ((c = getopt_long(argc, argv, ":i:I:RDpvnPLet:cwb", long_options, NULL)) != -1) { switch (c) { case 'i': @@ -3968,6 +3969,9 @@ zfs_do_send(int argc, char **argv) case 'p': flags.props = B_TRUE; break; + case 'b': + flags.backup = B_TRUE; + break; case 'P': flags.parsable = B_TRUE; flags.verbose = B_TRUE; @@ -4048,7 +4052,7 @@ zfs_do_send(int argc, char **argv) if (resume_token != NULL) { if (fromname != NULL || flags.replicate || flags.props || - flags.dedup) { + flags.backup || flags.dedup) { (void) fprintf(stderr, gettext("invalid flags combined with -t\n")); usage(B_FALSE); @@ -4090,7 +4094,8 @@ zfs_do_send(int argc, char **argv) char frombuf[ZFS_MAX_DATASET_NAME_LEN]; if (flags.replicate || flags.doall || flags.props || - flags.dedup || (strchr(argv[0], '@') == NULL && + flags.backup || flags.dedup || + (strchr(argv[0], '@') == NULL && (flags.dryrun || flags.verbose || flags.progress))) { (void) fprintf(stderr, gettext("Error: " "Unsupported flag with filesystem or bookmark.\n")); diff --git a/include/libzfs.h b/include/libzfs.h index 1244bf0e4..71a588325 100644 --- a/include/libzfs.h +++ b/include/libzfs.h @@ -673,6 +673,9 @@ typedef struct sendflags { /* raw encrypted records are permitted */ boolean_t raw; + + /* only send received properties (ie. -b) */ + boolean_t backup; } sendflags_t; typedef boolean_t (snapfilter_cb_t)(zfs_handle_t *, void *); diff --git a/lib/libzfs/libzfs_sendrecv.c b/lib/libzfs/libzfs_sendrecv.c index c850c8ba6..1623a75b4 100644 --- a/lib/libzfs/libzfs_sendrecv.c +++ b/lib/libzfs/libzfs_sendrecv.c @@ -621,6 +621,7 @@ typedef struct send_data { const char *fromsnap; const char *tosnap; boolean_t raw; + boolean_t backup; boolean_t recursive; boolean_t verbose; boolean_t seenfrom; @@ -651,7 +652,8 @@ typedef struct send_data { */ } send_data_t; -static void send_iterate_prop(zfs_handle_t *zhp, nvlist_t *nv); +static void +send_iterate_prop(zfs_handle_t *zhp, boolean_t received_only, nvlist_t *nv); static int send_iterate_snap(zfs_handle_t *zhp, void *arg) @@ -706,7 +708,7 @@ send_iterate_snap(zfs_handle_t *zhp, void *arg) } VERIFY(0 == nvlist_alloc(&nv, NV_UNIQUE_NAME, 0)); - send_iterate_prop(zhp, nv); + send_iterate_prop(zhp, sd->backup, nv); VERIFY(0 == nvlist_add_nvlist(sd->snapprops, snapname, nv)); nvlist_free(nv); @@ -715,11 +717,17 @@ send_iterate_snap(zfs_handle_t *zhp, void *arg) } static void -send_iterate_prop(zfs_handle_t *zhp, nvlist_t *nv) +send_iterate_prop(zfs_handle_t *zhp, boolean_t received_only, nvlist_t *nv) { + nvlist_t *props = NULL; nvpair_t *elem = NULL; - while ((elem = nvlist_next_nvpair(zhp->zfs_props, elem)) != NULL) { + if (received_only) + props = zfs_get_recvd_props(zhp); + else + props = zhp->zfs_props; + + while ((elem = nvlist_next_nvpair(props, elem)) != NULL) { char *propname = nvpair_name(elem); zfs_prop_t prop = zfs_name_to_prop(propname); nvlist_t *propnv; @@ -885,7 +893,7 @@ send_iterate_fs(zfs_handle_t *zhp, void *arg) /* iterate over props */ VERIFY(0 == nvlist_alloc(&nv, NV_UNIQUE_NAME, 0)); - send_iterate_prop(zhp, nv); + send_iterate_prop(zhp, sd->backup, nv); if (zfs_prop_get_int(zhp, ZFS_PROP_ENCRYPTION) != ZIO_CRYPT_OFF) { boolean_t encroot; @@ -951,7 +959,7 @@ out: static int gather_nvlist(libzfs_handle_t *hdl, const char *fsname, const char *fromsnap, const char *tosnap, boolean_t recursive, boolean_t raw, boolean_t verbose, - nvlist_t **nvlp, avl_tree_t **avlp) + boolean_t backup, nvlist_t **nvlp, avl_tree_t **avlp) { zfs_handle_t *zhp; send_data_t sd = { 0 }; @@ -968,6 +976,7 @@ gather_nvlist(libzfs_handle_t *hdl, const char *fsname, const char *fromsnap, sd.recursive = recursive; sd.raw = raw; sd.verbose = verbose; + sd.backup = backup; if ((error = send_iterate_fs(zhp, &sd)) != 0) { nvlist_free(sd.fss); @@ -1881,7 +1890,7 @@ zfs_send(zfs_handle_t *zhp, const char *fromsnap, const char *tosnap, } } - if (flags->replicate || flags->doall || flags->props) { + if (flags->replicate || flags->doall || flags->props || flags->backup) { dmu_replay_record_t drr = { 0 }; char *packbuf = NULL; size_t buflen = 0; @@ -1889,7 +1898,7 @@ zfs_send(zfs_handle_t *zhp, const char *fromsnap, const char *tosnap, ZIO_SET_CHECKSUM(&zc, 0, 0, 0, 0); - if (flags->replicate || flags->props) { + if (flags->replicate || flags->props || flags->backup) { nvlist_t *hdrnv; VERIFY(0 == nvlist_alloc(&hdrnv, NV_UNIQUE_NAME, 0)); @@ -1908,7 +1917,7 @@ zfs_send(zfs_handle_t *zhp, const char *fromsnap, const char *tosnap, err = gather_nvlist(zhp->zfs_hdl, zhp->zfs_name, fromsnap, tosnap, flags->replicate, flags->raw, - flags->verbose, &fss, &fsavl); + flags->verbose, flags->backup, &fss, &fsavl); if (err) goto err_out; VERIFY(0 == nvlist_add_nvlist(hdrnv, "fss", fss)); @@ -2078,7 +2087,7 @@ zfs_send(zfs_handle_t *zhp, const char *fromsnap, const char *tosnap, } if (!flags->dryrun && (flags->replicate || flags->doall || - flags->props)) { + flags->props || flags->backup)) { /* * write final end record. NB: want to do this even if * there was some error, because it might not be totally @@ -2816,7 +2825,7 @@ again: VERIFY(0 == nvlist_alloc(&deleted, NV_UNIQUE_NAME, 0)); if ((error = gather_nvlist(hdl, tofs, fromsnap, NULL, - recursive, B_TRUE, B_FALSE, &local_nv, &local_avl)) != 0) + recursive, B_TRUE, B_FALSE, B_FALSE, &local_nv, &local_avl)) != 0) return (error); /* @@ -4121,7 +4130,7 @@ zfs_receive_one(libzfs_handle_t *hdl, int infd, const char *tosnap, */ *cp = '\0'; if (gather_nvlist(hdl, destsnap, NULL, NULL, B_FALSE, B_TRUE, - B_FALSE, &local_nv, &local_avl) == 0) { + B_FALSE, B_FALSE, &local_nv, &local_avl) == 0) { *cp = '@'; fs = fsavl_find(local_avl, drrb->drr_toguid, NULL); fsavl_destroy(local_avl); diff --git a/man/man8/zfs.8 b/man/man8/zfs.8 index f42851328..ea585b533 100644 --- a/man/man8/zfs.8 +++ b/man/man8/zfs.8 @@ -194,7 +194,7 @@ .Ar snapshot bookmark .Nm .Cm send -.Op Fl DLPRcenpvw +.Op Fl DLPRbcenpvw .Op Oo Fl I Ns | Ns Fl i Oc Ar snapshot .Ar snapshot .Nm @@ -3321,7 +3321,7 @@ feature. .It Xo .Nm .Cm send -.Op Fl DLPRcenpvw +.Op Fl DLPRbcenpvw .Op Oo Fl I Ns | Ns Fl i Oc Ar snapshot .Ar snapshot .Xc @@ -3417,6 +3417,14 @@ See for details on ZFS feature flags and the .Sy embedded_data feature. +.It Fl b, -backup +Sends only received property values whether or not they are overridden by local +settings, but only if the dataset has ever been received. Use this option when +you want +.Nm zfs Cm receive +to restore received properties backed up on the sent dataset and to avoid +sending local settings that may have nothing to do with the source dataset, +but only with how the data is backed up. .It Fl c, -compressed Generate a more compact stream by using compressed WRITE records for blocks which are compressed on disk and in memory diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index da9c791f9..b80788e22 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -223,7 +223,7 @@ tags = ['functional', 'cli_root', 'zfs_rollback'] tests = ['zfs_send_001_pos', 'zfs_send_002_pos', 'zfs_send_003_pos', 'zfs_send_004_neg', 'zfs_send_005_pos', 'zfs_send_006_pos', 'zfs_send_007_pos', 'zfs_send_encrypted', 'zfs_send_raw', - 'zfs_send_sparse'] + 'zfs_send_sparse', 'zfs_send-b'] tags = ['functional', 'cli_root', 'zfs_send'] [tests/functional/cli_root/zfs_set] diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_receive/receive-o-x_props_override.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_receive/receive-o-x_props_override.ksh index e4e69851f..3d5d08db2 100755 --- a/tests/zfs-tests/tests/functional/cli_root/zfs_receive/receive-o-x_props_override.ksh +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_receive/receive-o-x_props_override.ksh @@ -45,105 +45,6 @@ function cleanup log_must zfs destroy -r -f $dest } -# -# Verify property $2 is set from source $4 on dataset $1 and has value $3. -# -# $1 checked dataset -# $2 user property -# $3 property value -# $4 source -# -function check_prop_source -{ - typeset dataset="$1" - typeset prop="$2" - typeset value="$3" - typeset source="$4" - typeset chk_value=$(get_prop "$prop" "$dataset") - typeset chk_source=$(get_source "$prop" "$dataset") - - if [[ "$chk_value" != "$value" || "$chk_source" != "$4" ]] - then - return 1 - else - return 0 - fi -} - -# -# Verify target dataset $1 inherit property $2 from dataset $3. -# -# $1 checked dataset -# $2 property -# $3 inherited dataset -# -function check_prop_inherit -{ - typeset checked_dtst="$1" - typeset prop="$2" - typeset inherited_dtst="$3" - typeset inherited_value=$(get_prop "$prop" "$inherited_dtst") - typeset value=$(get_prop "$prop" "$checked_dtst") - typeset source=$(get_source "$prop" "$checked_dtst") - - if [[ "$value" != "$inherited_value" || \ - "$source" != "inherited from $inherited_dtst" ]] - then - return 1 - else - return 0 - fi -} - -# -# Verify property $2 received value on dataset $1 has value $3 -# -# $1 checked dataset -# $2 property name -# $3 checked value -# -function check_prop_received -{ - typeset dataset="$1" - typeset prop="$2" - typeset value="$3" - - received=$(zfs get -H -o received "$prop" "$dataset") - if (($? != 0)); then - log_fail "Unable to get $prop received value for dataset " \ - "$dataset" - fi - if [[ "$received" == "$value" ]] - then - return 0 - else - return 1 - fi -} - -# -# Verify user property $2 is not set on dataset $1 -# -# $1 checked dataset -# $2 property name -# -function check_prop_missing -{ - typeset dataset="$1" - typeset prop="$2" - - value=$(zfs get -H -o value "$prop" "$dataset") - if (($? != 0)); then - log_fail "Unable to get $prop value for dataset $dataset" - fi - if [[ "-" == "$value" ]] - then - return 0 - else - return 1 - fi -} - log_assert "ZFS receive property override and exclude options work as expected." log_onexit cleanup diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_send/Makefile.am b/tests/zfs-tests/tests/functional/cli_root/zfs_send/Makefile.am index e82df61c7..682569da4 100644 --- a/tests/zfs-tests/tests/functional/cli_root/zfs_send/Makefile.am +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_send/Makefile.am @@ -12,4 +12,5 @@ dist_pkgdata_SCRIPTS = \ zfs_send_007_pos.ksh \ zfs_send_encrypted.ksh \ zfs_send_raw.ksh \ - zfs_send_sparse.ksh + zfs_send_sparse.ksh \ + zfs_send-b.ksh diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_send/zfs_send-b.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_send/zfs_send-b.ksh new file mode 100755 index 000000000..cd879846c --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_send/zfs_send-b.ksh @@ -0,0 +1,103 @@ +#!/bin/ksh -p +# +# This file and its contents are supplied under the terms of the +# Common Development and Distribution License ("CDDL"), version 1.0. +# You may only use this file in accordance with the terms of version +# 1.0 of the CDDL. +# +# A full copy of the text of the CDDL should have accompanied this +# source. A copy of the CDDL is also available via the Internet at +# http://www.illumos.org/license/CDDL. +# + +# +# Copyright 2018, loli10K . All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/cli_root/zfs_set/zfs_set_common.kshlib + +# +# DESCRIPTION: +# 'zfs send -b' should works as expected. +# +# STRATEGY: +# 1. Create a source dataset and set some properties +# 2. Verify command line options interact with '-b' correctly +# 3. Send the dataset and its properties to a new "backup" destination +# 4. Set some properties on the new "backup" dataset +# 5. Restore the "backup" dataset to a new destination +# 6. Verify only original (received) properties are sent from "backup" +# + +verify_runnable "both" + +function cleanup +{ + for ds in "$SENDFS" "$BACKUP" "$RESTORE"; do + datasetexists $ds && log_must zfs destroy -r $ds + done +} + +log_assert "'zfs send -b' should work as expected." +log_onexit cleanup + +SENDFS="$TESTPOOL/sendfs" +BACKUP="$TESTPOOL/backup" +RESTORE="$TESTPOOL/restore" + +# 1. Create a source dataset and set some properties +log_must zfs create $SENDFS +log_must zfs snapshot "$SENDFS@s1" +log_must zfs bookmark "$SENDFS@s1" "$SENDFS#bm" +log_must zfs snapshot "$SENDFS@s2" +log_must zfs set "compression=gzip" $SENDFS +log_must zfs set "org.zfsonlinux:prop=val" $SENDFS +log_must zfs set "org.zfsonlinux:snapprop=val" "$SENDFS@s1" + +# 2. Verify command line options interact with '-b' correctly +typeset opts=("" "p" "Rp" "cew" "nv" "D" "DLPRcenpvw") +for opt in ${opts[@]}; do + log_must eval "zfs send -b$opt $SENDFS@s1 > /dev/null" + log_must eval "zfs send -b$opt -i $SENDFS@s1 $SENDFS@s2 > /dev/null" + log_must eval "zfs send -b$opt -I $SENDFS@s1 $SENDFS@s2 > /dev/null" +done +for opt in ${opts[@]}; do + log_mustnot eval "zfs send -b$opt $SENDFS > /dev/null" + log_mustnot eval "zfs send -b$opt $SENDFS#bm > /dev/null" + log_mustnot eval "zfs send -b$opt -i $SENDFS#bm $SENDFS@s2 > /dev/null" +done + +# Do 3..6 in a loop to verify various combination of "zfs send" options +typeset opts=("" "p" "R" "pR" "cew") +for opt in ${opts[@]}; do + # 3. Send the dataset and its properties to a new "backup" destination + # NOTE: only need to send properties (-p) here + log_must eval "zfs send -p $SENDFS@s1 | zfs recv $BACKUP" + + # 4. Set some properties on the new "backup" dataset + # NOTE: override "received" values and set some new properties as well + log_must zfs set "compression=lz4" $BACKUP + log_must zfs set "exec=off" $BACKUP + log_must zfs set "org.zfsonlinux:prop=newval" $BACKUP + log_must zfs set "org.zfsonlinux:newprop=newval" $BACKUP + log_must zfs set "org.zfsonlinux:snapprop=newval" "$BACKUP@s1" + log_must zfs set "org.zfsonlinux:newsnapprop=newval" "$BACKUP@s1" + + # 5. Restore the "backup" dataset to a new destination + log_must eval "zfs send -b$opt $BACKUP@s1 | zfs recv $RESTORE" + + # 6. Verify only original (received) properties are sent from "backup" + log_must eval "check_prop_source $RESTORE compression gzip received" + log_must eval "check_prop_source $RESTORE org.zfsonlinux:prop val received" + log_must eval "check_prop_source $RESTORE@s1 org.zfsonlinux:snapprop val received" + log_must eval "check_prop_source $RESTORE exec on default" + log_must eval "check_prop_missing $RESTORE org.zfsonlinux:newprop" + log_must eval "check_prop_missing $RESTORE@s1 org.zfsonlinux:newsnapprop" + + # cleanup + log_must zfs destroy -r $BACKUP + log_must zfs destroy -r $RESTORE +done + +log_pass "'zfs send -b' works as expected." diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_set/zfs_set_common.kshlib b/tests/zfs-tests/tests/functional/cli_root/zfs_set/zfs_set_common.kshlib index d4e51dcbd..084a4a0a8 100644 --- a/tests/zfs-tests/tests/functional/cli_root/zfs_set/zfs_set_common.kshlib +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_set/zfs_set_common.kshlib @@ -267,3 +267,110 @@ function get_source echo "$source" } + +# +# Verify property $2 is set from source $4 on dataset $1 and has value $3. +# +# $1 checked dataset +# $2 user property +# $3 property value +# $4 source +# +# Returns: 0 if both expected source and value match, 1 otherwise +# +function check_prop_source +{ + typeset dataset="$1" + typeset prop="$2" + typeset value="$3" + typeset source="$4" + typeset chk_value=$(get_prop "$prop" "$dataset") + typeset chk_source=$(get_source "$prop" "$dataset") + + if [[ "$chk_value" != "$value" || "$chk_source" != "$4" ]] + then + return 1 + else + return 0 + fi +} + +# +# Verify target dataset $1 inherit property $2 from dataset $3. +# +# $1 checked dataset +# $2 property +# $3 inherited dataset +# +# Returns: 0 if property has expected value and is inherited, 1 otherwise +# +function check_prop_inherit +{ + typeset checked_dtst="$1" + typeset prop="$2" + typeset inherited_dtst="$3" + typeset inherited_value=$(get_prop "$prop" "$inherited_dtst") + typeset value=$(get_prop "$prop" "$checked_dtst") + typeset source=$(get_source "$prop" "$checked_dtst") + + if [[ "$value" != "$inherited_value" || \ + "$source" != "inherited from $inherited_dtst" ]] + then + return 1 + else + return 0 + fi +} + +# +# Verify property $2 received value on dataset $1 has value $3 +# +# $1 checked dataset +# $2 property name +# $3 checked value +# +# Returns: 0 if property has expected value and is received, 1 otherwise +# +function check_prop_received +{ + typeset dataset="$1" + typeset prop="$2" + typeset value="$3" + + received=$(zfs get -H -o received "$prop" "$dataset") + if (($? != 0)); then + log_fail "Unable to get $prop received value for dataset " \ + "$dataset" + fi + if [[ "$received" == "$value" ]] + then + return 0 + else + return 1 + fi +} + +# +# Verify user property $2 is not set on dataset $1 +# +# $1 checked dataset +# $2 property name +# +# Returns: 0 if property is missing (not set), 1 otherwise +# +function check_prop_missing +{ + typeset dataset="$1" + typeset prop="$2" + + value=$(zfs get -H -o value "$prop" "$dataset") + if (($? != 0)); then + log_fail "Unable to get $prop value for dataset $dataset" + fi + if [[ "-" == "$value" ]] + then + return 0 + else + return 1 + fi +}