From 9fb2771aa5f53a14f8c30dca6b9f609d192e50f6 Mon Sep 17 00:00:00 2001 From: Tony Hutter Date: Thu, 19 Dec 2019 16:26:07 -0800 Subject: [PATCH] Colorize zpool status output If the ZFS_COLOR env variable is set, then use ANSI color output in zpool status: - Column headers are bold - Degraded or offline pools/vdevs are yellow - Non-zero error counters and faulted vdevs/pools are red - The 'status:' and 'action:' sections are yellow if they're displaying a warning. This also includes a new 'faketty' function in libtest.shlib that is compatible with FreeBSD (code provided by @freqlabs). Reviewed-by: Jorgen Lundman Reviewed-by: Brian Behlendorf Reviewed-by: Ryan Moeller Signed-off-by: Tony Hutter Closes #9340 --- cmd/zpool/zpool_main.c | 394 ++++++++++++------ include/libzutil.h | 12 + lib/libzfs/libzfs_util.c | 92 ++++ man/man8/zpool.8 | 6 + tests/runfiles/common.run | 2 +- tests/zfs-tests/include/commands.cfg | 1 + tests/zfs-tests/include/libtest.shlib | 15 + .../functional/cli_root/zpool/Makefile.am | 3 +- .../tests/functional/cli_root/zpool/setup.ksh | 2 +- .../cli_root/zpool/zpool_colors.ksh | 91 ++++ 10 files changed, 479 insertions(+), 139 deletions(-) create mode 100755 tests/zfs-tests/tests/functional/cli_root/zpool/zpool_colors.ksh diff --git a/cmd/zpool/zpool_main.c b/cmd/zpool/zpool_main.c index 0ac8c2a5e4ee..f7696caac515 100644 --- a/cmd/zpool/zpool_main.c +++ b/cmd/zpool/zpool_main.c @@ -2033,6 +2033,28 @@ print_status_trim(vdev_stat_t *vs, boolean_t verbose) } } +/* + * Return the color associated with a health string. This includes returning + * NULL for no color change. + */ +static char * +health_str_to_color(const char *health) +{ + if (strcmp(health, gettext("FAULTED")) == 0 || + strcmp(health, gettext("SUSPENDED")) == 0 || + strcmp(health, gettext("UNAVAIL")) == 0) { + return (ANSI_RED); + } + + if (strcmp(health, gettext("OFFLINE")) == 0 || + strcmp(health, gettext("DEGRADED")) == 0 || + strcmp(health, gettext("REMOVED")) == 0) { + return (ANSI_YELLOW); + } + + return (NULL); +} + /* * Print out configuration state as requested by status_callback. */ @@ -2051,6 +2073,7 @@ print_status_config(zpool_handle_t *zhp, status_cbdata_t *cb, const char *name, const char *state; char *type; char *path = NULL; + char *rcolor = NULL, *wcolor = NULL, *ccolor = NULL; if (nvlist_lookup_nvlist_array(nv, ZPOOL_CONFIG_CHILDREN, &child, &children) != 0) @@ -2065,34 +2088,54 @@ print_status_config(zpool_handle_t *zhp, status_cbdata_t *cb, const char *name, return; state = zpool_state_to_name(vs->vs_state, vs->vs_aux); + if (isspare) { /* * For hot spares, we use the terms 'INUSE' and 'AVAILABLE' for * online drives. */ if (vs->vs_aux == VDEV_AUX_SPARED) - state = "INUSE"; + state = gettext("INUSE"); else if (vs->vs_state == VDEV_STATE_HEALTHY) - state = "AVAIL"; + state = gettext("AVAIL"); } - (void) printf("\t%*s%-*s %-8s", depth, "", cb->cb_namewidth - depth, + printf_color(health_str_to_color(state), + "\t%*s%-*s %-8s", depth, "", cb->cb_namewidth - depth, name, state); if (!isspare) { + if (vs->vs_read_errors) + rcolor = ANSI_RED; + + if (vs->vs_write_errors) + wcolor = ANSI_RED; + + if (vs->vs_checksum_errors) + ccolor = ANSI_RED; + if (cb->cb_literal) { - printf(" %5llu %5llu %5llu", - (u_longlong_t)vs->vs_read_errors, - (u_longlong_t)vs->vs_write_errors, + printf(" "); + printf_color(rcolor, "%5llu", + (u_longlong_t)vs->vs_read_errors); + printf(" "); + printf_color(wcolor, "%5llu", + (u_longlong_t)vs->vs_write_errors); + printf(" "); + printf_color(ccolor, "%5llu", (u_longlong_t)vs->vs_checksum_errors); } else { zfs_nicenum(vs->vs_read_errors, rbuf, sizeof (rbuf)); zfs_nicenum(vs->vs_write_errors, wbuf, sizeof (wbuf)); zfs_nicenum(vs->vs_checksum_errors, cbuf, sizeof (cbuf)); - printf(" %5s %5s %5s", rbuf, wbuf, cbuf); + printf(" "); + printf_color(rcolor, "%5s", rbuf); + printf(" "); + printf_color(wcolor, "%5s", wbuf); + printf(" "); + printf_color(ccolor, "%5s", cbuf); } - if (cb->cb_print_slow_ios) { if (children == 0) { /* Only leafs vdevs have slow IOs */ @@ -2107,16 +2150,15 @@ print_status_config(zpool_handle_t *zhp, status_cbdata_t *cb, const char *name, else printf(" %5s", rbuf); } - } if (nvlist_lookup_uint64(nv, ZPOOL_CONFIG_NOT_PRESENT, ¬present) == 0) { verify(nvlist_lookup_string(nv, ZPOOL_CONFIG_PATH, &path) == 0); - (void) printf(" was %s", path); + (void) printf(" %s %s", gettext("was"), path); } else if (vs->vs_aux != 0) { (void) printf(" "); - + color_start(ANSI_RED); switch (vs->vs_aux) { case VDEV_AUX_OPEN_FAILED: (void) printf(gettext("cannot open")); @@ -2188,6 +2230,7 @@ print_status_config(zpool_handle_t *zhp, status_cbdata_t *cb, const char *name, (void) printf(gettext("corrupted data")); break; } + color_end(); } /* The root vdev has the scrub/resilver stats */ @@ -2462,14 +2505,16 @@ show_import(nvlist_t *config) case ZPOOL_STATUS_MISSING_DEV_R: case ZPOOL_STATUS_MISSING_DEV_NR: case ZPOOL_STATUS_BAD_GUID_SUM: - (void) printf(gettext(" status: One or more devices are " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices are " "missing from the system.\n")); break; case ZPOOL_STATUS_CORRUPT_LABEL_R: case ZPOOL_STATUS_CORRUPT_LABEL_NR: - (void) printf(gettext(" status: One or more devices contains " - "corrupted data.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices contains" + " corrupted data.\n")); break; case ZPOOL_STATUS_CORRUPT_DATA: @@ -2478,78 +2523,96 @@ show_import(nvlist_t *config) break; case ZPOOL_STATUS_OFFLINE_DEV: - (void) printf(gettext(" status: One or more devices " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices " "are offlined.\n")); break; case ZPOOL_STATUS_CORRUPT_POOL: - (void) printf(gettext(" status: The pool metadata is " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool metadata is " "corrupted.\n")); break; case ZPOOL_STATUS_VERSION_OLDER: - (void) printf(gettext(" status: The pool is formatted using a " - "legacy on-disk version.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool is formatted using " + "a legacy on-disk version.\n")); break; case ZPOOL_STATUS_VERSION_NEWER: - (void) printf(gettext(" status: The pool is formatted using an " - "incompatible version.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool is formatted using " + "an incompatible version.\n")); break; case ZPOOL_STATUS_FEAT_DISABLED: - (void) printf(gettext(" status: Some supported features are " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("Some supported features are " "not enabled on the pool.\n")); break; case ZPOOL_STATUS_UNSUP_FEAT_READ: - (void) printf(gettext("status: The pool uses the following " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool uses the following " "feature(s) not supported on this system:\n")); + color_start(ANSI_YELLOW); zpool_print_unsup_feat(config); + color_end(); break; case ZPOOL_STATUS_UNSUP_FEAT_WRITE: - (void) printf(gettext("status: The pool can only be accessed " - "in read-only mode on this system. It\n\tcannot be " - "accessed in read-write mode because it uses the " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool can only be " + "accessed in read-only mode on this system. It\n\tcannot be" + " accessed in read-write mode because it uses the " "following\n\tfeature(s) not supported on this system:\n")); + color_start(ANSI_YELLOW); zpool_print_unsup_feat(config); + color_end(); break; case ZPOOL_STATUS_HOSTID_ACTIVE: - (void) printf(gettext(" status: The pool is currently " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool is currently " "imported by another system.\n")); break; case ZPOOL_STATUS_HOSTID_REQUIRED: - (void) printf(gettext(" status: The pool has the " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool has the " "multihost property on. It cannot\n\tbe safely imported " "when the system hostid is not set.\n")); break; case ZPOOL_STATUS_HOSTID_MISMATCH: - (void) printf(gettext(" status: The pool was last accessed by " - "another system.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool was last accessed " + "by another system.\n")); break; case ZPOOL_STATUS_FAULTED_DEV_R: case ZPOOL_STATUS_FAULTED_DEV_NR: - (void) printf(gettext(" status: One or more devices are " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices are " "faulted.\n")); break; case ZPOOL_STATUS_BAD_LOG: - (void) printf(gettext(" status: An intent log record cannot be " - "read.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("An intent log record cannot " + "be read.\n")); break; case ZPOOL_STATUS_RESILVERING: - (void) printf(gettext(" status: One or more devices were being " - "resilvered.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices were " + "being resilvered.\n")); break; case ZPOOL_STATUS_ERRATA: - (void) printf(gettext(" status: Errata #%d detected.\n"), + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("Errata #%d detected.\n"), errata); break; @@ -2644,13 +2707,15 @@ show_import(nvlist_t *config) "backup.\n")); break; case ZPOOL_STATUS_UNSUP_FEAT_READ: - (void) printf(gettext("action: The pool cannot be " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("The pool cannot be " "imported. Access the pool on a system that " "supports\n\tthe required feature(s), or recreate " "the pool from backup.\n")); break; case ZPOOL_STATUS_UNSUP_FEAT_WRITE: - (void) printf(gettext("action: The pool cannot be " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("The pool cannot be " "imported in read-write mode. Import the pool " "with\n" "\t\"-o readonly=on\", access the pool on a system " @@ -3720,7 +3785,7 @@ print_cmd_columns(vdev_cmd_data_list_t *vcdl, int use_dashes) for (j = 0; j < vcdl->uniq_cols_width[i]; j++) printf("-"); } else { - printf("%*s", vcdl->uniq_cols_width[i], + printf_color(ANSI_BOLD, "%*s", vcdl->uniq_cols_width[i], vcdl->uniq_cols[i]); } } @@ -7024,7 +7089,9 @@ print_scan_status(pool_scan_stat_t *ps) char processed_buf[7], scanned_buf[7], issued_buf[7], total_buf[7]; char srate_buf[7], irate_buf[7]; - (void) printf(gettext(" scan: ")); + printf(" "); + printf_color(ANSI_BOLD, gettext("scan:")); + printf(" "); /* If there's never been a scan, there's not much to say. */ if (ps == NULL || ps->pss_func == POOL_SCAN_NONE || @@ -7503,38 +7570,52 @@ status_callback(zpool_handle_t *zhp, void *data) health = zpool_get_state_str(zhp); - (void) printf(gettext(" pool: %s\n"), zpool_get_name(zhp)); - (void) printf(gettext(" state: %s\n"), health); + printf(" "); + printf_color(ANSI_BOLD, gettext("pool:")); + printf(" %s\n", zpool_get_name(zhp)); + printf(" "); + printf_color(ANSI_BOLD, gettext("state: ")); + + printf_color(health_str_to_color(health), "%s", health); + + printf("\n"); switch (reason) { case ZPOOL_STATUS_MISSING_DEV_R: - (void) printf(gettext("status: One or more devices could not " - "be opened. Sufficient replicas exist for\n\tthe pool to " - "continue functioning in a degraded state.\n")); - (void) printf(gettext("action: Attach the missing device and " - "online it using 'zpool online'.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices could " + "not be opened. Sufficient replicas exist for\n\tthe pool " + "to continue functioning in a degraded state.\n")); + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Attach the missing device " + "and online it using 'zpool online'.\n")); break; case ZPOOL_STATUS_MISSING_DEV_NR: - (void) printf(gettext("status: One or more devices could not " - "be opened. There are insufficient\n\treplicas for the " - "pool to continue functioning.\n")); - (void) printf(gettext("action: Attach the missing device and " - "online it using 'zpool online'.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices could " + "not be opened. There are insufficient\n\treplicas for the" + " pool to continue functioning.\n")); + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Attach the missing device " + "and online it using 'zpool online'.\n")); break; case ZPOOL_STATUS_CORRUPT_LABEL_R: - (void) printf(gettext("status: One or more devices could not " - "be used because the label is missing or\n\tinvalid. " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices could " + "not be used because the label is missing or\n\tinvalid. " "Sufficient replicas exist for the pool to continue\n\t" "functioning in a degraded state.\n")); - (void) printf(gettext("action: Replace the device using " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Replace the device using " "'zpool replace'.\n")); break; case ZPOOL_STATUS_CORRUPT_LABEL_NR: - (void) printf(gettext("status: One or more devices could not " - "be used because the label is missing \n\tor invalid. " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices could " + "not be used because the label is missing \n\tor invalid. " "There are insufficient replicas for the pool to " "continue\n\tfunctioning.\n")); zpool_explain_recover(zpool_get_handle(zhp), @@ -7542,175 +7623,209 @@ status_callback(zpool_handle_t *zhp, void *data) break; case ZPOOL_STATUS_FAILING_DEV: - (void) printf(gettext("status: One or more devices has " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices has " "experienced an unrecoverable error. An\n\tattempt was " "made to correct the error. Applications are " "unaffected.\n")); - (void) printf(gettext("action: Determine if the device needs " - "to be replaced, and clear the errors\n\tusing " - "'zpool clear' or replace the device with 'zpool " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Determine if the " + "device needs to be replaced, and clear the errors\n\tusing" + " 'zpool clear' or replace the device with 'zpool " "replace'.\n")); break; case ZPOOL_STATUS_OFFLINE_DEV: - (void) printf(gettext("status: One or more devices has " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices has " "been taken offline by the administrator.\n\tSufficient " "replicas exist for the pool to continue functioning in " "a\n\tdegraded state.\n")); - (void) printf(gettext("action: Online the device using " - "'zpool online' or replace the device with\n\t'zpool " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Online the device " + "using 'zpool online' or replace the device with\n\t'zpool " "replace'.\n")); break; case ZPOOL_STATUS_REMOVED_DEV: - (void) printf(gettext("status: One or more devices has " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices has " "been removed by the administrator.\n\tSufficient " "replicas exist for the pool to continue functioning in " "a\n\tdegraded state.\n")); - (void) printf(gettext("action: Online the device using " - "'zpool online' or replace the device with\n\t'zpool " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Online the device " + "using zpool online' or replace the device with\n\t'zpool " "replace'.\n")); break; case ZPOOL_STATUS_RESILVERING: - (void) printf(gettext("status: One or more devices is " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices is " "currently being resilvered. The pool will\n\tcontinue " "to function, possibly in a degraded state.\n")); - (void) printf(gettext("action: Wait for the resilver to " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Wait for the resilver to " "complete.\n")); break; case ZPOOL_STATUS_CORRUPT_DATA: - (void) printf(gettext("status: One or more devices has " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices has " "experienced an error resulting in data\n\tcorruption. " "Applications may be affected.\n")); - (void) printf(gettext("action: Restore the file in question " - "if possible. Otherwise restore the\n\tentire pool from " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Restore the file in question" + " if possible. Otherwise restore the\n\tentire pool from " "backup.\n")); break; case ZPOOL_STATUS_CORRUPT_POOL: - (void) printf(gettext("status: The pool metadata is corrupted " - "and the pool cannot be opened.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool metadata is " + "corrupted and the pool cannot be opened.\n")); zpool_explain_recover(zpool_get_handle(zhp), zpool_get_name(zhp), reason, config); break; case ZPOOL_STATUS_VERSION_OLDER: - (void) printf(gettext("status: The pool is formatted using a " - "legacy on-disk format. The pool can\n\tstill be used, " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool is formatted using " + "a legacy on-disk format. The pool can\n\tstill be used, " "but some features are unavailable.\n")); - (void) printf(gettext("action: Upgrade the pool using 'zpool " - "upgrade'. Once this is done, the\n\tpool will no longer " - "be accessible on software that does not support\n\t" + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Upgrade the pool using " + "'zpool upgrade'. Once this is done, the\n\tpool will no " + "longer be accessible on software that does not support\n\t" "feature flags.\n")); break; case ZPOOL_STATUS_VERSION_NEWER: - (void) printf(gettext("status: The pool has been upgraded to a " - "newer, incompatible on-disk version.\n\tThe pool cannot " - "be accessed on this system.\n")); - (void) printf(gettext("action: Access the pool from a system " - "running more recent software, or\n\trestore the pool from " - "backup.\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool has been upgraded " + "to a newer, incompatible on-disk version.\n\tThe pool " + "cannot be accessed on this system.\n")); + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Access the pool from a " + "system running more recent software, or\n\trestore the " + "pool from backup.\n")); break; case ZPOOL_STATUS_FEAT_DISABLED: - (void) printf(gettext("status: Some supported features are not " - "enabled on the pool. The pool can\n\tstill be used, but " - "some features are unavailable.\n")); - (void) printf(gettext("action: Enable all features using " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("Some supported features are " + "not enabled on the pool. The pool can\n\tstill be used, " + "but some features are unavailable.\n")); + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Enable all features using " "'zpool upgrade'. Once this is done,\n\tthe pool may no " "longer be accessible by software that does not support\n\t" "the features. See zpool-features(5) for details.\n")); break; case ZPOOL_STATUS_UNSUP_FEAT_READ: - (void) printf(gettext("status: The pool cannot be accessed on " - "this system because it uses the\n\tfollowing feature(s) " - "not supported on this system:\n")); + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool cannot be accessed " + "on this system because it uses the\n\tfollowing feature(s)" + " not supported on this system:\n")); zpool_print_unsup_feat(config); (void) printf("\n"); - (void) printf(gettext("action: Access the pool from a system " - "that supports the required feature(s),\n\tor restore the " - "pool from backup.\n")); + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Access the pool from a " + "system that supports the required feature(s),\n\tor " + "restore the pool from backup.\n")); break; case ZPOOL_STATUS_UNSUP_FEAT_WRITE: - (void) printf(gettext("status: The pool can only be accessed " - "in read-only mode on this system. It\n\tcannot be " - "accessed in read-write mode because it uses the " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool can only be " + "accessed in read-only mode on this system. It\n\tcannot be" + " accessed in read-write mode because it uses the " "following\n\tfeature(s) not supported on this system:\n")); zpool_print_unsup_feat(config); (void) printf("\n"); - (void) printf(gettext("action: The pool cannot be accessed in " - "read-write mode. Import the pool with\n" + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("The pool cannot be accessed " + "in read-write mode. Import the pool with\n" "\t\"-o readonly=on\", access the pool from a system that " "supports the\n\trequired feature(s), or restore the " "pool from backup.\n")); break; case ZPOOL_STATUS_FAULTED_DEV_R: - (void) printf(gettext("status: One or more devices are " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices are " "faulted in response to persistent errors.\n\tSufficient " "replicas exist for the pool to continue functioning " "in a\n\tdegraded state.\n")); - (void) printf(gettext("action: Replace the faulted device, " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Replace the faulted device, " "or use 'zpool clear' to mark the device\n\trepaired.\n")); break; case ZPOOL_STATUS_FAULTED_DEV_NR: - (void) printf(gettext("status: One or more devices are " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices are " "faulted in response to persistent errors. There are " "insufficient replicas for the pool to\n\tcontinue " "functioning.\n")); - (void) printf(gettext("action: Destroy and re-create the pool " - "from a backup source. Manually marking the device\n" + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Destroy and re-create the " + "pool from a backup source. Manually marking the device\n" "\trepaired using 'zpool clear' may allow some data " "to be recovered.\n")); break; case ZPOOL_STATUS_IO_FAILURE_MMP: - (void) printf(gettext("status: The pool is suspended because " - "multihost writes failed or were delayed;\n\tanother " - "system could import the pool undetected.\n")); - (void) printf(gettext("action: Make sure the pool's devices " - "are connected, then reboot your system and\n\timport the " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("The pool is suspended " + "because multihost writes failed or were delayed;\n\t" + "another system could import the pool undetected.\n")); + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Make sure the pool's devices" + " are connected, then reboot your system and\n\timport the " "pool.\n")); break; case ZPOOL_STATUS_IO_FAILURE_WAIT: case ZPOOL_STATUS_IO_FAILURE_CONTINUE: - (void) printf(gettext("status: One or more devices are " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("One or more devices are " "faulted in response to IO failures.\n")); - (void) printf(gettext("action: Make sure the affected devices " - "are connected, then run 'zpool clear'.\n")); + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Make sure the affected " + "devices are connected, then run 'zpool clear'.\n")); break; case ZPOOL_STATUS_BAD_LOG: - (void) printf(gettext("status: An intent log record " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("An intent log record " "could not be read.\n" "\tWaiting for administrator intervention to fix the " "faulted pool.\n")); - (void) printf(gettext("action: Either restore the affected " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Either restore the affected " "device(s) and run 'zpool online',\n" "\tor ignore the intent log records by running " "'zpool clear'.\n")); break; case ZPOOL_STATUS_HOSTID_MISMATCH: - (void) printf(gettext("status: Mismatch between pool hostid " - "and system hostid on imported pool.\n\tThis pool was " + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("Mismatch between pool hostid" + " and system hostid on imported pool.\n\tThis pool was " "previously imported into a system with a different " "hostid,\n\tand then was verbatim imported into this " "system.\n")); - (void) printf(gettext("action: Export this pool on all systems " - "on which it is imported.\n" + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("Export this pool on all " + "systems on which it is imported.\n" "\tThen import it to correct the mismatch.\n")); break; case ZPOOL_STATUS_ERRATA: - (void) printf(gettext("status: Errata #%d detected.\n"), + printf_color(ANSI_BOLD, gettext("status: ")); + printf_color(ANSI_YELLOW, gettext("Errata #%d detected.\n"), errata); switch (errata) { @@ -7718,16 +7833,18 @@ status_callback(zpool_handle_t *zhp, void *data) break; case ZPOOL_ERRATA_ZOL_2094_SCRUB: - (void) printf(gettext("action: To correct the issue " - "run 'zpool scrub'.\n")); + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("To correct the issue" + " run 'zpool scrub'.\n")); break; case ZPOOL_ERRATA_ZOL_6845_ENCRYPTION: (void) printf(gettext("\tExisting encrypted datasets " "contain an on-disk incompatibility\n\twhich " "needs to be corrected.\n")); - (void) printf(gettext("action: To correct the issue " - "backup existing encrypted datasets to new\n\t" + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("To correct the issue" + " backup existing encrypted datasets to new\n\t" "encrypted datasets and destroy the old ones. " "'zfs mount -o ro' can\n\tbe used to temporarily " "mount existing encrypted datasets readonly.\n")); @@ -7738,13 +7855,14 @@ status_callback(zpool_handle_t *zhp, void *data) "and bookmarks contain an on-disk\n\tincompat" "ibility. This may cause on-disk corruption if " "they are used\n\twith 'zfs recv'.\n")); - (void) printf(gettext("action: To correct the issue, " - "enable the bookmark_v2 feature. No additional\n\t" - "action is needed if there are no encrypted " - "snapshots or bookmarks.\n\tIf preserving the " - "encrypted snapshots and bookmarks is required, " - "use\n\ta non-raw send to backup and restore them. " - "Alternately, they may be\n\tremoved to resolve " + printf_color(ANSI_BOLD, gettext("action: ")); + printf_color(ANSI_YELLOW, gettext("To correct the" + "issue, enable the bookmark_v2 feature. No " + "additional\n\taction is needed if there are no " + "encrypted snapshots or bookmarks.\n\tIf preserving" + "the encrypted snapshots and bookmarks is required," + " use\n\ta non-raw send to backup and restore them." + " Alternately, they may be\n\tremoved to resolve " "the incompatibility.\n")); break; @@ -7764,9 +7882,11 @@ status_callback(zpool_handle_t *zhp, void *data) assert(reason == ZPOOL_STATUS_OK); } - if (msgid != NULL) - (void) printf(gettext(" see: http://zfsonlinux.org/msg/%s\n"), - msgid); + if (msgid != NULL) { + printf(" "); + printf_color(ANSI_BOLD, gettext("see:")); + printf(gettext(" http://zfsonlinux.org/msg/%s\n"), msgid); + } if (config != NULL) { uint64_t nerr; @@ -7782,7 +7902,6 @@ status_callback(zpool_handle_t *zhp, void *data) ZPOOL_CONFIG_SCAN_STATS, (uint64_t **)&ps, &c); (void) nvlist_lookup_uint64_array(nvroot, ZPOOL_CONFIG_REMOVAL_STATS, (uint64_t **)&prs, &c); - print_scan_status(ps); print_checkpoint_scan_warning(ps, pcs); print_removal_status(zhp, prs); @@ -7793,13 +7912,16 @@ status_callback(zpool_handle_t *zhp, void *data) if (cbp->cb_namewidth < 10) cbp->cb_namewidth = 10; + color_start(ANSI_BOLD); (void) printf(gettext("config:\n\n")); (void) printf(gettext("\t%-*s %-8s %5s %5s %5s"), cbp->cb_namewidth, "NAME", "STATE", "READ", "WRITE", "CKSUM"); + color_end(); - if (cbp->cb_print_slow_ios) - (void) printf(" %5s", gettext("SLOW")); + if (cbp->cb_print_slow_ios) { + printf_color(ANSI_BOLD, " %5s", gettext("SLOW")); + } if (cbp->vcdl != NULL) print_cmd_columns(cbp->vcdl, 0); diff --git a/include/libzutil.h b/include/libzutil.h index 3dd4fcfb23e5..98998e1950b5 100644 --- a/include/libzutil.h +++ b/include/libzutil.h @@ -146,6 +146,18 @@ extern int zpool_history_unpack(char *, uint64_t, uint64_t *, nvlist_t ***, struct zfs_cmd; int zfs_ioctl_fd(int fd, unsigned long request, struct zfs_cmd *zc); +/* + * List of colors to use + */ +#define ANSI_RED "\033[0;31m" +#define ANSI_YELLOW "\033[0;33m" +#define ANSI_RESET "\033[0m" +#define ANSI_BOLD "\033[1m" + +void color_start(char *color); +void color_end(void); +int printf_color(char *color, char *format, ...); + #ifdef __cplusplus } #endif diff --git a/lib/libzfs/libzfs_util.c b/lib/libzfs/libzfs_util.c index 4a545a027436..2ce3ad1062dc 100644 --- a/lib/libzfs/libzfs_util.c +++ b/lib/libzfs/libzfs_util.c @@ -1886,3 +1886,95 @@ zfs_version_print(void) return (0); } + +/* + * Return 1 if the user requested ANSI color output, and our terminal supports + * it. Return 0 for no color. + */ +static int +use_color(void) +{ + static int use_color = -1; + char *term; + + /* + * Optimization: + * + * For each zpool invocation, we do a single check to see if we should + * be using color or not, and cache that value for the lifetime of the + * the zpool command. That makes it cheap to call use_color() when + * we're printing with color. We assume that the settings are not going + * to change during the invocation of a zpool command (the user isn't + * going to change the ZFS_COLOR value while zpool is running, for + * example). + */ + if (use_color != -1) { + /* + * We've already figured out if we should be using color or + * not. Return the cached value. + */ + return (use_color); + } + + term = getenv("TERM"); + /* + * The user sets the ZFS_COLOR env var set to enable zpool ANSI color + * output. However if NO_COLOR is set (https://no-color.org/) then + * don't use it. Also, don't use color if terminal doesn't support + * it. + */ + if (libzfs_envvar_is_set("ZFS_COLOR") && + !libzfs_envvar_is_set("NO_COLOR") && + isatty(STDOUT_FILENO) && term && strcmp("dumb", term) != 0 && + strcmp("unknown", term) != 0) { + /* Color supported */ + use_color = 1; + } else { + use_color = 0; + } + + return (use_color); +} + +/* + * color_start() and color_end() are used for when you want to colorize a block + * of text. For example: + * + * color_start(ANSI_RED_FG) + * printf("hello"); + * printf("world"); + * color_end(); + */ +void +color_start(char *color) +{ + if (use_color()) + printf("%s", color); +} + +void +color_end(void) +{ + if (use_color()) + printf(ANSI_RESET); +} + +/* printf() with a color. If color is NULL, then do a normal printf. */ +int +printf_color(char *color, char *format, ...) +{ + va_list aptr; + int rc; + + if (color) + color_start(color); + + va_start(aptr, format); + rc = vprintf(format, aptr); + va_end(aptr); + + if (color) + color_end(); + + return (rc); +} diff --git a/man/man8/zpool.8 b/man/man8/zpool.8 index 4b15785c2921..272e4bc1ce4d 100644 --- a/man/man8/zpool.8 +++ b/man/man8/zpool.8 @@ -453,6 +453,12 @@ Cause to dump core on exit for the purposes of running .Sy ::findleaks . .El +.Bl -tag -width "ZFS_COLOR" +.It Ev ZFS_COLOR +Use ANSI color in +.Nm zpool status +output. +.El .Bl -tag -width "ZPOOL_IMPORT_PATH" .It Ev ZPOOL_IMPORT_PATH The search path for devices or files to use with the pool. This is a colon-separated list of directories in which diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 87849af653b2..a3396ac07ffc 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -282,7 +282,7 @@ tests = ['zfs_upgrade_001_pos', 'zfs_upgrade_002_pos', 'zfs_upgrade_003_pos', tags = ['functional', 'cli_root', 'zfs_upgrade'] [tests/functional/cli_root/zpool] -tests = ['zpool_001_neg', 'zpool_002_pos', 'zpool_003_pos'] +tests = ['zpool_001_neg', 'zpool_002_pos', 'zpool_003_pos', 'zpool_colors'] tags = ['functional', 'cli_root', 'zpool'] [tests/functional/cli_root/zpool_add] diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index 64b34f477344..d993a8576c76 100644 --- a/tests/zfs-tests/include/commands.cfg +++ b/tests/zfs-tests/include/commands.cfg @@ -77,6 +77,7 @@ export SYSTEM_FILES_COMMON='arp rm rmdir scp + script sed seq setfacl diff --git a/tests/zfs-tests/include/libtest.shlib b/tests/zfs-tests/include/libtest.shlib index 5b5f3189c5b2..df794418c9fc 100644 --- a/tests/zfs-tests/include/libtest.shlib +++ b/tests/zfs-tests/include/libtest.shlib @@ -3916,3 +3916,18 @@ function stat_size # ;; esac } + +# Run a command as if it was being run in a TTY. +# +# Usage: +# +# faketty command +# +function faketty +{ + if is_freebsd; then + script -q /dev/null "$@" + else + script --return --quiet -c "$*" /dev/null + fi +} diff --git a/tests/zfs-tests/tests/functional/cli_root/zpool/Makefile.am b/tests/zfs-tests/tests/functional/cli_root/zpool/Makefile.am index 2d0046c53a17..327f23621158 100644 --- a/tests/zfs-tests/tests/functional/cli_root/zpool/Makefile.am +++ b/tests/zfs-tests/tests/functional/cli_root/zpool/Makefile.am @@ -4,4 +4,5 @@ dist_pkgdata_SCRIPTS = \ cleanup.ksh \ zpool_001_neg.ksh \ zpool_002_pos.ksh \ - zpool_003_pos.ksh + zpool_003_pos.ksh \ + zpool_colors.ksh diff --git a/tests/zfs-tests/tests/functional/cli_root/zpool/setup.ksh b/tests/zfs-tests/tests/functional/cli_root/zpool/setup.ksh index 6a9af3bc28c3..4e3b6b0e9f47 100755 --- a/tests/zfs-tests/tests/functional/cli_root/zpool/setup.ksh +++ b/tests/zfs-tests/tests/functional/cli_root/zpool/setup.ksh @@ -29,4 +29,4 @@ DISK=${DISKS%% *} -default_setup $DISK +default_mirror_setup $DISKS diff --git a/tests/zfs-tests/tests/functional/cli_root/zpool/zpool_colors.ksh b/tests/zfs-tests/tests/functional/cli_root/zpool/zpool_colors.ksh new file mode 100755 index 000000000000..0ec1e84257a1 --- /dev/null +++ b/tests/zfs-tests/tests/functional/cli_root/zpool/zpool_colors.ksh @@ -0,0 +1,91 @@ +#!/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 of the CDDL is also available via the Internet at +# http://www.illumos.org/license/CDDL. +# +# CDDL HEADER END +# +# Copyright (c) 2019 Lawrence Livermore National Security, LLC. + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Test that zpool status colored output works. +# +# STRATEGY: +# 1. Create a pool with a bunch of errors and force fault one of the vdevs. +# 2. Look for 'pool:' in bold. +# 3. Look for 'DEGRADED' in yellow +# 3. Look for 'FAULTED' in red +# + +verify_runnable "both" + +function cleanup +{ + zinject -c all +} + +log_onexit cleanup + +log_assert "Test colorized zpool status output" + +DISK2="$(echo $DISKS | cut -d' ' -f2)" +DISK3="$(echo $DISKS | cut -d' ' -f3)" + +log_must dd if=/dev/urandom of=/$TESTDIR/testfile bs=10M count=1 + +log_must zpool sync + +log_must zpool offline -f $TESTPOOL $DISK3 +log_must wait_for_degraded $TESTPOOL +log_must zinject -d $DISK2 -e io -T read -f 20 $TESTPOOL +log_must zinject -d $DISK2 -e io -T write -f 20 $TESTPOOL + + +log_must zpool scrub -w $TESTPOOL +log_must zinject -c all + + +# Use 'script' to fake zpool status into thinking it's running in a tty. +# Log the output here in case it's needed for postmortem. +log_note "$(faketty TERM=xterm-256color ZFS_COLOR=1 zpool status)" + +# Replace the escape codes with "ESC" so they're easier to grep +out="$(faketty TERM=xterm-256color ZFS_COLOR=1 zpool status | \ + grep -E 'pool:|DEGRADED' | \ + sed -r 's/\s+//g;'$(echo -e 's/\033/ESC/g'))" + +log_note "$(echo $out)" + +log_note "Look for 'pool:' in bold" +log_must eval "echo \"$out\" | grep -q 'ESC\[1mpool:ESC\[0m' " + +log_note "Look for 'DEGRADED' in yellow" +log_must eval "echo \"$out\" | grep -q 'ESC\[0;33mDEGRADEDESC\[0m'" + +# +# The escape code for 'FAULTED' is a little more tricky. The line starts like +# this: +# +# loop2 FAULTED +# +# Luckily, awk counts the start and end escape codes as separate fields, so +# we can easily remove the vdev field to get what we want. +# +out="$(faketty TERM=xterm-256color ZFS_COLOR=1 zpool status \ + | awk '/FAULTED/{print $1$3$4}' | sed -r $(echo -e 's/\033/ESC/g'))" + +log_note "Look for 'FAULTED' in red" +log_must eval "echo \"$out\" | grep -q 'ESC\[0;31mFAULTEDESC\[0m'" + +log_pass "zpool status displayed colors"