JSON output support for zfs version and zfs get

This commit adds support for JSON output for zfs version and zfs get
commands. '-j' flag can be used to get output in JSON format.

Information is collected in nvlist objects which is later printed in
JSON format. Existing options that work for zfs get and zfs version
also work with '-j' flag.

man pages for zfs get and zfs version are updated accordingly.

Reviewed-by: Tony Hutter <hutter2@llnl.gov>
Reviewed-by: Ameer Hamza <ahamza@ixsystems.com>
Signed-off-by: Umer Saleem <usaleem@ixsystems.com>
Closes #16217
This commit is contained in:
Umer Saleem 2024-04-05 21:02:30 +05:00 committed by Brian Behlendorf
parent 6c7d41a643
commit aa15b60e58
5 changed files with 412 additions and 15 deletions

View File

@ -134,6 +134,10 @@ static int zfs_do_unzone(int argc, char **argv);
static int zfs_do_help(int argc, char **argv);
enum zfs_options {
ZFS_OPTION_JSON_NUMS_AS_INT = 1024
};
/*
* Enable a reasonable set of defaults for libumem debugging on DEBUG builds.
*/
@ -272,6 +276,8 @@ static zfs_command_t command_table[] = {
#define NCOMMAND (sizeof (command_table) / sizeof (command_table[0]))
#define MAX_CMD_LEN 256
zfs_command_t *current_command;
static const char *
@ -292,7 +298,7 @@ get_usage(zfs_help_t idx)
"<filesystem|volume>@<snap>[%<snap>][,...]\n"
"\tdestroy <filesystem|volume>#<bookmark>\n"));
case HELP_GET:
return (gettext("\tget [-rHp] [-d max] "
return (gettext("\tget [-rHp] [-j [--json-int]] [-d max] "
"[-o \"all\" | field[,...]]\n"
"\t [-t type[,...]] [-s source[,...]]\n"
"\t <\"all\" | property[,...]> "
@ -420,7 +426,7 @@ get_usage(zfs_help_t idx)
"\t <filesystem|volume>\n"
"\tchange-key -i [-l] <filesystem|volume>\n"));
case HELP_VERSION:
return (gettext("\tversion\n"));
return (gettext("\tversion [-j]\n"));
case HELP_REDACT:
return (gettext("\tredact <snapshot> <bookmark> "
"<redaction_snapshot> ...\n"));
@ -1885,7 +1891,109 @@ is_recvd_column(zprop_get_cbdata_t *cbp)
}
/*
* zfs get [-rHp] [-o all | field[,field]...] [-s source[,source]...]
* Generates an nvlist with output version for every command based on params.
* Purpose of this is to add a version of JSON output, considering the schema
* format might be updated for each command in future.
*
* Schema:
*
* "output_version": {
* "command": string,
* "vers_major": integer,
* "vers_minor": integer,
* }
*/
static nvlist_t *
zfs_json_schema(int maj_v, int min_v)
{
nvlist_t *sch = NULL;
nvlist_t *ov = NULL;
char cmd[MAX_CMD_LEN];
snprintf(cmd, MAX_CMD_LEN, "zfs %s", current_command->name);
sch = fnvlist_alloc();
ov = fnvlist_alloc();
fnvlist_add_string(ov, "command", cmd);
fnvlist_add_uint32(ov, "vers_major", maj_v);
fnvlist_add_uint32(ov, "vers_minor", min_v);
fnvlist_add_nvlist(sch, "output_version", ov);
fnvlist_free(ov);
return (sch);
}
static void
fill_dataset_info(nvlist_t *list, zfs_handle_t *zhp, boolean_t as_int)
{
char createtxg[ZFS_MAXPROPLEN];
zfs_type_t type = zfs_get_type(zhp);
nvlist_add_string(list, "name", zfs_get_name(zhp));
switch (type) {
case ZFS_TYPE_FILESYSTEM:
fnvlist_add_string(list, "type", "FILESYSTEM");
break;
case ZFS_TYPE_VOLUME:
fnvlist_add_string(list, "type", "VOLUME");
break;
case ZFS_TYPE_SNAPSHOT:
fnvlist_add_string(list, "type", "SNAPSHOT");
break;
case ZFS_TYPE_POOL:
fnvlist_add_string(list, "type", "POOL");
break;
case ZFS_TYPE_BOOKMARK:
fnvlist_add_string(list, "type", "BOOKMARK");
break;
default:
fnvlist_add_string(list, "type", "UNKNOWN");
break;
}
if (type != ZFS_TYPE_POOL)
fnvlist_add_string(list, "pool", zfs_get_pool_name(zhp));
if (as_int) {
fnvlist_add_uint64(list, "createtxg", zfs_prop_get_int(zhp,
ZFS_PROP_CREATETXG));
} else {
if (zfs_prop_get(zhp, ZFS_PROP_CREATETXG, createtxg,
sizeof (createtxg), NULL, NULL, 0, B_TRUE) == 0)
fnvlist_add_string(list, "createtxg", createtxg);
}
if (type == ZFS_TYPE_SNAPSHOT) {
char *ds, *snap;
ds = snap = strdup(zfs_get_name(zhp));
ds = strsep(&snap, "@");
fnvlist_add_string(list, "dataset", ds);
fnvlist_add_string(list, "snapshot_name", snap);
free(ds);
}
}
static int
zprop_collect_property(const char *name, zprop_get_cbdata_t *cbp,
const char *propname, const char *value, zprop_source_t sourcetype,
const char *source, const char *recvd_value, nvlist_t *nvl)
{
if (cbp->cb_json) {
if ((sourcetype & cbp->cb_sources) == 0)
return (0);
else {
return (zprop_nvlist_one_property(propname, value,
sourcetype, source, recvd_value, nvl,
cbp->cb_json_as_int));
}
} else {
zprop_print_one_property(name, cbp,
propname, value, sourcetype, source, recvd_value);
return (0);
}
}
/*
* zfs get [-rHp] [-j [--json-int]] [-o all | field[,field]...]
* [-s source[,source]...]
* < all | property[,property]... > < fs | snap | vol > ...
*
* -r recurse over any child datasets
@ -1898,6 +2006,8 @@ is_recvd_column(zprop_get_cbdata_t *cbp)
* "local,default,inherited,received,temporary,none". Default is
* all six.
* -p Display values in parsable (literal) format.
* -j Display output in JSON format.
* --json-int Display numbers as integers instead of strings.
*
* Prints properties for the given datasets. The user can control which
* columns to display as well as which property types to allow.
@ -1917,9 +2027,21 @@ get_callback(zfs_handle_t *zhp, void *data)
nvlist_t *user_props = zfs_get_user_props(zhp);
zprop_list_t *pl = cbp->cb_proplist;
nvlist_t *propval;
nvlist_t *item, *d, *props;
item = d = props = NULL;
const char *strval;
const char *sourceval;
boolean_t received = is_recvd_column(cbp);
int err = 0;
if (cbp->cb_json) {
d = fnvlist_lookup_nvlist(cbp->cb_jsobj, "datasets");
if (d == NULL) {
fprintf(stderr, "datasets obj not found.\n");
exit(1);
}
props = fnvlist_alloc();
}
for (; pl != NULL; pl = pl->pl_next) {
char *recvdval = NULL;
@ -1954,9 +2076,9 @@ get_callback(zfs_handle_t *zhp, void *data)
cbp->cb_literal) == 0))
recvdval = rbuf;
zprop_print_one_property(zfs_get_name(zhp), cbp,
err = zprop_collect_property(zfs_get_name(zhp), cbp,
zfs_prop_to_name(pl->pl_prop),
buf, sourcetype, source, recvdval);
buf, sourcetype, source, recvdval, props);
} else if (zfs_prop_userquota(pl->pl_user_prop)) {
sourcetype = ZPROP_SRC_LOCAL;
@ -1966,8 +2088,9 @@ get_callback(zfs_handle_t *zhp, void *data)
(void) strlcpy(buf, "-", sizeof (buf));
}
zprop_print_one_property(zfs_get_name(zhp), cbp,
pl->pl_user_prop, buf, sourcetype, source, NULL);
err = zprop_collect_property(zfs_get_name(zhp), cbp,
pl->pl_user_prop, buf, sourcetype, source, NULL,
props);
} else if (zfs_prop_written(pl->pl_user_prop)) {
sourcetype = ZPROP_SRC_LOCAL;
@ -1977,8 +2100,9 @@ get_callback(zfs_handle_t *zhp, void *data)
(void) strlcpy(buf, "-", sizeof (buf));
}
zprop_print_one_property(zfs_get_name(zhp), cbp,
pl->pl_user_prop, buf, sourcetype, source, NULL);
err = zprop_collect_property(zfs_get_name(zhp), cbp,
pl->pl_user_prop, buf, sourcetype, source, NULL,
props);
} else {
if (nvlist_lookup_nvlist(user_props,
pl->pl_user_prop, &propval) != 0) {
@ -2010,9 +2134,24 @@ get_callback(zfs_handle_t *zhp, void *data)
cbp->cb_literal) == 0))
recvdval = rbuf;
zprop_print_one_property(zfs_get_name(zhp), cbp,
err = zprop_collect_property(zfs_get_name(zhp), cbp,
pl->pl_user_prop, strval, sourcetype,
source, recvdval);
source, recvdval, props);
}
if (err != 0)
return (err);
}
if (cbp->cb_json) {
if (!nvlist_empty(props)) {
item = fnvlist_alloc();
fill_dataset_info(item, zhp, cbp->cb_json_as_int);
fnvlist_add_nvlist(item, "properties", props);
fnvlist_add_nvlist(d, zfs_get_name(zhp), item);
fnvlist_free(props);
fnvlist_free(item);
} else {
fnvlist_free(props);
}
}
@ -2029,6 +2168,7 @@ zfs_do_get(int argc, char **argv)
int ret = 0;
int limit = 0;
zprop_list_t fake_name = { 0 };
nvlist_t *data;
/*
* Set up default columns and sources.
@ -2040,8 +2180,14 @@ zfs_do_get(int argc, char **argv)
cb.cb_columns[3] = GET_COL_SOURCE;
cb.cb_type = ZFS_TYPE_DATASET;
struct option long_options[] = {
{"json-int", no_argument, NULL, ZFS_OPTION_JSON_NUMS_AS_INT},
{0, 0, 0, 0}
};
/* check options */
while ((c = getopt(argc, argv, ":d:o:s:rt:Hp")) != -1) {
while ((c = getopt_long(argc, argv, ":d:o:s:jrt:Hp", long_options,
NULL)) != -1) {
switch (c) {
case 'p':
cb.cb_literal = B_TRUE;
@ -2055,6 +2201,17 @@ zfs_do_get(int argc, char **argv)
case 'H':
cb.cb_scripted = B_TRUE;
break;
case 'j':
cb.cb_json = B_TRUE;
cb.cb_jsobj = zfs_json_schema(0, 1);
data = fnvlist_alloc();
fnvlist_add_nvlist(cb.cb_jsobj, "datasets", data);
fnvlist_free(data);
break;
case ZFS_OPTION_JSON_NUMS_AS_INT:
cb.cb_json_as_int = B_TRUE;
cb.cb_literal = B_TRUE;
break;
case ':':
(void) fprintf(stderr, gettext("missing argument for "
"'%c' option\n"), optopt);
@ -2178,7 +2335,6 @@ found2:;
found3:;
}
break;
case '?':
(void) fprintf(stderr, gettext("invalid option '%c'\n"),
optopt);
@ -2195,6 +2351,12 @@ found3:;
usage(B_FALSE);
}
if (!cb.cb_json && cb.cb_json_as_int) {
(void) fprintf(stderr, gettext("'--json-int' only works with"
" '-j' option\n"));
usage(B_FALSE);
}
fields = argv[0];
/*
@ -2235,6 +2397,11 @@ found3:;
ret = zfs_for_each(argc, argv, flags, types, NULL,
&cb.cb_proplist, limit, get_callback, &cb);
if (ret == 0 && cb.cb_json)
zcmd_print_json(cb.cb_jsobj);
else if (ret != 0 && cb.cb_json)
nvlist_free(cb.cb_jsobj);
if (cb.cb_proplist == &fake_name)
zprop_free_list(fake_name.pl_next);
else
@ -8811,8 +8978,39 @@ found:;
static int
zfs_do_version(int argc, char **argv)
{
(void) argc, (void) argv;
return (zfs_version_print() != 0);
int c;
nvlist_t *jsobj = NULL, *zfs_ver = NULL;
boolean_t json = B_FALSE;
while ((c = getopt(argc, argv, "j")) != -1) {
switch (c) {
case 'j':
json = B_TRUE;
jsobj = zfs_json_schema(0, 1);
break;
case '?':
(void) fprintf(stderr, gettext("invalid option '%c'\n"),
optopt);
usage(B_FALSE);
}
}
argc -= optind;
if (argc != 0) {
(void) fprintf(stderr, "too many arguments\n");
usage(B_FALSE);
}
if (json) {
zfs_ver = zfs_version_nvlist();
if (zfs_ver) {
fnvlist_add_nvlist(jsobj, "zfs_version", zfs_ver);
zcmd_print_json(jsobj);
fnvlist_free(zfs_ver);
return (0);
} else
return (-1);
} else
return (zfs_version_print() != 0);
}
/* Display documentation */

View File

@ -631,6 +631,8 @@ _LIBZFS_H int zprop_get_list(libzfs_handle_t *, char *, zprop_list_t **,
zfs_type_t);
_LIBZFS_H void zprop_free_list(zprop_list_t *);
_LIBZFS_H void zcmd_print_json(nvlist_t *);
#define ZFS_GET_NCOLS 5
typedef enum {
@ -658,9 +660,12 @@ typedef struct zprop_get_cbdata {
boolean_t cb_scripted;
boolean_t cb_literal;
boolean_t cb_first;
boolean_t cb_json;
zprop_list_t *cb_proplist;
zfs_type_t cb_type;
vdev_cbdata_t cb_vdevs;
nvlist_t *cb_jsobj;
boolean_t cb_json_as_int;
} zprop_get_cbdata_t;
#define ZFS_SET_NOMOUNT 1
@ -674,6 +679,9 @@ _LIBZFS_H void zprop_print_one_property(const char *, zprop_get_cbdata_t *,
const char *, const char *, zprop_source_t, const char *,
const char *);
_LIBZFS_H int zprop_nvlist_one_property(const char *, const char *,
zprop_source_t, const char *, const char *, nvlist_t *, boolean_t);
/*
* Iterator functions.
*/
@ -979,6 +987,7 @@ _LIBZFS_H boolean_t libzfs_envvar_is_set(const char *);
_LIBZFS_H const char *zfs_version_userland(void);
_LIBZFS_H char *zfs_version_kernel(void);
_LIBZFS_H int zfs_version_print(void);
_LIBZFS_H nvlist_t *zfs_version_nvlist(void);
/*
* Given a device or file, determine if it is part of a pool.

View File

@ -68,6 +68,7 @@
* as necessary.
*/
#define URI_REGEX "^\\([A-Za-z][A-Za-z0-9+.\\-]*\\):"
#define STR_NUMS "0123456789"
int
libzfs_errno(libzfs_handle_t *hdl)
@ -1267,6 +1268,14 @@ zcmd_read_dst_nvlist(libzfs_handle_t *hdl, zfs_cmd_t *zc, nvlist_t **nvlp)
* ================================================================
*/
void
zcmd_print_json(nvlist_t *nvl)
{
nvlist_print_json(stdout, nvl);
(void) putchar('\n');
nvlist_free(nvl);
}
static void
zprop_print_headers(zprop_get_cbdata_t *cbp, zfs_type_t type)
{
@ -1393,6 +1402,103 @@ zprop_print_headers(zprop_get_cbdata_t *cbp, zfs_type_t type)
(void) printf("\n");
}
/*
* Add property value and source to provided nvlist, according to
* settings in cb structure. Later to be printed in JSON format.
*/
int
zprop_nvlist_one_property(const char *propname,
const char *value, zprop_source_t sourcetype, const char *source,
const char *recvd_value, nvlist_t *nvl, boolean_t as_int)
{
int ret = 0;
nvlist_t *src_nv, *prop;
boolean_t all_numeric = strspn(value, STR_NUMS) == strlen(value);
src_nv = prop = NULL;
if ((nvlist_alloc(&prop, NV_UNIQUE_NAME, 0) != 0) ||
(nvlist_alloc(&src_nv, NV_UNIQUE_NAME, 0) != 0)) {
ret = -1;
goto err;
}
if (as_int && all_numeric) {
uint64_t val;
sscanf(value, "%lld", (u_longlong_t *)&val);
if (nvlist_add_uint64(prop, "value", val) != 0) {
ret = -1;
goto err;
}
} else {
if (nvlist_add_string(prop, "value", value) != 0) {
ret = -1;
goto err;
}
}
switch (sourcetype) {
case ZPROP_SRC_NONE:
if (nvlist_add_string(src_nv, "type", "NONE") != 0 ||
(nvlist_add_string(src_nv, "data", "-") != 0)) {
ret = -1;
goto err;
}
break;
case ZPROP_SRC_DEFAULT:
if (nvlist_add_string(src_nv, "type", "DEFAULT") != 0 ||
(nvlist_add_string(src_nv, "data", "-") != 0)) {
ret = -1;
goto err;
}
break;
case ZPROP_SRC_LOCAL:
if (nvlist_add_string(src_nv, "type", "LOCAL") != 0 ||
(nvlist_add_string(src_nv, "data", "-") != 0)) {
ret = -1;
goto err;
}
break;
case ZPROP_SRC_TEMPORARY:
if (nvlist_add_string(src_nv, "type", "TEMPORARY") != 0 ||
(nvlist_add_string(src_nv, "data", "-") != 0)) {
ret = -1;
goto err;
}
break;
case ZPROP_SRC_INHERITED:
if (nvlist_add_string(src_nv, "type", "INHERITED") != 0 ||
(nvlist_add_string(src_nv, "data", source) != 0)) {
ret = -1;
goto err;
}
break;
case ZPROP_SRC_RECEIVED:
if (nvlist_add_string(src_nv, "type", "RECEIVED") != 0 ||
(nvlist_add_string(src_nv, "data",
(recvd_value == NULL ? "-" : recvd_value)) != 0)) {
ret = -1;
goto err;
}
break;
default:
assert(!"unhandled zprop_source_t");
if (nvlist_add_string(src_nv, "type",
"unhandled zprop_source_t") != 0) {
ret = -1;
goto err;
}
}
if ((nvlist_add_nvlist(prop, "source", src_nv) != 0) ||
(nvlist_add_nvlist(nvl, propname, prop)) != 0) {
ret = -1;
goto err;
}
err:
nvlist_free(src_nv);
nvlist_free(prop);
return (ret);
}
/*
* Display a single line of output, according to the settings in the callback
* structure.
@ -1999,6 +2105,34 @@ zfs_version_print(void)
return (0);
}
/*
* Returns an nvlist with both zfs userland and kernel versions.
* Returns NULL on error.
*/
nvlist_t *
zfs_version_nvlist(void)
{
nvlist_t *nvl;
char kmod_ver[64];
if (nvlist_alloc(&nvl, NV_UNIQUE_NAME, 0) != 0)
return (NULL);
if (nvlist_add_string(nvl, "userland", ZFS_META_ALIAS) != 0)
goto err;
char *kver = zfs_version_kernel();
if (kver == NULL) {
fprintf(stderr, "zfs_version_kernel() failed: %s\n",
zfs_strerror(errno));
goto err;
}
(void) snprintf(kmod_ver, 64, "zfs-kmod-%s", kver);
if (nvlist_add_string(nvl, "kernel", kmod_ver) != 0)
goto err;
return (nvl);
err:
nvlist_free(nvl);
return (NULL);
}
/*
* Return 1 if the user requested ANSI color output, and our terminal supports
* it. Return 0 for no color.

View File

@ -46,6 +46,7 @@
.Cm get
.Op Fl r Ns | Ns Fl d Ar depth
.Op Fl Hp
.Op Fl j Op Ar --json-int
.Oo Fl o Ar field Ns Oo , Ns Ar field Oc Ns Oc
.Oo Fl s Ar source Ns Oo , Ns Ar source Oc Ns Oc
.Oo Fl t Ar type Ns Oo , Ns Ar type Oc Ns Oc
@ -91,6 +92,7 @@ dataset.
.Cm get
.Op Fl r Ns | Ns Fl d Ar depth
.Op Fl Hp
.Op Fl j Op Ar --json-int
.Oo Fl o Ar field Ns Oo , Ns Ar field Oc Ns Oc
.Oo Fl s Ar source Ns Oo , Ns Ar source Oc Ns Oc
.Oo Fl t Ar type Ns Oo , Ns Ar type Oc Ns Oc
@ -128,6 +130,11 @@ The value
can be used to display all properties that apply to the given dataset's type
.Pq Sy filesystem , volume , snapshot , No or Sy bookmark .
.Bl -tag -width "-s source"
.It Fl j Op Ar --json-int
Display the output in JSON format.
Specify
.Sy --json-int
to display numbers in integer format instead of strings for JSON output.
.It Fl H
Display output in a form more easily parsed by scripts.
Any headers are omitted, and fields are explicitly separated by a single tab
@ -283,6 +290,50 @@ The following command gets a single property value:
on
.Ed
.Pp
The following command gets a single property value recursively in JSON format:
.Bd -literal -compact -offset Ds
.No # Nm zfs Cm get Fl j Fl r Sy mountpoint Ar pool/home | Nm jq
{
"output_version": {
"command": "zfs get",
"vers_major": 0,
"vers_minor": 1
},
"datasets": {
"pool/home": {
"name": "pool/home",
"type": "FILESYSTEM",
"pool": "pool",
"createtxg": "10",
"properties": {
"mountpoint": {
"value": "/pool/home",
"source": {
"type": "DEFAULT",
"data": "-"
}
}
}
},
"pool/home/bob": {
"name": "pool/home/bob",
"type": "FILESYSTEM",
"pool": "pool",
"createtxg": "1176",
"properties": {
"mountpoint": {
"value": "/pool/home/bob",
"source": {
"type": "DEFAULT",
"data": "-"
}
}
}
}
}
}
.Ed
.Pp
The following command lists all properties with local settings for
.Ar pool/home/bob :
.Bd -literal -compact -offset Ds

View File

@ -48,6 +48,7 @@
.Fl ?V
.Nm
.Cm version
.Op Fl j
.Nm
.Cm subcommand
.Op Ar arguments
@ -153,10 +154,14 @@ Displays a help message.
.It Xo
.Nm
.Cm version
.Op Fl j
.Xc
Displays the software version of the
.Nm
userland utility and the zfs kernel module.
Use
.Fl j
option to output in JSON format.
.El
.
.Ss Dataset Management