Add zfs create dryrun

Adds the ability to sanity check zfs create arguments and to see the
value of any additional properties that will local to the dataset.  For
example, automation that may need to adjust quota on a parent filesystem
before creating a volume may call `zfs create -nP -V <size> <volume>` to
obtain the value of refreservation.  This adds the following options to
zfs create:

- -n dry-run (no-op)
- -v verbose
- -P parseable (implies verbose)

Reviewed-by: Ryan Moeller <ryan@ixsystems.com>
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: Matt Ahrens <matt@delphix.com>
Reviewed-by: Jerry Jelinek <jerry.jelinek@joyent.com>
Signed-off-by: Mike Gerdts <mike.gerdts@joyent.com>
Closes #8974
This commit is contained in:
Mike Gerdts 2019-07-16 13:19:24 -05:00 committed by Brian Behlendorf
parent 93e28d661e
commit d45d7f08fa
6 changed files with 525 additions and 31 deletions

View File

@ -254,9 +254,9 @@ get_usage(zfs_help_t idx)
return (gettext("\tclone [-p] [-o property=value] ... "
"<snapshot> <filesystem|volume>\n"));
case HELP_CREATE:
return (gettext("\tcreate [-p] [-o property=value] ... "
return (gettext("\tcreate [-Pnpv] [-o property=value] ... "
"<filesystem>\n"
"\tcreate [-ps] [-b blocksize] [-o property=value] ... "
"\tcreate [-Pnpsv] [-b blocksize] [-o property=value] ... "
"-V <size> <volume>\n"));
case HELP_DESTROY:
return (gettext("\tdestroy [-fnpRrv] <filesystem|volume>\n"
@ -867,8 +867,8 @@ usage:
}
/*
* zfs create [-p] [-o prop=value] ... fs
* zfs create [-ps] [-b blocksize] [-o prop=value] ... -V vol size
* zfs create [-Pnpv] [-o prop=value] ... fs
* zfs create [-Pnpsv] [-b blocksize] [-o prop=value] ... -V vol size
*
* Create a new dataset. This command can be used to create filesystems
* and volumes. Snapshot creation is handled by 'zfs snapshot'.
@ -880,16 +880,29 @@ usage:
* SPA_VERSION_REFRESERVATION, we set a refreservation instead.
*
* The '-p' flag creates all the non-existing ancestors of the target first.
*
* The '-n' flag is no-op (dry run) mode. This will perform a user-space sanity
* check of arguments and properties, but does not check for permissions,
* available space, etc.
*
* The '-v' flag is for verbose output.
*
* The '-P' flag is used for parseable output. It implies '-v'.
*/
static int
zfs_do_create(int argc, char **argv)
{
zfs_type_t type = ZFS_TYPE_FILESYSTEM;
zpool_handle_t *zpool_handle = NULL;
nvlist_t *real_props = NULL;
uint64_t volsize = 0;
int c;
boolean_t noreserve = B_FALSE;
boolean_t bflag = B_FALSE;
boolean_t parents = B_FALSE;
boolean_t dryrun = B_FALSE;
boolean_t verbose = B_FALSE;
boolean_t parseable = B_FALSE;
int ret = 1;
nvlist_t *props;
uint64_t intval;
@ -898,7 +911,7 @@ zfs_do_create(int argc, char **argv)
nomem();
/* check options */
while ((c = getopt(argc, argv, ":V:b:so:p")) != -1) {
while ((c = getopt(argc, argv, ":PV:b:nso:pv")) != -1) {
switch (c) {
case 'V':
type = ZFS_TYPE_VOLUME;
@ -914,6 +927,10 @@ zfs_do_create(int argc, char **argv)
nomem();
volsize = intval;
break;
case 'P':
verbose = B_TRUE;
parseable = B_TRUE;
break;
case 'p':
parents = B_TRUE;
break;
@ -931,6 +948,9 @@ zfs_do_create(int argc, char **argv)
intval) != 0)
nomem();
break;
case 'n':
dryrun = B_TRUE;
break;
case 'o':
if (!parseprop(props, optarg))
goto error;
@ -938,6 +958,9 @@ zfs_do_create(int argc, char **argv)
case 's':
noreserve = B_TRUE;
break;
case 'v':
verbose = B_TRUE;
break;
case ':':
(void) fprintf(stderr, gettext("missing size "
"argument\n"));
@ -969,14 +992,9 @@ zfs_do_create(int argc, char **argv)
goto badusage;
}
if (type == ZFS_TYPE_VOLUME && !noreserve) {
zpool_handle_t *zpool_handle;
nvlist_t *real_props = NULL;
uint64_t spa_version;
if (dryrun || (type == ZFS_TYPE_VOLUME && !noreserve)) {
char msg[ZFS_MAX_DATASET_NAME_LEN * 2];
char *p;
zfs_prop_t resv_prop;
char *strval;
char msg[1024];
if ((p = strchr(argv[0], '/')) != NULL)
*p = '\0';
@ -985,6 +1003,22 @@ zfs_do_create(int argc, char **argv)
*p = '/';
if (zpool_handle == NULL)
goto error;
(void) snprintf(msg, sizeof (msg),
dryrun ? gettext("cannot verify '%s'") :
gettext("cannot create '%s'"), argv[0]);
if (props && (real_props = zfs_valid_proplist(g_zfs, type,
props, 0, NULL, zpool_handle, B_TRUE, msg)) == NULL) {
zpool_close(zpool_handle);
goto error;
}
}
if (type == ZFS_TYPE_VOLUME && !noreserve) {
uint64_t spa_version;
zfs_prop_t resv_prop;
char *strval;
spa_version = zpool_get_prop_int(zpool_handle,
ZPOOL_PROP_VERSION, NULL);
if (spa_version >= SPA_VERSION_REFRESERVATION)
@ -992,18 +1026,8 @@ zfs_do_create(int argc, char **argv)
else
resv_prop = ZFS_PROP_RESERVATION;
(void) snprintf(msg, sizeof (msg),
gettext("cannot create '%s'"), argv[0]);
if (props && (real_props = zfs_valid_proplist(g_zfs, type,
props, 0, NULL, zpool_handle, B_TRUE, msg)) == NULL) {
zpool_close(zpool_handle);
goto error;
}
volsize = zvol_volsize_to_reservation(zpool_handle, volsize,
real_props);
nvlist_free(real_props);
zpool_close(zpool_handle);
if (nvlist_lookup_string(props, zfs_prop_to_name(resv_prop),
&strval) != 0) {
@ -1014,6 +1038,10 @@ zfs_do_create(int argc, char **argv)
}
}
}
if (zpool_handle != NULL) {
zpool_close(zpool_handle);
nvlist_free(real_props);
}
if (parents && zfs_name_valid(argv[0], type)) {
/*
@ -1025,7 +1053,49 @@ zfs_do_create(int argc, char **argv)
ret = 0;
goto error;
}
if (zfs_create_ancestors(g_zfs, argv[0]) != 0)
if (verbose) {
(void) printf(parseable ? "create_ancestors\t%s\n" :
dryrun ? "would create ancestors of %s\n" :
"create ancestors of %s\n", argv[0]);
}
if (!dryrun) {
if (zfs_create_ancestors(g_zfs, argv[0]) != 0) {
goto error;
}
}
}
if (verbose) {
nvpair_t *nvp = NULL;
(void) printf(parseable ? "create\t%s\n" :
dryrun ? "would create %s\n" : "create %s\n", argv[0]);
while ((nvp = nvlist_next_nvpair(props, nvp)) != NULL) {
uint64_t uval;
char *sval;
switch (nvpair_type(nvp)) {
case DATA_TYPE_UINT64:
VERIFY0(nvpair_value_uint64(nvp, &uval));
(void) printf(parseable ?
"property\t%s\t%llu\n" : "\t%s=%llu\n",
nvpair_name(nvp), (u_longlong_t)uval);
break;
case DATA_TYPE_STRING:
VERIFY0(nvpair_value_string(nvp, &sval));
(void) printf(parseable ?
"property\t%s\t%s\n" : "\t%s=%s\n",
nvpair_name(nvp), sval);
break;
default:
(void) fprintf(stderr, "property '%s' "
"has illegal type %d\n",
nvpair_name(nvp), nvpair_type(nvp));
abort();
}
}
}
if (dryrun) {
ret = 0;
goto error;
}

View File

@ -28,9 +28,9 @@
.\" Copyright (c) 2014 Integros [integros.com]
.\" Copyright 2019 Richard Laager. All rights reserved.
.\" Copyright 2018 Nexenta Systems, Inc.
.\" Copyright 2018 Joyent, Inc.
.\" Copyright 2019 Joyent, Inc.
.\"
.Dd April 30, 2019
.Dd June 30, 2019
.Dt ZFS 8 SMM
.Os Linux
.Sh NAME
@ -41,12 +41,12 @@
.Fl ?V
.Nm
.Cm create
.Op Fl p
.Op Fl Pnpv
.Oo Fl o Ar property Ns = Ns Ar value Oc Ns ...
.Ar filesystem
.Nm
.Cm create
.Op Fl ps
.Op Fl Pnpsv
.Op Fl b Ar blocksize
.Oo Fl o Ar property Ns = Ns Ar value Oc Ns ...
.Fl V Ar size Ar volume
@ -2556,7 +2556,7 @@ subcommand.
.It Xo
.Nm
.Cm create
.Op Fl p
.Op Fl Pnpv
.Oo Fl o Ar property Ns = Ns Ar value Oc Ns ...
.Ar filesystem
.Xc
@ -2585,6 +2585,48 @@ Any property specified on the command line using the
.Fl o
option is ignored.
If the target filesystem already exists, the operation completes successfully.
.It Fl n
Do a dry-run
.Pq Qq No-op
creation.
No datasets will be created.
This is useful in conjunction with the
.Fl v
or
.Fl P
flags to validate properties that are passed via
.Fl o
options and those implied by other options.
The actual dataset creation can still fail due to insufficient privileges or
available capacity.
.It Fl P
Print machine-parsable verbose information about the created dataset.
Each line of output contains a key and one or two values, all separated by tabs.
The
.Sy create_ancestors
and
.Sy create
keys have
.Em filesystem
as their only value.
The
.Sy create_ancestors
key only appears if the
.Fl p
option is used.
The
.Sy property
key has two values, a property name that property's value.
The
.Sy property
key may appear zero or more times, once for each property that will be set local
to
.Em filesystem
due to the use of the
.Fl o
option.
.It Fl v
Print verbose information about the created dataset.
.El
.It Xo
.Nm
@ -2641,6 +2683,52 @@ See
in the
.Sx Native Properties
section for more information about sparse volumes.
.It Fl n
Do a dry-run
.Pq Qq No-op
creation.
No datasets will be created.
This is useful in conjunction with the
.Fl v
or
.Fl P
flags to validate properties that are passed via
.Fl o
options and those implied by other options.
The actual dataset creation can still fail due to insufficient privileges or
available capacity.
.It Fl P
Print machine-parsable verbose information about the created dataset.
Each line of output contains a key and one or two values, all separated by tabs.
The
.Sy create_ancestors
and
.Sy create
keys have
.Em volume
as their only value.
The
.Sy create_ancestors
key only appears if the
.Fl p
option is used.
The
.Sy property
key has two values, a property name that property's value.
The
.Sy property
key may appear zero or more times, once for each property that will be set local
to
.Em volume
due to the use of the
.Fl b
or
.Fl o
options, as well as
.Sy refreservation
if the volume is not sparse.
.It Fl v
Print verbose information about the created dataset.
.El
.It Xo
.Nm

View File

@ -143,7 +143,7 @@ tests = ['zfs_create_001_pos', 'zfs_create_002_pos', 'zfs_create_003_pos',
'zfs_create_007_pos', 'zfs_create_008_neg', 'zfs_create_009_neg',
'zfs_create_010_neg', 'zfs_create_011_pos', 'zfs_create_012_pos',
'zfs_create_013_pos', 'zfs_create_014_pos', 'zfs_create_encrypted',
'zfs_create_crypt_combos']
'zfs_create_crypt_combos', 'zfs_create_dryrun', 'zfs_create_verbose']
tags = ['functional', 'cli_root', 'zfs_create']
[tests/functional/cli_root/zfs_destroy]

View File

@ -17,7 +17,9 @@ dist_pkgdata_SCRIPTS = \
zfs_create_013_pos.ksh \
zfs_create_014_pos.ksh \
zfs_create_encrypted.ksh \
zfs_create_crypt_combos.ksh
zfs_create_crypt_combos.ksh \
zfs_create_dryrun.ksh \
zfs_create_verbose.ksh
dist_pkgdata_DATA = \
properties.kshlib \

View File

@ -0,0 +1,169 @@
#!/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 2019 Joyent, Inc.
#
. $STF_SUITE/include/libtest.shlib
. $STF_SUITE/tests/functional/cli_root/zfs_create/zfs_create_common.kshlib
#
# DESCRIPTION:
# zfs create -n should perform basic sanity checking but should never create a
# dataset. If -v and/or -P are used, it should verbose about what would be
# created if sanity checks pass.
#
# STRATEGY:
# 1. Attempt to create a file system and a volume using various combinations of
# -n with -v and -P.
#
verify_runnable "both"
#
# Verifies that valid commands with -n and without -[vP]:
# - succeed
# - do not create a dataset
# - do not generate output
#
function dry_create_no_output
{
typeset -a cmd=(zfs create -n "$@")
log_note "$0: ${cmd[@]}"
log_must "${cmd[@]}"
datasetexists "$TESTPOOL/$TESTFS1" &&
log_fail "$TESTPOOL/$TESTFS1 unexpectedly created by '${cmd[@]}'"
typeset out=$("${cmd[@]}" 2>&1)
[[ -z "$out" ]] ||
log_fail "unexpected output '$out' from '${cmd[@]}'"
}
#
# Verifies that commands with invalid properties or invalid property values
# - fail
# - do not create a dataset
# - generate a message on stderr
#
function dry_create_error
{
typeset -a cmd=(zfs create -n "$@")
log_note "$0: ${cmd[@]}"
log_mustnot "${cmd[@]}"
datasetexists "$TESTPOOL/$TESTFS1" &&
log_fail "$TESTPOOL/$TESTFS1 unexpectedly created by '${cmd[@]}'"
typeset out=$("${cmd[@]}" 2>&1 >/dev/null)
[[ -z "$out" ]] &&
log_fail "expected an error message but got none from '${cmd[@]}'"
}
#
# Verifies that dry-run commands with parseable output
# - succeed
# - do not create datasets
# - generate parseable output on stdout
# - output matches expectations
#
function dry_create_parseable
{
typeset -n exp=$1
shift
typeset -a cmd=(zfs create -Pn "$@")
typeset ds=${cmd[${#cmd[@]} - 1]}
typeset out
typeset -a toks
typeset -a props
typeset found_create=false
log_note "$0: ${cmd[@]}"
out=$("${cmd[@]}")
(( $? == 0 )) ||
log_fail "unexpected failure getting stdout from '${cmd[@]}'"
datasetexists "$TESTPOOL/$TESTFS1" &&
log_fail "$TESTPOOL/$TESTFS1 unexpectedly created by '${cmd[@]}'"
echo "$out" | while IFS=$'\t' read -A toks; do
log_note "verifying ${toks[@]}"
case ${toks[0]} in
create)
log_must test "${#toks[@]}" -eq 2
log_must test "${toks[1]}" == "$ds"
found_create="yes, I found create"
;;
property)
log_must test "${#toks[@]}" -eq 3
typeset prop=${toks[1]}
typeset val=${toks[2]}
if [[ -z "${exp[$prop]}" ]]; then
log_fail "unexpectedly got property '$prop'"
fi
# We may not know the exact value a property will take
# on. This is the case for at least refreservation.
if [[ ${exp[$prop]} != "*" ]]; then
log_must test "${exp[$prop]}" == "$val"
fi
unset exp[$prop]
;;
*)
log_fail "Unexpected line ${toks[@]}"
;;
esac
done
log_must test "$found_create" == "yes, I found create"
log_must test "extra props: ${!exp[@]}" == "extra props: "
}
function cleanup
{
if datasetexists "$TESTPOOL/$TESTFS1"; then
log_must zfs destroy -r "$TESTPOOL/$TESTFS1"
fi
}
log_onexit cleanup
log_assert "zfs create -n creates nothing but can describe what would be" \
"created"
# Typical creations should succeed
dry_create_no_output "$TESTPOOL/$TESTFS1"
dry_create_no_output -V 10m "$TESTPOOL/$TESTFS1"
# It shouldn't do a space check right now
dry_create_no_output -V 100t "$TESTPOOL/$TESTFS1"
# It shouldn't create parent datasets either
dry_create_no_output -p "$TESTPOOL/$TESTFS1/$TESTFS2"
dry_create_no_output -pV 10m "$TESTPOOL/$TESTFS1/$TESTFS2"
# Various invalid properties should be recognized and result in an error
dry_create_error -o nosuchprop=42 "$TESTPOOL/$TESTFS1"
dry_create_error -b 1234 -V 10m "$TESTPOOL/$TESTFS1"
# Parseable output should be parseable.
typeset -A expect
expect=([compression]=on)
dry_create_parseable expect -o compression=on "$TESTPOOL/$TESTFS1"
# Sparse volumes should not get a gratuitous refreservation
expect=([volblocksize]=4096 [volsize]=$((1024 * 1024 * 10)))
dry_create_parseable expect -b 4k -V 10m -s "$TESTPOOL/$TESTFS1"
# Non-sparse volumes should have refreservation
expect=(
[volblocksize]=4096
[volsize]=$((1024 * 1024 * 10))
[refreservation]="*"
)
dry_create_parseable expect -b 4k -V 10m "$TESTPOOL/$TESTFS1"
log_pass "zfs create -n creates nothing but can describe what would be" \
"created"

View File

@ -0,0 +1,165 @@
#!/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 2019 Joyent, Inc.
#
. $STF_SUITE/include/libtest.shlib
. $STF_SUITE/tests/functional/cli_root/zfs_create/zfs_create_common.kshlib
#
# DESCRIPTION:
# zfs create -P without -n should be verbose about dataset creation.
#
# STRATEGY:
# 1. Attempt to create a file system and a volume using various properties
# and -P
# 2. Exercise the combination of -p and -P.
#
verify_runnable "both"
#
# Verifies that non dry-run commands with parseable output
# - succeed
# - create datasets
# - generate parseable output on stdout
# - output matches expectations
#
function dry_create_parseable
{
typeset -n exp=$1
shift
typeset -a cmd=(zfs create -P "$@")
typeset ds=${cmd[${#cmd[@]} - 1]}
typeset out
typeset -a toks
typeset -a props
typeset found_create=false
typeset create_ancestors=
typeset opt
# Parse the arguments to see if -p was used.
while getopts :PV:b:ospv opt; do
case $opt in
p) create_ancestors=needed ;;
*) continue ;;
esac
done
log_note "$0: ${cmd[@]}"
out=$("${cmd[@]}")
(( $? == 0 )) ||
log_fail "unexpected failure getting stdout from '${cmd[@]}'"
datasetexists "$TESTPOOL/$TESTFS1" ||
log_fail "$TESTPOOL/$TESTFS1 unexpectedly created by '${cmd[@]}'"
echo "$out" | while IFS=$'\t' read -A toks; do
log_note "verifying ${toks[@]}"
case ${toks[0]} in
create_ancestors)
case "$create_ancestors" in
needed)
log_must test "${toks[1]}" == "$ds"
create_ancestors="found ${toks[1]}"
;;
found*)
log_fail "multiple ancestor creation" \
"$create_ancestors and ${toks[1]}"
;;
"")
log_fail "unexpected create_ancestors"
;;
*)
log_fail "impossible error: fix the test"
;;
esac
;;
create)
log_must test "${#toks[@]}" -eq 2
log_must test "${toks[1]}" == "$ds"
found_create="yes, I found create"
;;
property)
log_must test "${#toks[@]}" -eq 3
typeset prop=${toks[1]}
typeset val=${toks[2]}
if [[ -z "${exp[$prop]}" ]]; then
log_fail "unexpectedly got property '$prop'"
fi
# We may not know the exact value a property will take
# on. This is the case for at least refreservation.
if [[ ${exp[$prop]} != "*" ]]; then
log_must test "${exp[$prop]}" == "$val"
fi
unset exp[$prop]
;;
*)
log_fail "Unexpected line ${toks[@]}"
;;
esac
done
log_must test "$found_create" == "yes, I found create"
log_must test "extra props: ${!exp[@]}" == "extra props: "
case "$create_ancestors" in
"")
log_must_busy zfs destroy "$ds"
;;
"found $ds")
log_must_busy zfs destroy -r "$(echo "$ds" | cut -d/ -f1-2)"
;;
needed)
log_fail "Expected but did not find create_ancestors"
;;
*)
log_fail "Unexpected value for create_ancestors:" \
"$create_ancestors"
;;
esac
}
function cleanup
{
if datasetexists "$TESTPOOL/$TESTFS1"; then
log_must_busy zfs destroy -r "$TESTPOOL/$TESTFS1"
fi
}
log_onexit cleanup
log_assert "zfs create -v creates datasets verbosely"
# Parseable output should be parseable.
typeset -A expect
expect=([compression]=on)
dry_create_parseable expect -o compression=on "$TESTPOOL/$TESTFS1"
# Ancestor creation with -p should emit relevant line
expect=([compression]=on)
dry_create_parseable expect -p -o compression=on "$TESTPOOL/$TESTFS1"
expect=([compression]=on)
dry_create_parseable expect -p -o compression=on "$TESTPOOL/$TESTFS1/$TESTVOL"
# Sparse volumes should not get a gratuitous refreservation
expect=([volblocksize]=4096 [volsize]=$((1024 * 1024 * 10)))
dry_create_parseable expect -b 4k -V 10m -s "$TESTPOOL/$TESTFS1"
# Non-sparse volumes should have refreservation
expect=(
[volblocksize]=4096
[volsize]=$((1024 * 1024 * 10))
[refreservation]="*"
)
dry_create_parseable expect -b 4k -V 10m "$TESTPOOL/$TESTFS1"
log_pass "zfs create -v creates datasets verbosely"