Skip to content

Commit e404bf3

Browse files
committed
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. Signed-off-by: Tony Hutter <[email protected]>
1 parent da92d5c commit e404bf3

File tree

9 files changed

+468
-136
lines changed

9 files changed

+468
-136
lines changed

cmd/zpool/zpool_main.c

Lines changed: 253 additions & 133 deletions
Large diffs are not rendered by default.

include/libzutil.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,18 @@ extern int zpool_history_unpack(char *, uint64_t, uint64_t *, nvlist_t ***,
146146
struct zfs_cmd;
147147
int zfs_ioctl_fd(int fd, unsigned long request, struct zfs_cmd *zc);
148148

149+
/*
150+
* List of colors to use ("FG" for forground color, "BG" for background)
151+
*/
152+
#define ANSI_RED "\033[0;31m"
153+
#define ANSI_YELLOW "\033[0;33m"
154+
#define ANSI_RESET "\033[0m"
155+
#define ANSI_BOLD "\033[1m"
156+
157+
void color_start(char *color);
158+
void color_end(void);
159+
int printf_color(char *color, char *format, ...);
160+
149161
#ifdef __cplusplus
150162
}
151163
#endif

lib/libzfs/libzfs_util.c

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,3 +1886,106 @@ zfs_version_print(void)
18861886

18871887
return (0);
18881888
}
1889+
1890+
/*
1891+
* Return 1 if the user requested ANSI color output, and our terminal supports
1892+
* it. Return 0 for no color.
1893+
*/
1894+
static int
1895+
use_color(void)
1896+
{
1897+
static int use_color = -1;
1898+
char *term;
1899+
1900+
/*
1901+
* Optimization:
1902+
*
1903+
* For each zpool invocation, we do a single check to see if we should
1904+
* be using color or not, and cache that value for the lifetime of the
1905+
* the zpool command. That makes it cheap to call use_color() when
1906+
* we're printing with color. We assume that the settings are not going
1907+
* to change during the invocation of a zpool command (the user isn't
1908+
* going to change the ZFS_COLOR value while zpool is running, for
1909+
* example).
1910+
*/
1911+
if (use_color != -1) {
1912+
/*
1913+
* We've already figured out if we should be using color or
1914+
* not. Return the cached value.
1915+
*/
1916+
return (use_color);
1917+
}
1918+
1919+
term = getenv("TERM");
1920+
if (libzfs_envvar_is_set("ZFS_COLOR")) {
1921+
printf("%s: no_color %d, isatty %d, term=%s\n", __func__,
1922+
isatty(STDOUT_FILENO), term ? term : "NULL");
1923+
}
1924+
1925+
/*
1926+
* The user sets the ZFS_COLOR env var set to enable zpool ANSI color
1927+
* output. However if NO_COLOR is set (https://no-color.org/) then
1928+
* don't use it. Also, don't use color if terminal doesn't support
1929+
* it.
1930+
*/
1931+
if (libzfs_envvar_is_set("ZFS_COLOR") &&
1932+
!libzfs_envvar_is_set("NO_COLOR") &&
1933+
isatty(STDOUT_FILENO) && term && strcmp("dumb", term) != 0 &&
1934+
strcmp("unknown", term) != 0) {
1935+
/* Color supported */
1936+
use_color = 1;
1937+
} else {
1938+
use_color = 0;
1939+
}
1940+
1941+
return (use_color);
1942+
}
1943+
1944+
/*
1945+
* color_start() and color_end() are used for when you want to colorize a block
1946+
* of text. For example:
1947+
*
1948+
* color_start(ANSI_RED_FG)
1949+
* printf("hello");
1950+
* printf("world");
1951+
* color_end();
1952+
*/
1953+
void
1954+
color_start(char *color)
1955+
{
1956+
if (use_color())
1957+
printf("%s", color);
1958+
}
1959+
1960+
void
1961+
color_end(void)
1962+
{
1963+
if (use_color())
1964+
printf(ANSI_RESET);
1965+
}
1966+
1967+
/* printf() with a color. If color is NULL, then do a normal printf. */
1968+
int
1969+
printf_color(char *color, char *format, ...)
1970+
{
1971+
char buf[4090];
1972+
va_list aptr;
1973+
int rc;
1974+
1975+
if (color)
1976+
color_start(color);
1977+
1978+
va_start(aptr, format);
1979+
vsnprintf(buf, sizeof (buf), format, aptr);
1980+
va_end(aptr);
1981+
1982+
/* Null terminate */
1983+
buf[sizeof (buf) - 1] = '\0';
1984+
1985+
rc = printf("%s", buf);
1986+
1987+
if (color)
1988+
color_end();
1989+
1990+
return (rc);
1991+
}

man/man8/zpool.8

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,12 @@ Cause
453453
to dump core on exit for the purposes of running
454454
.Sy ::findleaks .
455455
.El
456+
.Bl -tag -width "ZFS_COLOR"
457+
.It Ev ZFS_COLOR
458+
Use ANSI color in
459+
.Nm zpool status
460+
output.
461+
.El
456462
.Bl -tag -width "ZPOOL_IMPORT_PATH"
457463
.It Ev ZPOOL_IMPORT_PATH
458464
The search path for devices or files to use with the pool. This is a colon-separated list of directories in which

