diff --git a/cmd/zfs/zfs_main.c b/cmd/zfs/zfs_main.c index 456a6ca3083b..d583f9279bf4 100644 --- a/cmd/zfs/zfs_main.c +++ b/cmd/zfs/zfs_main.c @@ -254,9 +254,9 @@ get_usage(zfs_help_t idx) return (gettext("\tclone [-p] [-o property=value] ... " " \n")); case HELP_CREATE: - return (gettext("\tcreate [-p] [-o property=value] ... " + return (gettext("\tcreate [-Pnpv] [-o property=value] ... " "\n" - "\tcreate [-ps] [-b blocksize] [-o property=value] ... " + "\tcreate [-Pnpsv] [-b blocksize] [-o property=value] ... " "-V \n")); case HELP_DESTROY: return (gettext("\tdestroy [-fnpRrv] \n" @@ -867,8 +867,8 @@ zfs_do_clone(int argc, char **argv) } /* - * 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 @@ zfs_do_clone(int argc, char **argv) * 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,25 +1003,31 @@ zfs_do_create(int argc, char **argv) *p = '/'; if (zpool_handle == NULL) goto error; - spa_version = zpool_get_prop_int(zpool_handle, - ZPOOL_PROP_VERSION, NULL); - if (spa_version >= SPA_VERSION_REFRESERVATION) - resv_prop = ZFS_PROP_REFRESERVATION; - else - resv_prop = ZFS_PROP_RESERVATION; (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) + resv_prop = ZFS_PROP_REFRESERVATION; + else + resv_prop = ZFS_PROP_RESERVATION; 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,8 +1053,50 @@ zfs_do_create(int argc, char **argv) ret = 0; goto error; } - if (zfs_create_ancestors(g_zfs, argv[0]) != 0) - goto error; + 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; } /* pass to libzfs */ diff --git a/man/man8/zfs.8 b/man/man8/zfs.8 index 7058ec503fad..21c9742c8da8 100644 --- a/man/man8/zfs.8 +++ b/man/man8/zfs.8 @@ -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,24 @@ 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-parseable verbose information about the created dataset. +.It Fl v +Print verbose information about the created dataset. .El .It Xo .Nm @@ -2641,6 +2659,24 @@ 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-parseable verbose information about the created dataset. +.It Fl v +Print verbose information about the created dataset. .El .It Xo .Nm diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index d8b2a0b42b31..acdf6cd79708 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -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'] tags = ['functional', 'cli_root', 'zfs_create'] [tests/functional/cli_root/zfs_destroy] diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_create/Makefile.am b/tests/zfs-tests/tests/functional/cli_root/zfs_create/Makefile.am index a36d02161423..802f1736cc87 100644 --- a/tests/zfs-tests/tests/functional/cli_root/zfs_create/Makefile.am +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_create/Makefile.am @@ -17,7 +17,8 @@ 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 dist_pkgdata_DATA = \ properties.kshlib \ diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_create/zfs_create_dryrun.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_create/zfs_create_dryrun.ksh new file mode 100755 index 000000000000..64b8296f46bf --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_create/zfs_create_dryrun.ksh @@ -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"