aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlek P <[email protected]>2018-03-19 15:40:58 -0400
committerBrian Behlendorf <[email protected]>2018-03-19 12:40:58 -0700
commit272b5d730f9ddd1db7cd7511da7bba75d6bd2450 (patch)
tree9c7d8a1e05571bc8b157427815e05fd65f1dfe91
parenta76f3d0437e5e974f0f748f8735af3539443b388 (diff)
Add JSON output support to channel programs
The changes piggyback JSON output support on top of channel programs (#6558). This way the JSON output support is targeted to scripting use cases and is easily maintainable since it really only touches one function (zfs_do_channel_program()). This patch ports Joyent's JSON nvlist library from illumos to enable easy JSON printing of channel program output nvlist. To keep the delta small I also took advantage of the fact that printing in zfs_do_channel_program() was almost always done before exiting the program. Reviewed by: Matt Ahrens <[email protected]> Reviewed-by: Tony Hutter <[email protected]> Reviewed-by: Richard Elling <[email protected]> Reviewed-by: Brian Behlendorf <[email protected]> Signed-off-by: Alek Pinchuk <[email protected]> Closes #7281
-rw-r--r--cmd/zfs/zfs_main.c25
-rw-r--r--configure.ac1
-rw-r--r--include/libnvpair.h2
-rw-r--r--lib/libnvpair/Makefile.am1
-rw-r--r--lib/libnvpair/libnvpair_json.c403
-rw-r--r--man/man8/zfs-program.86
-rw-r--r--man/man8/zfs.88
-rw-r--r--tests/runfiles/linux.run4
-rw-r--r--tests/zfs-tests/tests/functional/cli_root/Makefile.am1
-rw-r--r--tests/zfs-tests/tests/functional/cli_root/zfs_program/Makefile.am5
-rwxr-xr-xtests/zfs-tests/tests/functional/cli_root/zfs_program/cleanup.ksh30
-rwxr-xr-xtests/zfs-tests/tests/functional/cli_root/zfs_program/setup.ksh32
-rwxr-xr-xtests/zfs-tests/tests/functional/cli_root/zfs_program/zfs_program_json.ksh132
13 files changed, 639 insertions, 11 deletions
diff --git a/cmd/zfs/zfs_main.c b/cmd/zfs/zfs_main.c
index e241831db..d148516f1 100644
--- a/cmd/zfs/zfs_main.c
+++ b/cmd/zfs/zfs_main.c
@@ -27,6 +27,7 @@
* Copyright (c) 2013 Steven Hartland. All rights reserved.
* Copyright 2016 Igor Kozhukhov <[email protected]>.
* Copyright 2016 Nexenta Systems, Inc.
+ * Copyright (c) 2018 Datto Inc.
*/
#include <assert.h>
@@ -358,7 +359,7 @@ get_usage(zfs_help_t idx)
case HELP_BOOKMARK:
return (gettext("\tbookmark <snapshot> <bookmark>\n"));
case HELP_CHANNEL_PROGRAM:
- return (gettext("\tprogram [-n] [-t <instruction limit>] "
+ return (gettext("\tprogram [-jn] [-t <instruction limit>] "
"[-m <memory limit (b)>] <pool> <program file> "
"[lua args...]\n"));
case HELP_LOAD_KEY:
@@ -7220,11 +7221,11 @@ zfs_do_channel_program(int argc, char **argv)
nvlist_t *outnvl;
uint64_t instrlimit = ZCP_DEFAULT_INSTRLIMIT;
uint64_t memlimit = ZCP_DEFAULT_MEMLIMIT;
- boolean_t sync_flag = B_TRUE;
+ boolean_t sync_flag = B_TRUE, json_output = B_FALSE;
zpool_handle_t *zhp;
/* check options */
- while ((c = getopt(argc, argv, "nt:m:")) != -1) {
+ while ((c = getopt(argc, argv, "nt:m:j")) != -1) {
switch (c) {
case 't':
case 'm': {
@@ -7266,6 +7267,10 @@ zfs_do_channel_program(int argc, char **argv)
sync_flag = B_FALSE;
break;
}
+ case 'j': {
+ json_output = B_TRUE;
+ break;
+ }
case '?':
(void) fprintf(stderr, gettext("invalid option '%c'\n"),
optopt);
@@ -7391,14 +7396,18 @@ zfs_do_channel_program(int argc, char **argv)
gettext("Channel program execution failed:\n%s\n"),
errstring);
if (ret == ETIME && instructions != 0)
- (void) fprintf(stderr, "%llu Lua instructions\n",
+ (void) fprintf(stderr,
+ gettext("%llu Lua instructions\n"),
(u_longlong_t)instructions);
} else {
- (void) printf("Channel program fully executed ");
- if (nvlist_empty(outnvl)) {
- (void) printf("with no return value.\n");
+ if (json_output) {
+ (void) nvlist_print_json(stdout, outnvl);
+ } else if (nvlist_empty(outnvl)) {
+ (void) fprintf(stdout, gettext("Channel program fully "
+ "executed and did not produce output.\n"));
} else {
- (void) printf("with return value:\n");
+ (void) fprintf(stdout, gettext("Channel program fully "
+ "executed and produced output:\n"));
dump_nvlist(outnvl, 4);
}
}
diff --git a/configure.ac b/configure.ac
index 9f1162386..c5585ea40 100644
--- a/configure.ac
+++ b/configure.ac
@@ -205,6 +205,7 @@ AC_CONFIG_FILES([
tests/zfs-tests/tests/functional/cli_root/zfs_load-key/Makefile
tests/zfs-tests/tests/functional/cli_root/zfs/Makefile
tests/zfs-tests/tests/functional/cli_root/zfs_mount/Makefile
+ tests/zfs-tests/tests/functional/cli_root/zfs_program/Makefile
tests/zfs-tests/tests/functional/cli_root/zfs_promote/Makefile
tests/zfs-tests/tests/functional/cli_root/zfs_property/Makefile
tests/zfs-tests/tests/functional/cli_root/zfs_receive/Makefile
diff --git a/include/libnvpair.h b/include/libnvpair.h
index 4c2615d92..5277f9574 100644
--- a/include/libnvpair.h
+++ b/include/libnvpair.h
@@ -20,6 +20,7 @@
*/
/*
* Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, Joyent, Inc. All rights reserved.
*/
#ifndef _LIBNVPAIR_H
@@ -46,6 +47,7 @@ extern int nvpair_value_match_regex(nvpair_t *, int, char *, regex_t *,
char **);
extern void nvlist_print(FILE *, nvlist_t *);
+int nvlist_print_json(FILE *, nvlist_t *);
extern void dump_nvlist(nvlist_t *, int);
/*
diff --git a/lib/libnvpair/Makefile.am b/lib/libnvpair/Makefile.am
index d6ba6f89a..8d6519f30 100644
--- a/lib/libnvpair/Makefile.am
+++ b/lib/libnvpair/Makefile.am
@@ -18,6 +18,7 @@ lib_LTLIBRARIES = libnvpair.la
USER_C = \
libnvpair.c \
+ libnvpair_json.c \
nvpair_alloc_system.c
KERNEL_C = \
diff --git a/lib/libnvpair/libnvpair_json.c b/lib/libnvpair/libnvpair_json.c
new file mode 100644
index 000000000..46fab2e3d
--- /dev/null
+++ b/lib/libnvpair/libnvpair_json.c
@@ -0,0 +1,403 @@
+/*
+ * 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 (c) 2014, Joyent, Inc.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <strings.h>
+#include <wchar.h>
+#include <sys/debug.h>
+
+#include "libnvpair.h"
+
+#define FPRINTF(fp, ...) \
+ do { \
+ if (fprintf(fp, __VA_ARGS__) < 0) \
+ return (-1); \
+ } while (0)
+
+/*
+ * When formatting a string for JSON output we must escape certain characters,
+ * as described in RFC4627. This applies to both member names and
+ * DATA_TYPE_STRING values.
+ *
+ * This function will only operate correctly if the following conditions are
+ * met:
+ *
+ * 1. The input String is encoded in the current locale.
+ *
+ * 2. The current locale includes the Basic Multilingual Plane (plane 0)
+ * as defined in the Unicode standard.
+ *
+ * The output will be entirely 7-bit ASCII (as a subset of UTF-8) with all
+ * representable Unicode characters included in their escaped numeric form.
+ */
+static int
+nvlist_print_json_string(FILE *fp, const char *input)
+{
+ mbstate_t mbr;
+ wchar_t c;
+ size_t sz;
+
+ bzero(&mbr, sizeof (mbr));
+
+ FPRINTF(fp, "\"");
+ while ((sz = mbrtowc(&c, input, MB_CUR_MAX, &mbr)) > 0) {
+ switch (c) {
+ case '"':
+ FPRINTF(fp, "\\\"");
+ break;
+ case '\n':
+ FPRINTF(fp, "\\n");
+ break;
+ case '\r':
+ FPRINTF(fp, "\\r");
+ break;
+ case '\\':
+ FPRINTF(fp, "\\\\");
+ break;
+ case '\f':
+ FPRINTF(fp, "\\f");
+ break;
+ case '\t':
+ FPRINTF(fp, "\\t");
+ break;
+ case '\b':
+ FPRINTF(fp, "\\b");
+ break;
+ default:
+ if ((c >= 0x00 && c <= 0x1f) ||
+ (c > 0x7f && c <= 0xffff)) {
+ /*
+ * Render both Control Characters and Unicode
+ * characters in the Basic Multilingual Plane
+ * as JSON-escaped multibyte characters.
+ */
+ FPRINTF(fp, "\\u%04x", (int)(0xffff & c));
+ } else if (c >= 0x20 && c <= 0x7f) {
+ /*
+ * Render other 7-bit ASCII characters directly
+ * and drop other, unrepresentable characters.
+ */
+ FPRINTF(fp, "%c", (int)(0xff & c));
+ }
+ break;
+ }
+ input += sz;
+ }
+
+ if (sz == (size_t)-1 || sz == (size_t)-2) {
+ /*
+ * We last read an invalid multibyte character sequence,
+ * so return an error.
+ */
+ return (-1);
+ }
+
+ FPRINTF(fp, "\"");
+ return (0);
+}
+
+/*
+ * Dump a JSON-formatted representation of an nvlist to the provided FILE *.
+ * This routine does not output any new-lines or additional whitespace other
+ * than that contained in strings, nor does it call fflush(3C).
+ */
+int
+nvlist_print_json(FILE *fp, nvlist_t *nvl)
+{
+ nvpair_t *curr;
+ boolean_t first = B_TRUE;
+
+ FPRINTF(fp, "{");
+
+ for (curr = nvlist_next_nvpair(nvl, NULL); curr;
+ curr = nvlist_next_nvpair(nvl, curr)) {
+ data_type_t type = nvpair_type(curr);
+
+ if (!first)
+ FPRINTF(fp, ",");
+ else
+ first = B_FALSE;
+
+ if (nvlist_print_json_string(fp, nvpair_name(curr)) == -1)
+ return (-1);
+ FPRINTF(fp, ":");
+
+ switch (type) {
+ case DATA_TYPE_STRING: {
+ char *string = fnvpair_value_string(curr);
+ if (nvlist_print_json_string(fp, string) == -1)
+ return (-1);
+ break;
+ }
+
+ case DATA_TYPE_BOOLEAN: {
+ FPRINTF(fp, "true");
+ break;
+ }
+
+ case DATA_TYPE_BOOLEAN_VALUE: {
+ FPRINTF(fp, "%s", fnvpair_value_boolean_value(curr) ==
+ B_TRUE ? "true" : "false");
+ break;
+ }
+
+ case DATA_TYPE_BYTE: {
+ FPRINTF(fp, "%hhu", fnvpair_value_byte(curr));
+ break;
+ }
+
+ case DATA_TYPE_INT8: {
+ FPRINTF(fp, "%hhd", fnvpair_value_int8(curr));
+ break;
+ }
+
+ case DATA_TYPE_UINT8: {
+ FPRINTF(fp, "%hhu", fnvpair_value_uint8(curr));
+ break;
+ }
+
+ case DATA_TYPE_INT16: {
+ FPRINTF(fp, "%hd", fnvpair_value_int16(curr));
+ break;
+ }
+
+ case DATA_TYPE_UINT16: {
+ FPRINTF(fp, "%hu", fnvpair_value_uint16(curr));
+ break;
+ }
+
+ case DATA_TYPE_INT32: {
+ FPRINTF(fp, "%d", fnvpair_value_int32(curr));
+ break;
+ }
+
+ case DATA_TYPE_UINT32: {
+ FPRINTF(fp, "%u", fnvpair_value_uint32(curr));
+ break;
+ }
+
+ case DATA_TYPE_INT64: {
+ FPRINTF(fp, "%lld",
+ (long long)fnvpair_value_int64(curr));
+ break;
+ }
+
+ case DATA_TYPE_UINT64: {
+ FPRINTF(fp, "%llu",
+ (unsigned long long)fnvpair_value_uint64(curr));
+ break;
+ }
+
+ case DATA_TYPE_HRTIME: {
+ hrtime_t val;
+ VERIFY0(nvpair_value_hrtime(curr, &val));
+ FPRINTF(fp, "%llu", (unsigned long long)val);
+ break;
+ }
+
+ case DATA_TYPE_DOUBLE: {
+ double val;
+ VERIFY0(nvpair_value_double(curr, &val));
+ FPRINTF(fp, "%f", val);
+ break;
+ }
+
+ case DATA_TYPE_NVLIST: {
+ if (nvlist_print_json(fp,
+ fnvpair_value_nvlist(curr)) == -1)
+ return (-1);
+ break;
+ }
+
+ case DATA_TYPE_STRING_ARRAY: {
+ char **val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_string_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ if (nvlist_print_json_string(fp, val[i]) == -1)
+ return (-1);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_NVLIST_ARRAY: {
+ nvlist_t **val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_nvlist_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ if (nvlist_print_json(fp, val[i]) == -1)
+ return (-1);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_BOOLEAN_ARRAY: {
+ boolean_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_boolean_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, val[i] == B_TRUE ?
+ "true" : "false");
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_BYTE_ARRAY: {
+ uchar_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_byte_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%hhu", val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_UINT8_ARRAY: {
+ uint8_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_uint8_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%hhu", val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_INT8_ARRAY: {
+ int8_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_int8_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%hd", val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_UINT16_ARRAY: {
+ uint16_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_uint16_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%hu", val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_INT16_ARRAY: {
+ int16_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_int16_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%hd", val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_UINT32_ARRAY: {
+ uint32_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_uint32_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%u", val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_INT32_ARRAY: {
+ int32_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_int32_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%d", val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_UINT64_ARRAY: {
+ uint64_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_uint64_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%llu",
+ (unsigned long long)val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_INT64_ARRAY: {
+ int64_t *val;
+ uint_t valsz, i;
+ VERIFY0(nvpair_value_int64_array(curr, &val, &valsz));
+ FPRINTF(fp, "[");
+ for (i = 0; i < valsz; i++) {
+ if (i > 0)
+ FPRINTF(fp, ",");
+ FPRINTF(fp, "%lld", (long long)val[i]);
+ }
+ FPRINTF(fp, "]");
+ break;
+ }
+
+ case DATA_TYPE_UNKNOWN:
+ return (-1);
+ }
+ }
+
+ FPRINTF(fp, "}");
+ return (0);
+}
diff --git a/man/man8/zfs-program.8 b/man/man8/zfs-program.8
index 8a478b70f..72a33761b 100644
--- a/man/man8/zfs-program.8
+++ b/man/man8/zfs-program.8
@@ -18,7 +18,7 @@
.Nd executes ZFS channel programs
.Sh SYNOPSIS
.Cm "zfs program"
-.Op Fl n
+.Op Fl jn
.Op Fl t Ar instruction-limit
.Op Fl m Ar memory-limit
.Ar pool
@@ -46,6 +46,10 @@ will be run on
and any attempts to access or modify other pools will cause an error.
.Sh OPTIONS
.Bl -tag -width "-t"
+.It Fl j
+Display channel program output in JSON format. When this flag is specified and
+standard output is empty - channel program encountered an error. The details of
+such an error will be printed to standard error in plain text.
.It Fl n
Executes a read-only channel program, which runs faster.
The program cannot change on-disk state by calling functions from the
diff --git a/man/man8/zfs.8 b/man/man8/zfs.8
index 7d7af1540..bfae494c6 100644
--- a/man/man8/zfs.8
+++ b/man/man8/zfs.8
@@ -300,7 +300,7 @@
.Ar snapshot Ar snapshot Ns | Ns Ar filesystem
.Nm
.Cm program
-.Op Fl n
+.Op Fl jn
.Op Fl t Ar timeout
.Op Fl m Ar memory_limit
.Ar pool script
@@ -4264,7 +4264,7 @@ Display the path's inode change time as the first column of output.
.It Xo
.Nm
.Cm program
-.Op Fl n
+.Op Fl jn
.Op Fl t Ar timeout
.Op Fl m Ar memory_limit
.Ar pool script
@@ -4286,6 +4286,10 @@ For full documentation of the ZFS channel program interface, see the manual
page for
.Xr zfs-program 8 .
.Bl -tag -width ""
+.It Fl j
+Display channel program output in JSON format. When this flag is specified and
+standard output is empty - channel program encountered an error. The details of
+such an error will be printed to standard error in plain text.
.It Fl n
Executes a read-only channel program, which runs faster.
The program cannot change on-disk state by calling functions from
diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run
index f2fdfd753..7c2ca84bf 100644
--- a/tests/runfiles/linux.run
+++ b/tests/runfiles/linux.run
@@ -177,6 +177,10 @@ tests = ['zfs_mount_001_pos', 'zfs_mount_002_pos', 'zfs_mount_003_pos',
'zfs_mount_encrypted', 'zfs_mount_remount']
tags = ['functional', 'cli_root', 'zfs_mount']
+[tests/functional/cli_root/zfs_program]
+tests = ['zfs_program_json']
+tags = ['functional', 'cli_root', 'zfs_program']
+
[tests/functional/cli_root/zfs_promote]
tests = ['zfs_promote_001_pos', 'zfs_promote_002_pos', 'zfs_promote_003_pos',
'zfs_promote_004_pos', 'zfs_promote_005_pos', 'zfs_promote_006_neg',
diff --git a/tests/zfs-tests/tests/functional/cli_root/Makefile.am b/tests/zfs-tests/tests/functional/cli_root/Makefile.am
index bc64e3bba..a7c56b665 100644
--- a/tests/zfs-tests/tests/functional/cli_root/Makefile.am
+++ b/tests/zfs-tests/tests/functional/cli_root/Makefile.am
@@ -16,6 +16,7 @@ SUBDIRS = \
zfs_inherit \
zfs_load-key \
zfs_mount \
+ zfs_program \
zfs_promote \
zfs_property \
zfs_receive \
diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_program/Makefile.am b/tests/zfs-tests/tests/functional/cli_root/zfs_program/Makefile.am
new file mode 100644
index 000000000..d797a636b
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/cli_root/zfs_program/Makefile.am
@@ -0,0 +1,5 @@
+pkgdatadir = $(datadir)/@PACKAGE@/zfs-tests/tests/functional/cli_root/zfs_program
+dist_pkgdata_SCRIPTS = \
+ setup.ksh \
+ cleanup.ksh \
+ zfs_program_json.ksh
diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_program/cleanup.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_program/cleanup.ksh
new file mode 100755
index 000000000..79cd6e9f9
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/cli_root/zfs_program/cleanup.ksh
@@ -0,0 +1,30 @@
+#!/bin/ksh -p
+#
+# 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 http://www.opensolaris.org/os/licensing.
+# 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
+#
+
+#
+# Copyright 2007 Sun Microsystems, Inc. All rights reserved.
+# Use is subject to license terms.
+#
+
+. $STF_SUITE/include/libtest.shlib
+
+default_cleanup
diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_program/setup.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_program/setup.ksh
new file mode 100755
index 000000000..6a9af3bc2
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/cli_root/zfs_program/setup.ksh
@@ -0,0 +1,32 @@
+#!/bin/ksh -p
+#
+# 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 http://www.opensolaris.org/os/licensing.
+# 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
+#
+
+#
+# Copyright 2007 Sun Microsystems, Inc. All rights reserved.
+# Use is subject to license terms.
+#
+
+. $STF_SUITE/include/libtest.shlib
+
+DISK=${DISKS%% *}
+
+default_setup $DISK
diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_program/zfs_program_json.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_program/zfs_program_json.ksh
new file mode 100755
index 000000000..02fd173e1
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/cli_root/zfs_program/zfs_program_json.ksh
@@ -0,0 +1,132 @@
+#!/bin/ksh -p
+#
+# CDDL HEADER START
+#
+# 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 is of the CDDL is also available via the Internet
+# at http://www.illumos.org/license/CDDL.
+#
+# CDDL HEADER END
+#
+
+#
+# Copyright (c) 2018 Datto Inc.
+#
+
+. $STF_SUITE/include/libtest.shlib
+
+#
+# DESCRIPTION:
+#
+# STRATEGY:
+# 1. Compare JSON output formatting for a channel program to template
+# 2. Using bad command line option (-Z) gives correct error output
+#
+
+verify_runnable "both"
+
+function cleanup
+{
+ log_must zfs destroy $TESTDS
+ return 0
+}
+log_onexit cleanup
+
+log_assert "Channel programs output valid JSON"
+
+TESTDS="$TESTPOOL/zcp-json"
+log_must zfs create $TESTDS
+
+TESTZCP="/$TESTDS/zfs_rlist.zcp"
+cat > "$TESTZCP" << EOF
+ succeeded = {}
+ failed = {}
+
+ function list_recursive(root, prop)
+ for child in zfs.list.children(root) do
+ list_recursive(child, prop)
+ end
+ val, src = zfs.get_prop(root, prop)
+ if (val == nil) then
+ failed[root] = val
+ else
+ succeeded[root] = val
+ end
+ end
+
+ args = ...
+
+ argv = args["argv"]
+
+ list_recursive(argv[1], argv[2])
+
+ results = {}
+ results["succeeded"] = succeeded
+ results["failed"] = failed
+ return results
+EOF
+
+# 1. Compare JSON output formatting for a channel program to template
+typeset -a pos_cmds=("recordsize" "type")
+typeset -a pos_cmds_out=(
+"{
+ \"return\": {
+ \"failed\": {},
+ \"succeeded\": {
+ \"$TESTDS\": 131072
+ }
+ }
+}"
+"{
+ \"return\": {
+ \"failed\": {},
+ \"succeeded\": {
+ \"$TESTDS\": \"filesystem\"
+ }
+ }
+}")
+typeset -i cnt=0
+typeset cmd
+for cmd in ${pos_cmds[@]}; do
+ log_must zfs program $TESTPOOL $TESTZCP $TESTDS $cmd 2>&1
+ log_must zfs program $TESTPOOL -j $TESTZCP $TESTDS $cmd 2>&1
+ # json.tool is needed to guarantee consistent ordering of fields
+ # sed is needed to trim trailing space in CentOS 6's json.tool output
+ OUTPUT=$(zfs program $TESTPOOL -j $TESTZCP $TESTDS $cmd 2>&1 | python -m json.tool | sed 's/[[:space:]]*$//')
+ if [ "$OUTPUT" != "${pos_cmds_out[$cnt]}" ]; then
+ log_note "Got :$OUTPUT"
+ log_note "Expected:${pos_cmds_out[$cnt]}"
+ log_fail "Unexpected channel program output";
+ fi
+ cnt=$((cnt + 1))
+done
+
+# 2. Using bad command line option (-Z) gives correct error output
+typeset -a neg_cmds=("-Z")
+typeset -a neg_cmds_out=(
+"invalid option 'Z'
+usage:
+ program [-jn] [-t <instruction limit>] [-m <memory limit (b)>] <pool> <program file> [lua args...]
+
+For the property list, run: zfs set|get
+
+For the delegated permission list, run: zfs allow|unallow")
+cnt=0
+for cmd in ${neg_cmds[@]}; do
+ log_mustnot zfs program $TESTPOOL $TESTZCP $TESTDS $cmd 2>&1
+ log_mustnot zfs program $TESTPOOL -j $TESTZCP $TESTDS $cmd 2>&1
+ OUTPUT=$(zfs program $TESTPOOL -j $TESTZCP $TESTDS $cmd 2>&1)
+ if [ "$OUTPUT" != "${neg_cmds_out[$cnt]}" ]; then
+ log_note "Got :$OUTPUT"
+ log_note "Expected:${neg_cmds_out[$cnt]}"
+ log_fail "Unexpected channel program error output";
+ fi
+ cnt=$((cnt + 1))
+done
+
+log_pass "Channel programs output valid JSON"