tests/runfiles/common.run

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ tests = ['zfs_upgrade_001_pos', 'zfs_upgrade_002_pos', 'zfs_upgrade_003_pos',
277277
tags = ['functional', 'cli_root', 'zfs_upgrade']
278278

279279
[tests/functional/cli_root/zpool]
280-
tests = ['zpool_001_neg', 'zpool_002_pos', 'zpool_003_pos']
280+
tests = ['zpool_001_neg', 'zpool_002_pos', 'zpool_003_pos', 'zpool_colors']
281281
tags = ['functional', 'cli_root', 'zpool']
282282

283283
[tests/functional/cli_root/zpool_add]

tests/zfs-tests/include/commands.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export SYSTEM_FILES='arp
108108
setenforce
109109
setfacl
110110
setfattr
111+
script
111112
sh
112113
sha256sum
113114
shuf

tests/zfs-tests/tests/functional/cli_root/zpool/Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ dist_pkgdata_SCRIPTS = \
44
cleanup.ksh \
55
zpool_001_neg.ksh \
66
zpool_002_pos.ksh \
7-
zpool_003_pos.ksh
7+
zpool_003_pos.ksh \
8+
zpool_colors.ksh

tests/zfs-tests/tests/functional/cli_root/zpool/setup.ksh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@
2929

3030
DISK=${DISKS%% *}
3131

32-
default_setup $DISK
32+
default_mirror_setup $DISKS
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/bin/ksh -p
2+
#
3+
# CDDL HEADER START
4+
#
5+
# This file and its contents are supplied under the terms of the
6+
# Common Development and Distribution License ("CDDL"), version 1.0.
7+
# You may only use this file in accordance with the terms of version
8+
# 1.0 of the CDDL.
9+
#
10+
# A full copy of the text of the CDDL should have accompanied this
11+
# source. A copy of the CDDL is also available via the Internet at
12+
# http://www.illumos.org/license/CDDL.
13+
#
14+
# CDDL HEADER END
15+
#
16+
# Copyright (c) 2019 Lawrence Livermore National Security, LLC.
17+
18+
. $STF_SUITE/include/libtest.shlib
19+
20+
#
21+
# DESCRIPTION:
22+
# Test that zpool status colored output works.
23+
#
24+
# STRATEGY:
25+
# 1. Create a pool with a bunch of errors and force fault one of the vdevs.
26+
# 2. Look for 'pool:' in bold.
27+
# 3. Look for 'DEGRADED' in yellow
28+
# 3. Look for 'FAULTED' in red
29+
#
30+
31+
verify_runnable "both"
32+
33+
function cleanup
34+
{
35+
zinject -c all
36+
}
37+
38+
log_onexit cleanup
39+
40+
log_assert "Test colorized zpool status output"
41+
42+
DISK2="$(echo $DISKS | cut -d' ' -f2)"
43+
DISK3="$(echo $DISKS | cut -d' ' -f3)"
44+
45+
log_must dd if=/dev/urandom of=/$TESTDIR/testfile bs=10M count=1
46+
47+
log_must zpool export $TESTPOOL
48+
log_must zpool import $TESTPOOL
49+
50+
log_must zpool offline -f $TESTPOOL $DISK3
51+
log_must wait_for_degraded $TESTPOOL
52+
log_must zinject -d $DISK2 -e io -T read -f 20 $TESTPOOL
53+
log_must zinject -d $DISK2 -e io -T write -f 20 $TESTPOOL
54+
55+
56+
log_must zpool scrub $TESTPOOL
57+
log_must wait_scrubbed $TESTPOOL
58+
log_must zinject -c all
59+
60+
# Use 'script' to fake zpool status into thinking it's running in a tty.
61+
# Replace the escape codes with "ESC" so they're easier to grep
62+
out="$(script --return --quiet -c 'ZFS_COLOR=1 zpool status' /dev/null | \
63+
grep -E 'pool:|DEGRADED' | \
64+
sed -r 's/\s+//g;'$(echo -e 's/\033/ESC/g'))"
65+
66+
log_note "$(echo $out)"
67+
68+
log_note "Look for 'pool:' in bold"
69+
log_must eval "echo \"$out\" | grep -q 'ESC\[1mpool:ESC\[0m' "
70+
71+
log_note "Look for 'DEGRADED' in yellow"
72+
log_must eval "echo \"$out\" | grep -q 'ESC\[0;33mDEGRADEDESC\[0m'"
73+
74+
#
75+
# The escape code for 'FAULTED' is a little more tricky. The line starts like
76+
# this:
77+
#
78+
# <start red escape code> loop2 FAULTED <end escape code>
79+
#
80+
# Luckily, awk counts the start and end escape codes as separate fields, so
81+
# we can easily remove the vdev field to get what we want.
82+
#
83+
out="$(script --return --quiet -c 'ZFS_COLOR=1 zpool status' /dev/null \
84+
| awk '/FAULTED/{print $1$3$4}' | sed -r $(echo -e 's/\033/ESC/g'))"
85+
86+
log_note "Look for 'FAULTED' in red"
87+
log_must eval "echo \"$out\" | grep -q 'ESC\[0;31mFAULTEDESC\[0m'"
88+
89+
log_pass "zpool status displayed colors"

0 commit comments

Comments
 (0)