diff --git a/cmd/zfs/zfs_main.c b/cmd/zfs/zfs_main.c index 920243579466..b790e614aab3 100644 --- a/cmd/zfs/zfs_main.c +++ b/cmd/zfs/zfs_main.c @@ -413,7 +413,7 @@ get_usage(zfs_help_t idx) case HELP_RELEASE: return (gettext("\trelease [-r] ...\n")); case HELP_DIFF: - return (gettext("\tdiff [-FHth] " + return (gettext("\tdiff [-FHthj] " "[snapshot|filesystem]\n")); case HELP_BOOKMARK: return (gettext("\tbookmark " @@ -8064,6 +8064,128 @@ find_command_idx(const char *command, int *idx) return (1); } +typedef struct diff_json_cbdata { + boolean_t timestamped; +} diff_json_cbdata_t; + +/* + * Populate the "changes" sub-object of a diff JSON entry with the specific + * attributes that changed: rename destination path, nlink {from,to}, + * mode {from,to}, and ctime {from,to}. The object is omitted entirely when + * there is nothing to report. + */ +static void +diff_json_add_changes(nvlist_t *nvl, zfs_diff_entry_t *entry, + boolean_t timestamped) +{ + if (!timestamped && entry->de_type != ZFS_DIFF_RENAMED && + entry->de_nlink_from == 0 && entry->de_mode_from == 0) + return; + + nvlist_t *changes = fnvlist_alloc(); + + if (entry->de_type == ZFS_DIFF_RENAMED) + fnvlist_add_string(changes, "path", entry->de_path); + if (entry->de_nlink_from != 0) { + nvlist_t *nlink = fnvlist_alloc(); + fnvlist_add_uint64(nlink, "from", entry->de_nlink_from); + fnvlist_add_uint64(nlink, "to", entry->de_nlink); + fnvlist_add_nvlist(changes, "nlink", nlink); + fnvlist_free(nlink); + } + if (entry->de_mode_from != 0) { + nvlist_t *mode = fnvlist_alloc(); + fnvlist_add_uint64(mode, "from", entry->de_mode_from); + fnvlist_add_uint64(mode, "to", entry->de_mode); + fnvlist_add_nvlist(changes, "mode", mode); + fnvlist_free(mode); + } + if (timestamped) { + nvlist_t *ctime = fnvlist_alloc(); + if (entry->de_ctime_from[0] || entry->de_ctime_from[1]) + fnvlist_add_uint64_array(ctime, "from", + entry->de_ctime_from, 2); + if (entry->de_ctime_to[0] || entry->de_ctime_to[1]) + fnvlist_add_uint64_array(ctime, "to", + entry->de_ctime_to, 2); + fnvlist_add_nvlist(changes, "ctime", ctime); + fnvlist_free(ctime); + } + + fnvlist_add_nvlist(nvl, "changes", changes); + fnvlist_free(changes); +} + +static int +diff_json_callback(zfs_diff_entry_t *entry, void *arg) +{ + diff_json_cbdata_t *cbd = arg; + nvlist_t *nvl; + const char *type_str; + const char *file_type; + mode_t m = (mode_t)entry->de_mode; + + switch (entry->de_type) { + case ZFS_DIFF_ADDED: + type_str = "added"; + break; + case ZFS_DIFF_REMOVED: + type_str = "removed"; + break; + case ZFS_DIFF_MODIFIED: + type_str = "modified"; + break; + case ZFS_DIFF_RENAMED: + type_str = "renamed"; + break; + default: + type_str = "unknown"; + break; + } + + if (S_ISREG(m)) + file_type = "regular"; + else if (S_ISDIR(m)) + file_type = "directory"; + else if (S_ISLNK(m)) + file_type = "symlink"; + else if (S_ISBLK(m)) + file_type = "block"; + else if (S_ISCHR(m)) + file_type = "char"; + else if (S_ISFIFO(m)) + file_type = "fifo"; + else if (S_ISSOCK(m)) + file_type = "socket"; +#ifdef S_IFDOOR + else if ((m & S_IFMT) == S_IFDOOR) + file_type = "door"; +#endif +#ifdef S_IFPORT + else if ((m & S_IFMT) == S_IFPORT) + file_type = "port"; +#endif + else + file_type = "unknown"; + + nvl = fnvlist_alloc(); + fnvlist_add_string(nvl, "change_type", type_str); + fnvlist_add_string(nvl, "mountpoint", entry->de_mntpt); + fnvlist_add_uint64(nvl, "inode", entry->de_inode); + fnvlist_add_uint64(nvl, "gen", entry->de_gen); + fnvlist_add_string(nvl, "file_type", file_type); + fnvlist_add_string(nvl, "path", + entry->de_type == ZFS_DIFF_RENAMED ? + entry->de_path2 : entry->de_path); + + diff_json_add_changes(nvl, entry, cbd->timestamped); + + (void) nvlist_print_json(stdout, nvl); + (void) fputc('\n', stdout); + fnvlist_free(nvl); + return (0); +} + static int zfs_do_diff(int argc, char **argv) { @@ -8075,8 +8197,9 @@ zfs_do_diff(int argc, char **argv) int err = 0; int c; struct sigaction sa; + boolean_t json = B_FALSE; - while ((c = getopt(argc, argv, "FHth")) != -1) { + while ((c = getopt(argc, argv, "FHthj")) != -1) { switch (c) { case 'F': flags |= ZFS_DIFF_CLASSIFY; @@ -8090,6 +8213,9 @@ zfs_do_diff(int argc, char **argv) case 'h': flags |= ZFS_DIFF_NO_MANGLE; break; + case 'j': + json = B_TRUE; + break; default: (void) fprintf(stderr, gettext("invalid option '%c'\n"), optopt); @@ -8146,7 +8272,16 @@ zfs_do_diff(int argc, char **argv) goto out; } - err = zfs_show_diffs(zhp, STDOUT_FILENO, fromsnap, tosnap, flags); + if (json) { + diff_json_cbdata_t cbd = { 0 }; + cbd.timestamped = (flags & ZFS_DIFF_TIMESTAMP) != 0; + + err = zfs_iter_diffs(zhp, fromsnap, tosnap, flags, + diff_json_callback, &cbd); + } else { + err = zfs_show_diffs(zhp, STDOUT_FILENO, fromsnap, tosnap, + flags); + } out: zfs_close(zhp); diff --git a/include/libzfs.h b/include/libzfs.h index ff29488bd2ef..2195c09d3248 100644 --- a/include/libzfs.h +++ b/include/libzfs.h @@ -941,8 +941,71 @@ typedef enum diff_flags { ZFS_DIFF_NO_MANGLE = 1 << 3 } diff_flags_t; +typedef enum zfs_diff_type { + ZFS_DIFF_ADDED = '+', + ZFS_DIFF_REMOVED = '-', + ZFS_DIFF_MODIFIED = 'M', + ZFS_DIFF_RENAMED = 'R', +} zfs_diff_type_t; + +typedef struct zfs_diff_entry { + zfs_diff_type_t de_type; + + /* ctime [seconds, nanoseconds] in from-snapshot; zero for added */ + uint64_t de_ctime_from[2]; + + /* dataset mount point; prepend to de_path / de_path2 for full path */ + const char *de_mntpt; + + /* relative path in tosnap (adds/modifies) or fromsnap (removes) */ + const char *de_path; + + /* relative rename destination; NULL unless ZFS_DIFF_RENAMED */ + const char *de_path2; + + /* ZFS object number, equals st_ino */ + uint64_t de_inode; + + /* txg in which the object was created; lower 32 bits equal st_gen */ + uint64_t de_gen; + + /* full POSIX mode word (type bits + permission bits) */ + uint64_t de_mode; + + /* hard link count */ + uint64_t de_nlink; + + /* ctime [seconds, nanoseconds] in to-snapshot; zero for removed */ + uint64_t de_ctime_to[2]; + + /* + * The following fields are set for ZFS_DIFF_MODIFIED entries only. + */ + + /* nlink in from-snapshot; 0 if nlink did not change */ + uint64_t de_nlink_from; + + /* mode in from-snapshot; 0 if mode did not change */ + uint64_t de_mode_from; +} zfs_diff_entry_t; + +/* + * Callback for zfs_iter_diffs(). Return non-zero to abort iteration. + * + * Some snapshot management operations are blocked for the duration of the + * zfs_iter_diffs() call, so long-running work inside the callback is not + * advisable. + * + * The entry and the strings it points to (de_mntpt, de_path, de_path2) are + * stack-allocated and valid only for the duration of the call. Make a copy + * if any of them need to outlive the callback. + */ +typedef int (*zfs_diff_cb_t)(zfs_diff_entry_t *entry, void *arg); + _LIBZFS_H int zfs_show_diffs(zfs_handle_t *, int, const char *, const char *, int); +_LIBZFS_H int zfs_iter_diffs(zfs_handle_t *, const char *, const char *, + int, zfs_diff_cb_t, void *); /* * Miscellaneous functions. diff --git a/lib/libzfs/libzfs.abi b/lib/libzfs/libzfs.abi index 6a523ba50f87..bab71b415581 100644 --- a/lib/libzfs/libzfs.abi +++ b/lib/libzfs/libzfs.abi @@ -408,6 +408,7 @@ + @@ -5404,6 +5405,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/libzfs/libzfs_diff.c b/lib/libzfs/libzfs_diff.c index 5f50bce531f7..8e6399347806 100644 --- a/lib/libzfs/libzfs_diff.c +++ b/lib/libzfs/libzfs_diff.c @@ -196,10 +196,29 @@ print_cmn(FILE *fp, differ_info_t *di, const char *file) } } -static void +static int print_rename(FILE *fp, differ_info_t *di, const char *old, const char *new, zfs_stat_t *isb) { + if (di->di_cb != NULL) { + zfs_diff_entry_t entry = { 0 }; + + entry.de_type = ZFS_DIFF_RENAMED; + entry.de_ctime_from[0] = di->di_from_stat.zs_ctime[0]; + entry.de_ctime_from[1] = di->di_from_stat.zs_ctime[1]; + entry.de_ctime_to[0] = isb->zs_ctime[0]; + entry.de_ctime_to[1] = isb->zs_ctime[1]; + entry.de_mntpt = di->dsmnt; + entry.de_path = old; + entry.de_path2 = new; + entry.de_inode = di->di_current_obj; + entry.de_mode = isb->zs_mode; + entry.de_nlink = isb->zs_links; + entry.de_gen = isb->zs_gen; + di->di_cb_ret = di->di_cb(&entry, di->di_cb_arg); + return (di->di_cb_ret); + } + if (isatty(fileno(fp))) color_start(ZDIFF_RENAMED_COLOR); if (di->timestamped) @@ -216,12 +235,32 @@ print_rename(FILE *fp, differ_info_t *di, const char *old, const char *new, if (isatty(fileno(fp))) color_end(); + return (0); } -static void +static int print_link_change(FILE *fp, differ_info_t *di, int delta, const char *file, zfs_stat_t *isb) { + if (di->di_cb != NULL) { + zfs_diff_entry_t entry = { 0 }; + + entry.de_type = ZFS_DIFF_MODIFIED; + entry.de_ctime_from[0] = di->di_from_stat.zs_ctime[0]; + entry.de_ctime_from[1] = di->di_from_stat.zs_ctime[1]; + entry.de_ctime_to[0] = isb->zs_ctime[0]; + entry.de_ctime_to[1] = isb->zs_ctime[1]; + entry.de_mntpt = di->dsmnt; + entry.de_path = file; + entry.de_inode = di->di_current_obj; + entry.de_mode = isb->zs_mode; + entry.de_nlink = isb->zs_links; + entry.de_gen = isb->zs_gen; + entry.de_nlink_from = isb->zs_links - delta; + di->di_cb_ret = di->di_cb(&entry, di->di_cb_arg); + return (di->di_cb_ret); + } + if (isatty(fileno(fp))) color_start(ZDIFF_MODIFIED_COLOR); @@ -236,12 +275,43 @@ print_link_change(FILE *fp, differ_info_t *di, int delta, const char *file, (void) fprintf(fp, "\t(%+d)\n", delta); if (isatty(fileno(fp))) color_end(); + return (0); } -static void +static int print_file(FILE *fp, differ_info_t *di, char type, const char *file, zfs_stat_t *isb) { + if (di->di_cb != NULL) { + zfs_diff_entry_t entry = { 0 }; + + entry.de_type = (zfs_diff_type_t)type; + if (type == ZDIFF_REMOVED) { + entry.de_ctime_from[0] = isb->zs_ctime[0]; + entry.de_ctime_from[1] = isb->zs_ctime[1]; + } else if (type == *ZDIFF_MODIFIED) { + entry.de_ctime_from[0] = di->di_from_stat.zs_ctime[0]; + entry.de_ctime_from[1] = di->di_from_stat.zs_ctime[1]; + entry.de_ctime_to[0] = isb->zs_ctime[0]; + entry.de_ctime_to[1] = isb->zs_ctime[1]; + } else { + /* ZDIFF_ADDED */ + entry.de_ctime_to[0] = isb->zs_ctime[0]; + entry.de_ctime_to[1] = isb->zs_ctime[1]; + } + entry.de_mntpt = di->dsmnt; + entry.de_path = file; + entry.de_inode = di->di_current_obj; + entry.de_mode = isb->zs_mode; + entry.de_nlink = isb->zs_links; + entry.de_gen = isb->zs_gen; + if (type == *ZDIFF_MODIFIED && + di->di_from_stat.zs_mode != isb->zs_mode) + entry.de_mode_from = di->di_from_stat.zs_mode; + di->di_cb_ret = di->di_cb(&entry, di->di_cb_arg); + return (di->di_cb_ret); + } + if (isatty(fileno(fp))) color_start(type_to_color(type)); @@ -257,6 +327,7 @@ print_file(FILE *fp, differ_info_t *di, char type, const char *file, if (isatty(fileno(fp))) color_end(); + return (0); } static int @@ -320,20 +391,21 @@ write_inuse_diffs_one(FILE *fp, differ_info_t *di, uint64_t dobj) else change = tsb.zs_links - fsb.zs_links; + di->di_current_obj = dobj; + di->di_from_stat = fsb; + if (fobjerr) { if (change) { - print_link_change(fp, di, change, tobjname, &tsb); - return (0); + return (print_link_change(fp, di, change, + tobjname, &tsb)); } - print_file(fp, di, ZDIFF_ADDED, tobjname, &tsb); - return (0); + return (print_file(fp, di, ZDIFF_ADDED, tobjname, &tsb)); } else if (tobjerr) { if (change) { - print_link_change(fp, di, change, fobjname, &fsb); - return (0); + return (print_link_change(fp, di, change, + fobjname, &fsb)); } - print_file(fp, di, ZDIFF_REMOVED, fobjname, &fsb); - return (0); + return (print_file(fp, di, ZDIFF_REMOVED, fobjname, &fsb)); } if (fmode != tmode && fsb.zs_gen == tsb.zs_gen) @@ -346,19 +418,21 @@ write_inuse_diffs_one(FILE *fp, differ_info_t *di, uint64_t dobj) fsb.zs_ctime[1] == tsb.zs_ctime[1]) return (0); if (change) { - print_link_change(fp, di, change, - change > 0 ? fobjname : tobjname, &tsb); + return (print_link_change(fp, di, change, + change > 0 ? fobjname : tobjname, &tsb)); } else if (strcmp(fobjname, tobjname) == 0) { - print_file(fp, di, *ZDIFF_MODIFIED, fobjname, &tsb); + return (print_file(fp, di, *ZDIFF_MODIFIED, + fobjname, &tsb)); } else { - print_rename(fp, di, fobjname, tobjname, &tsb); + return (print_rename(fp, di, fobjname, tobjname, &tsb)); } - return (0); } else { + int ret; /* file re-created or object re-used */ - print_file(fp, di, ZDIFF_REMOVED, fobjname, &fsb); - print_file(fp, di, ZDIFF_ADDED, tobjname, &tsb); - return (0); + if ((ret = print_file(fp, di, ZDIFF_REMOVED, + fobjname, &fsb)) != 0) + return (ret); + return (print_file(fp, di, ZDIFF_ADDED, tobjname, &tsb)); } } @@ -390,8 +464,8 @@ describe_free(FILE *fp, differ_info_t *di, uint64_t object, char *namebuf, return (0); } - print_file(fp, di, ZDIFF_REMOVED, namebuf, &sb); - return (0); + di->di_current_obj = object; + return (print_file(fp, di, ZDIFF_REMOVED, namebuf, &sb)); } static int @@ -418,8 +492,10 @@ write_free_diffs(FILE *fp, differ_info_t *di, dmu_diff_record_t *dr) if (zc.zc_obj > dr->ddr_last) { break; } - (void) describe_free(fp, di, zc.zc_obj, fobjname, - MAXPATHLEN); + int ferr; + if ((ferr = describe_free(fp, di, zc.zc_obj, + fobjname, MAXPATHLEN)) != 0) + return (ferr); } else if (errno == ESRCH) { break; } else { @@ -441,14 +517,17 @@ differ(void *arg) { differ_info_t *di = arg; dmu_diff_record_t dr; - FILE *ofp; + FILE *ofp = NULL; int err = 0; - if ((ofp = fdopen(di->outputfd, "w")) == NULL) { - di->zerr = errno; - strlcpy(di->errbuf, zfs_strerror(errno), sizeof (di->errbuf)); - (void) close(di->datafd); - return ((void *)-1); + if (di->di_cb == NULL) { + if ((ofp = fdopen(di->outputfd, "w")) == NULL) { + di->zerr = errno; + strlcpy(di->errbuf, zfs_strerror(errno), + sizeof (di->errbuf)); + (void) close(di->datafd); + return ((void *)-1); + } } for (;;) { @@ -486,7 +565,8 @@ differ(void *arg) break; } - (void) fclose(ofp); + if (ofp != NULL) + (void) fclose(ofp); (void) close(di->datafd); if (err) return ((void *)-1); @@ -743,13 +823,19 @@ setup_differ_info(zfs_handle_t *zhp, const char *fromsnap, return (0); } -int -zfs_show_diffs(zfs_handle_t *zhp, int outfd, const char *fromsnap, - const char *tosnap, int flags) +/* + * Core differ execution: create a pipe, spawn the differ thread as the + * consumer, then issue ZFS_IOC_DIFF with the write end of the pipe so the + * kernel streams dmu_diff_record_t ranges into it. The differ thread reads + * those ranges and either writes formatted text to di->outputfd or invokes + * di->di_cb for each changed object, depending on which is set. The two + * ends must run concurrently because the ioctl blocks until the pipe drains. + */ +static int +run_differ(zfs_handle_t *zhp, differ_info_t *di) { zfs_cmd_t zc = {"\0"}; char errbuf[ERRBUFLEN]; - differ_info_t di = { 0 }; pthread_t tid; int pipefd[2]; int iocerr; @@ -757,37 +843,23 @@ zfs_show_diffs(zfs_handle_t *zhp, int outfd, const char *fromsnap, (void) snprintf(errbuf, sizeof (errbuf), dgettext(TEXT_DOMAIN, "zfs diff failed")); - if (setup_differ_info(zhp, fromsnap, tosnap, &di)) { - teardown_differ_info(&di); - return (-1); - } - if (pipe2(pipefd, O_CLOEXEC)) { zfs_error_aux(zhp->zfs_hdl, "%s", zfs_strerror(errno)); - teardown_differ_info(&di); return (zfs_error(zhp->zfs_hdl, EZFS_PIPEFAILED, errbuf)); } - di.scripted = (flags & ZFS_DIFF_PARSEABLE); - di.classify = (flags & ZFS_DIFF_CLASSIFY); - di.timestamped = (flags & ZFS_DIFF_TIMESTAMP); - di.no_mangle = (flags & ZFS_DIFF_NO_MANGLE); - - di.outputfd = outfd; - di.datafd = pipefd[0]; + di->datafd = pipefd[0]; - if (pthread_create(&tid, NULL, differ, &di)) { + if (pthread_create(&tid, NULL, differ, di)) { zfs_error_aux(zhp->zfs_hdl, "%s", zfs_strerror(errno)); (void) close(pipefd[0]); (void) close(pipefd[1]); - teardown_differ_info(&di); return (zfs_error(zhp->zfs_hdl, EZFS_THREADCREATEFAILED, errbuf)); } - /* do the ioctl() */ - (void) strlcpy(zc.zc_value, di.fromsnap, strlen(di.fromsnap) + 1); - (void) strlcpy(zc.zc_name, di.tosnap, strlen(di.tosnap) + 1); + (void) strlcpy(zc.zc_value, di->fromsnap, strlen(di->fromsnap) + 1); + (void) strlcpy(zc.zc_name, di->tosnap, strlen(di->tosnap) + 1); zc.zc_cookie = pipefd[1]; iocerr = zfs_ioctl(zhp->zfs_hdl, ZFS_IOC_DIFF, &zc); @@ -802,17 +874,16 @@ zfs_show_diffs(zfs_handle_t *zhp, int outfd, const char *fromsnap, } else if (errno == EXDEV) { zfs_error_aux(zhp->zfs_hdl, dgettext(TEXT_DOMAIN, "\n Not an earlier snapshot from the same fs")); - } else if (errno != EPIPE || di.zerr == 0) { + } else if (errno != EPIPE || di->zerr == 0) { zfs_error_aux(zhp->zfs_hdl, "%s", zfs_strerror(errno)); } (void) close(pipefd[1]); (void) pthread_cancel(tid); (void) pthread_join(tid, NULL); - teardown_differ_info(&di); - if (di.zerr != 0 && di.zerr != EPIPE) { + if (di->zerr != 0 && di->zerr != EPIPE) { zfs_error_aux(zhp->zfs_hdl, "%s", - zfs_strerror(di.zerr)); - return (zfs_error(zhp->zfs_hdl, EZFS_DIFF, di.errbuf)); + zfs_strerror(di->zerr)); + return (zfs_error(zhp->zfs_hdl, EZFS_DIFF, di->errbuf)); } else { return (zfs_error(zhp->zfs_hdl, EZFS_DIFFDATA, errbuf)); } @@ -821,10 +892,66 @@ zfs_show_diffs(zfs_handle_t *zhp, int outfd, const char *fromsnap, (void) close(pipefd[1]); (void) pthread_join(tid, NULL); - if (di.zerr != 0) { - zfs_error_aux(zhp->zfs_hdl, "%s", zfs_strerror(di.zerr)); - return (zfs_error(zhp->zfs_hdl, EZFS_DIFF, di.errbuf)); + if (di->di_cb_ret != 0) + return (di->di_cb_ret); + if (di->zerr != 0) { + zfs_error_aux(zhp->zfs_hdl, "%s", zfs_strerror(di->zerr)); + return (zfs_error(zhp->zfs_hdl, EZFS_DIFF, di->errbuf)); } - teardown_differ_info(&di); return (0); } + +/* + * Display diff between two snapshots as formatted text written to outfd. + */ +int +zfs_show_diffs(zfs_handle_t *zhp, int outfd, const char *fromsnap, + const char *tosnap, int flags) +{ + differ_info_t di = { 0 }; + int err; + + if (setup_differ_info(zhp, fromsnap, tosnap, &di)) { + teardown_differ_info(&di); + return (-1); + } + + di.scripted = (flags & ZFS_DIFF_PARSEABLE); + di.classify = (flags & ZFS_DIFF_CLASSIFY); + di.timestamped = (flags & ZFS_DIFF_TIMESTAMP); + di.no_mangle = (flags & ZFS_DIFF_NO_MANGLE); + di.outputfd = outfd; + + err = run_differ(zhp, &di); + teardown_differ_info(&di); + return (err); +} + +/* + * Iterate over diff entries between two snapshots, invoking cb for each + * changed object with a structured zfs_diff_entry_t. Returns non-zero if + * cb aborts early or if the diff operation fails. + */ +int +zfs_iter_diffs(zfs_handle_t *zhp, const char *fromsnap, const char *tosnap, + int flags, zfs_diff_cb_t cb, void *cbarg) +{ + differ_info_t di = { 0 }; + int err; + + if (setup_differ_info(zhp, fromsnap, tosnap, &di)) { + teardown_differ_info(&di); + return (-1); + } + + di.classify = (flags & ZFS_DIFF_CLASSIFY); + di.timestamped = (flags & ZFS_DIFF_TIMESTAMP); + di.no_mangle = (flags & ZFS_DIFF_NO_MANGLE); + di.outputfd = -1; + di.di_cb = cb; + di.di_cb_arg = cbarg; + + err = run_differ(zhp, &di); + teardown_differ_info(&di); + return (err); +} diff --git a/lib/libzfs/libzfs_impl.h b/lib/libzfs/libzfs_impl.h index b4dce167f785..6c9cf115d0e7 100644 --- a/lib/libzfs/libzfs_impl.h +++ b/lib/libzfs/libzfs_impl.h @@ -241,6 +241,11 @@ typedef struct differ_info { int cleanupfd; int outputfd; int datafd; + zfs_diff_cb_t di_cb; /* callback; NULL = use fp */ + void *di_cb_arg; /* opaque arg passed to di_cb */ + int di_cb_ret; /* last di_cb return; non-zero aborts */ + uint64_t di_current_obj; /* object number for current entry */ + zfs_stat_t di_from_stat; /* from-snapshot stats for entry */ } differ_info_t; extern int do_mount(zfs_handle_t *zhp, const char *mntpt, const char *opts, diff --git a/man/man8/zfs-diff.8 b/man/man8/zfs-diff.8 index 5b94ea524666..283b1d981e5a 100644 --- a/man/man8/zfs-diff.8 +++ b/man/man8/zfs-diff.8 @@ -40,7 +40,7 @@ .Sh SYNOPSIS .Nm zfs .Cm diff -.Op Fl FHth +.Op Fl FHthj .Ar snapshot Ar snapshot Ns | Ns Ar filesystem . .Sh DESCRIPTION @@ -97,6 +97,99 @@ Display the path's inode change time as the first column of output. Do not .Sy \e0 Ns Ar ooo Ns -escape non-ASCII paths. +.It Fl j +Output one JSON object per line +.Pq NDJSON +instead of the default text format. +Each object contains the following fields: +.Bl -tag -compact -offset 2n -width "change_type" +.It Sy change_type +One of +.Sy added , removed , modified , renamed . +.It Sy inode +ZFS object number, equal to +.Sy st_ino . +.It Sy gen +Transaction group (txg) in which the object was created. +The lower 32 bits equal +.Sy st_gen . +The combination of +.Sy inode +and +.Sy gen +uniquely identifies a file across its lifetime, even if the inode number +is later reused by a different object. +.It Sy file_type +One of +.Sy regular , directory , symlink , block , char , fifo , socket , door , port , unknown . +.It Sy mountpoint +Mount point of the dataset. +Prepend to +.Sy path +or +.Sy changes.path +to obtain the full filesystem path. +.It Sy path +Dataset-relative path of the object. +For renamed entries this is the new path after the rename. +.It Sy changes +Object describing what changed. +Present only for +.Sy renamed +and +.Sy modified +entries where a specific change is known. +Contains one or more of: +.Bl -tag -compact -offset 2n -width "nlink" +.It Sy path +Dataset-relative original path before rename. +Present only for +.Sy renamed +entries. +.It Sy nlink +Object with +.Sy from +and +.Sy to +fields containing the old and new hard link counts. +Present only for +.Sy modified +entries where the link count changed. +.It Sy mode +Object with +.Sy from +and +.Sy to +fields containing the old and new POSIX mode words. +Present only for +.Sy modified +entries where the mode changed. +.It Sy ctime +Object with +.Sy from +and +.Sy to +fields, each an array of +.Sy [seconds, nanoseconds] . +.Sy from +is absent for +.Sy added +entries; +.Sy to +is absent for +.Sy removed +entries. +Present only when +.Fl t +is also specified. +.El +The absence of +.Sy changes , +or the absence of a particular field within it, does not mean the object +is otherwise unchanged. +Data content, file size, timestamps, and extended attributes may have +changed without being reflected here. +.El .El . .Sh EXAMPLES @@ -117,6 +210,19 @@ R F /tank/test/oldname -> /tank/test/newname + F /tank/test/created M F /tank/test/modified .Ed +.Ss Example 2 : No Showing the differences as JSON +The following example shows the same changes in JSON format using the +.Fl j +option. +One object is emitted per line. +.Bd -literal -compact -offset Ds +.No # Nm zfs Cm diff Fl j Ar tank/test@before tank/test +{"change_type":"modified","mountpoint":"/tank/test","inode":2,"gen":6215,"file_type":"directory","path":"/d1","changes":{"mode":{"from":16877,"to":16832}}} +{"change_type":"modified","mountpoint":"/tank/test","inode":34,"gen":6213,"file_type":"directory","path":"/"} +{"change_type":"modified","mountpoint":"/tank/test","inode":128,"gen":6216,"file_type":"regular","path":"/d1/f1"} +{"change_type":"renamed","mountpoint":"/tank/test","inode":129,"gen":6218,"file_type":"regular","path":"/f3","changes":{"path":"/f2"}} +{"change_type":"added","mountpoint":"/tank/test","inode":130,"gen":6254,"file_type":"regular","path":"/d1/f5"} +.Ed . .Sh SEE ALSO .Xr zfs-snapshot 8 diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_diff/zfs_diff_changes.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_diff/zfs_diff_changes.ksh index 03ce4f456dff..5db6dd99837f 100755 --- a/tests/zfs-tests/tests/functional/cli_root/zfs_diff/zfs_diff_changes.ksh +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_diff/zfs_diff_changes.ksh @@ -56,6 +56,60 @@ function verify_object_change # fi } +# +# JSON change type strings corresponding to text symbols +# +typeset -A CHANGE_JSON +CHANGE_JSON["-"]="removed" +CHANGE_JSON["+"]="added" +CHANGE_JSON["M"]="modified" +CHANGE_JSON["R"]="renamed" + +# +# Verify object $path has $change type in JSON output and that each +# JSON line contains the required fields (change_type, inode, gen, +# file_type, path) +# +function verify_object_change_json # +{ + path="$1" + change="$2" + expected="${CHANGE_JSON[$change]}" + relpath="${path#$MNTPOINT}" + + log_must eval "zfs diff -j $TESTSNAP1 $TESTSNAP2 > $FILEDIFF" + + # Every line must be valid JSON with required fields + while IFS= read -r line; do + echo "$line" | log_must jq -e ' + has("change_type") and + has("mountpoint") and + has("inode") and + has("gen") and + has("file_type") and + has("path")' > /dev/null + done < "$FILEDIFF" + + # Renamed entries must have changes.path + if [[ "$change" == "R" ]]; then + diffchg="$(jq -r --arg p "$relpath" \ + 'select(.path == $p) | .changes.path' \ + "$FILEDIFF" 2>/dev/null | head -1)" + [[ -n "$diffchg" ]] || log_fail \ + "JSON: renamed entry for $path missing changes.path" + fi + + # Find the entry for our path and verify change_type + diffchg="$(jq -r --arg p "$relpath" \ + 'select(.path == $p) | .change_type' \ + "$FILEDIFF" 2>/dev/null | head -1)" + if [[ "$diffchg" != "$expected" ]]; then + log_fail "JSON: unexpected change_type for $path ('$diffchg' != '$expected')" + else + log_note "JSON: object $path change_type is correct: '$diffchg'" + fi +} + log_assert "'zfs diff' should display changes correctly." log_onexit cleanup @@ -70,6 +124,7 @@ MNTPOINT="$(get_prop mountpoint $DATASET)" log_must touch "$MNTPOINT/fremoved" log_must touch "$MNTPOINT/frenamed" log_must touch "$MNTPOINT/fmodified" +log_must touch "$MNTPOINT/flinked" log_must mkdir "$MNTPOINT/dremoved" log_must mkdir "$MNTPOINT/drenamed" log_must mkdir "$MNTPOINT/dmodified" @@ -79,6 +134,7 @@ log_must zfs snapshot "$TESTSNAP1" log_must rm -f "$MNTPOINT/fremoved" log_must mv "$MNTPOINT/frenamed" "$MNTPOINT/frenamed.new" log_must touch "$MNTPOINT/fmodified" +log_must ln "$MNTPOINT/flinked" "$MNTPOINT/flinked.hard" log_must rmdir "$MNTPOINT/dremoved" log_must mv "$MNTPOINT/drenamed" "$MNTPOINT/drenamed.new" log_must touch "$MNTPOINT/dmodified/file" @@ -94,4 +150,60 @@ verify_object_change "$MNTPOINT/drenamed.new" "R" verify_object_change "$MNTPOINT/dmodified" "M" verify_object_change "$MNTPOINT/dcreated" "+" +verify_object_change_json "$MNTPOINT/fremoved" "-" +verify_object_change_json "$MNTPOINT/frenamed.new" "R" +verify_object_change_json "$MNTPOINT/fmodified" "M" +verify_object_change_json "$MNTPOINT/fcreated" "+" +verify_object_change_json "$MNTPOINT/dremoved" "-" +verify_object_change_json "$MNTPOINT/drenamed.new" "R" +verify_object_change_json "$MNTPOINT/dmodified" "M" +verify_object_change_json "$MNTPOINT/dcreated" "+" + +# Verify changes.nlink {from, to} for the hard-linked file +log_must eval "zfs diff -j $TESTSNAP1 $TESTSNAP2 > $FILEDIFF" +nlink_from="$(jq -r --arg p "/flinked" \ + 'select(.path == $p) | .changes.nlink.from' "$FILEDIFF" 2>/dev/null)" +nlink_to="$(jq -r --arg p "/flinked" \ + 'select(.path == $p) | .changes.nlink.to' "$FILEDIFF" 2>/dev/null)" +[[ "$nlink_from" == "1" && "$nlink_to" == "2" ]] || \ + log_fail "JSON: expected nlink {from:1, to:2} for flinked, got from=$nlink_from to=$nlink_to" +log_note "JSON: flinked changes.nlink correct: from=$nlink_from to=$nlink_to" + +# Verify changes.ctime {from, to} with -jt +log_must eval "zfs diff -jt $TESTSNAP1 $TESTSNAP2 > $FILEDIFF" + +# Every entry must have changes.ctime with at least one of from/to +while IFS= read -r line; do + echo "$line" | log_must jq -e 'has("changes") and (.changes | has("ctime"))' \ + > /dev/null +done < "$FILEDIFF" + +# modified entry must have both changes.ctime.from and changes.ctime.to +ctime_from="$(jq -r --arg p "/fmodified" \ + 'select(.path == $p) | .changes.ctime.from' "$FILEDIFF" 2>/dev/null)" +ctime_to="$(jq -r --arg p "/fmodified" \ + 'select(.path == $p) | .changes.ctime.to' "$FILEDIFF" 2>/dev/null)" +[[ -n "$ctime_from" && "$ctime_from" != "null" && \ + -n "$ctime_to" && "$ctime_to" != "null" ]] || \ + log_fail "JSON: fmodified missing changes.ctime from/to" +log_note "JSON: fmodified changes.ctime.from=$ctime_from changes.ctime.to=$ctime_to" + +# added entry must have changes.ctime.to but not changes.ctime.from +ctime_from="$(jq -r --arg p "/fcreated" \ + 'select(.path == $p) | .changes.ctime.from' "$FILEDIFF" 2>/dev/null)" +ctime_to="$(jq -r --arg p "/fcreated" \ + 'select(.path == $p) | .changes.ctime.to' "$FILEDIFF" 2>/dev/null)" +[[ "$ctime_from" == "null" && -n "$ctime_to" && "$ctime_to" != "null" ]] || \ + log_fail "JSON: fcreated ctime should have to but not from" +log_note "JSON: fcreated changes.ctime.to=$ctime_to (no from, as expected)" + +# removed entry must have changes.ctime.from but not changes.ctime.to +ctime_from="$(jq -r --arg p "/fremoved" \ + 'select(.path == $p) | .changes.ctime.from' "$FILEDIFF" 2>/dev/null)" +ctime_to="$(jq -r --arg p "/fremoved" \ + 'select(.path == $p) | .changes.ctime.to' "$FILEDIFF" 2>/dev/null)" +[[ -n "$ctime_from" && "$ctime_from" != "null" && "$ctime_to" == "null" ]] || \ + log_fail "JSON: fremoved ctime should have from but not to" +log_note "JSON: fremoved changes.ctime.from=$ctime_from (no to, as expected)" + log_pass "'zfs diff' displays changes correctly." diff --git a/tests/zfs-tests/tests/functional/cli_root/zfs_diff/zfs_diff_cliargs.ksh b/tests/zfs-tests/tests/functional/cli_root/zfs_diff/zfs_diff_cliargs.ksh index b9c6f584ce48..ef06a6a5e54f 100755 --- a/tests/zfs-tests/tests/functional/cli_root/zfs_diff/zfs_diff_cliargs.ksh +++ b/tests/zfs-tests/tests/functional/cli_root/zfs_diff/zfs_diff_cliargs.ksh @@ -40,7 +40,7 @@ function cleanup log_assert "'zfs diff' should only work with supported options." log_onexit cleanup -typeset goodopts=("" "-h" "-t" "-th" "-H" "-Hh" "-Ht" "-Hth" "-F" "-Fh" "-Ft" "-Fth" "-FH" "-FHh" "-FHt" "-FHth") +typeset goodopts=("" "-h" "-t" "-th" "-H" "-Hh" "-Ht" "-Hth" "-F" "-Fh" "-Ft" "-Fth" "-FH" "-FHh" "-FHt" "-FHth" "-j" "-jt" "-jF" "-jFt") typeset badopts=("-f" "-T" "-Fx" "-Ho" "-tT" "-") DATASET="$TESTPOOL/$TESTFS"