1073 lines
38 KiB
Python
Executable File
1073 lines
38 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (c) 2008 Ben Rockwood <benr@cuddletech.com>,
|
|
# Copyright (c) 2010 Martin Matuska <mm@FreeBSD.org>,
|
|
# Copyright (c) 2010-2011 Jason J. Hellenthal <jhell@DataIX.net>,
|
|
# Copyright (c) 2017 Scot W. Stevenson <scot.stevenson@gmail.com>
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions
|
|
# are met:
|
|
#
|
|
# 1. Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# 2. Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
# SUCH DAMAGE.
|
|
"""Print statistics on the ZFS ARC Cache and other information
|
|
|
|
Provides basic information on the ARC, its efficiency, the L2ARC (if present),
|
|
the Data Management Unit (DMU), Virtual Devices (VDEVs), and tunables. See
|
|
the in-source documentation and code at
|
|
https://github.com/openzfs/zfs/blob/master/module/zfs/arc.c for details.
|
|
The original introduction to arc_summary can be found at
|
|
http://cuddletech.com/?p=454
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import errno
|
|
|
|
# We can't use env -S portably, and we need python3 -u to handle pipes in
|
|
# the shell abruptly closing the way we want to, so...
|
|
import io
|
|
if isinstance(sys.__stderr__.buffer, io.BufferedWriter):
|
|
os.execv(sys.executable, [sys.executable, "-u"] + sys.argv)
|
|
|
|
DESCRIPTION = 'Print ARC and other statistics for OpenZFS'
|
|
INDENT = ' '*8
|
|
LINE_LENGTH = 72
|
|
DATE_FORMAT = '%a %b %d %H:%M:%S %Y'
|
|
TITLE = 'ZFS Subsystem Report'
|
|
|
|
SECTIONS = 'arc archits dmu l2arc spl tunables vdev zil'.split()
|
|
SECTION_HELP = 'print info from one section ('+' '.join(SECTIONS)+')'
|
|
|
|
# Tunables and SPL are handled separately because they come from
|
|
# different sources
|
|
SECTION_PATHS = {'arc': 'arcstats',
|
|
'dmu': 'dmu_tx',
|
|
'l2arc': 'arcstats', # L2ARC stuff lives in arcstats
|
|
'zfetch': 'zfetchstats',
|
|
'zil': 'zil'}
|
|
|
|
parser = argparse.ArgumentParser(description=DESCRIPTION)
|
|
parser.add_argument('-a', '--alternate', action='store_true', default=False,
|
|
help='use alternate formatting for tunables and SPL',
|
|
dest='alt')
|
|
parser.add_argument('-d', '--description', action='store_true', default=False,
|
|
help='print descriptions with tunables and SPL',
|
|
dest='desc')
|
|
parser.add_argument('-g', '--graph', action='store_true', default=False,
|
|
help='print graph on ARC use and exit', dest='graph')
|
|
parser.add_argument('-p', '--page', type=int, dest='page',
|
|
help='print page by number (DEPRECATED, use "-s")')
|
|
parser.add_argument('-r', '--raw', action='store_true', default=False,
|
|
help='dump all available data with minimal formatting',
|
|
dest='raw')
|
|
parser.add_argument('-s', '--section', dest='section', help=SECTION_HELP)
|
|
ARGS = parser.parse_args()
|
|
|
|
|
|
if sys.platform.startswith('freebsd'):
|
|
# Requires py36-sysctl on FreeBSD
|
|
import sysctl
|
|
|
|
def is_value(ctl):
|
|
return ctl.type != sysctl.CTLTYPE_NODE
|
|
|
|
def namefmt(ctl, base='vfs.zfs.'):
|
|
# base is removed from the name
|
|
cut = len(base)
|
|
return ctl.name[cut:]
|
|
|
|
def load_kstats(section):
|
|
base = 'kstat.zfs.misc.{section}.'.format(section=section)
|
|
fmt = lambda kstat: '{name} : {value}'.format(name=namefmt(kstat, base),
|
|
value=kstat.value)
|
|
kstats = sysctl.filter(base)
|
|
return [fmt(kstat) for kstat in kstats if is_value(kstat)]
|
|
|
|
def get_params(base):
|
|
ctls = sysctl.filter(base)
|
|
return {namefmt(ctl): str(ctl.value) for ctl in ctls if is_value(ctl)}
|
|
|
|
def get_tunable_params():
|
|
return get_params('vfs.zfs')
|
|
|
|
def get_vdev_params():
|
|
return get_params('vfs.zfs.vdev')
|
|
|
|
def get_version_impl(request):
|
|
# FreeBSD reports versions for zpl and spa instead of zfs and spl.
|
|
name = {'zfs': 'zpl',
|
|
'spl': 'spa'}[request]
|
|
mib = 'vfs.zfs.version.{}'.format(name)
|
|
version = sysctl.filter(mib)[0].value
|
|
return '{} version {}'.format(name, version)
|
|
|
|
def get_descriptions(_request):
|
|
ctls = sysctl.filter('vfs.zfs')
|
|
return {namefmt(ctl): ctl.description for ctl in ctls if is_value(ctl)}
|
|
|
|
|
|
elif sys.platform.startswith('linux'):
|
|
KSTAT_PATH = '/proc/spl/kstat/zfs'
|
|
SPL_PATH = '/sys/module/spl/parameters'
|
|
TUNABLES_PATH = '/sys/module/zfs/parameters'
|
|
|
|
def load_kstats(section):
|
|
path = os.path.join(KSTAT_PATH, section)
|
|
with open(path) as f:
|
|
return list(f)[2:] # Get rid of header
|
|
|
|
def get_params(basepath):
|
|
"""Collect information on the Solaris Porting Layer (SPL) or the
|
|
tunables, depending on the PATH given. Does not check if PATH is
|
|
legal.
|
|
"""
|
|
result = {}
|
|
for name in os.listdir(basepath):
|
|
path = os.path.join(basepath, name)
|
|
with open(path) as f:
|
|
value = f.read()
|
|
result[name] = value.strip()
|
|
return result
|
|
|
|
def get_spl_params():
|
|
return get_params(SPL_PATH)
|
|
|
|
def get_tunable_params():
|
|
return get_params(TUNABLES_PATH)
|
|
|
|
def get_vdev_params():
|
|
return get_params(TUNABLES_PATH)
|
|
|
|
def get_version_impl(request):
|
|
# The original arc_summary called /sbin/modinfo/{spl,zfs} to get
|
|
# the version information. We switch to /sys/module/{spl,zfs}/version
|
|
# to make sure we get what is really loaded in the kernel
|
|
try:
|
|
with open("/sys/module/{}/version".format(request)) as f:
|
|
return f.read().strip()
|
|
except:
|
|
return "(unknown)"
|
|
|
|
def get_descriptions(request):
|
|
"""Get the descriptions of the Solaris Porting Layer (SPL) or the
|
|
tunables, return with minimal formatting.
|
|
"""
|
|
|
|
if request not in ('spl', 'zfs'):
|
|
print('ERROR: description of "{0}" requested)'.format(request))
|
|
sys.exit(1)
|
|
|
|
descs = {}
|
|
target_prefix = 'parm:'
|
|
|
|
# We would prefer to do this with /sys/modules -- see the discussion at
|
|
# get_version() -- but there isn't a way to get the descriptions from
|
|
# there, so we fall back on modinfo
|
|
command = ["/sbin/modinfo", request, "-0"]
|
|
|
|
info = ''
|
|
|
|
try:
|
|
|
|
info = subprocess.run(command, stdout=subprocess.PIPE,
|
|
check=True, universal_newlines=True)
|
|
raw_output = info.stdout.split('\0')
|
|
|
|
except subprocess.CalledProcessError:
|
|
print("Error: Descriptions not available",
|
|
"(can't access kernel module)")
|
|
sys.exit(1)
|
|
|
|
for line in raw_output:
|
|
|
|
if not line.startswith(target_prefix):
|
|
continue
|
|
|
|
line = line[len(target_prefix):].strip()
|
|
name, raw_desc = line.split(':', 1)
|
|
desc = raw_desc.rsplit('(', 1)[0]
|
|
|
|
if desc == '':
|
|
desc = '(No description found)'
|
|
|
|
descs[name.strip()] = desc.strip()
|
|
|
|
return descs
|
|
|
|
def handle_unraisableException(exc_type, exc_value=None, exc_traceback=None,
|
|
err_msg=None, object=None):
|
|
handle_Exception(exc_type, object, exc_traceback)
|
|
|
|
def handle_Exception(ex_cls, ex, tb):
|
|
if ex_cls is KeyboardInterrupt:
|
|
sys.exit()
|
|
|
|
if ex_cls is BrokenPipeError:
|
|
# It turns out that while sys.exit() triggers an exception
|
|
# not handled message on Python 3.8+, os._exit() does not.
|
|
os._exit(0)
|
|
|
|
if ex_cls is OSError:
|
|
if ex.errno == errno.ENOTCONN:
|
|
sys.exit()
|
|
|
|
raise ex
|
|
|
|
if hasattr(sys,'unraisablehook'): # Python 3.8+
|
|
sys.unraisablehook = handle_unraisableException
|
|
sys.excepthook = handle_Exception
|
|
|
|
|
|
def cleanup_line(single_line):
|
|
"""Format a raw line of data from /proc and isolate the name value
|
|
part, returning a tuple with each. Currently, this gets rid of the
|
|
middle '4'. For example "arc_no_grow 4 0" returns the tuple
|
|
("arc_no_grow", "0").
|
|
"""
|
|
name, _, value = single_line.split()
|
|
|
|
return name, value
|
|
|
|
|
|
def draw_graph(kstats_dict):
|
|
"""Draw a primitive graph representing the basic information on the
|
|
ARC -- its size and the proportion used by MFU and MRU -- and quit.
|
|
We use max size of the ARC to calculate how full it is. This is a
|
|
very rough representation.
|
|
"""
|
|
|
|
arc_stats = isolate_section('arcstats', kstats_dict)
|
|
|
|
GRAPH_INDENT = ' '*4
|
|
GRAPH_WIDTH = 70
|
|
arc_max = int(arc_stats['c_max'])
|
|
arc_size = f_bytes(arc_stats['size'])
|
|
arc_perc = f_perc(arc_stats['size'], arc_max)
|
|
data_size = f_bytes(arc_stats['data_size'])
|
|
meta_size = f_bytes(arc_stats['metadata_size'])
|
|
dnode_size = f_bytes(arc_stats['dnode_size'])
|
|
|
|
info_form = ('ARC: {0} ({1}) Data: {2} Meta: {3} Dnode: {4}')
|
|
info_line = info_form.format(arc_size, arc_perc, data_size, meta_size,
|
|
dnode_size)
|
|
info_spc = ' '*int((GRAPH_WIDTH-len(info_line))/2)
|
|
info_line = GRAPH_INDENT+info_spc+info_line
|
|
|
|
graph_line = GRAPH_INDENT+'+'+('-'*(GRAPH_WIDTH-2))+'+'
|
|
|
|
arc_perc = float(int(arc_stats['size'])/arc_max)
|
|
data_perc = float(int(arc_stats['data_size'])/arc_max)
|
|
meta_perc = float(int(arc_stats['metadata_size'])/arc_max)
|
|
dnode_perc = float(int(arc_stats['dnode_size'])/arc_max)
|
|
total_ticks = float(arc_perc)*GRAPH_WIDTH
|
|
data_ticks = data_perc*GRAPH_WIDTH
|
|
meta_ticks = meta_perc*GRAPH_WIDTH
|
|
dnode_ticks = dnode_perc*GRAPH_WIDTH
|
|
other_ticks = total_ticks-(data_ticks+meta_ticks+dnode_ticks)
|
|
|
|
core_form = 'D'*int(data_ticks)+'M'*int(meta_ticks)+'N'*int(dnode_ticks)+\
|
|
'O'*int(other_ticks)
|
|
core_spc = ' '*(GRAPH_WIDTH-(2+len(core_form)))
|
|
core_line = GRAPH_INDENT+'|'+core_form+core_spc+'|'
|
|
|
|
for line in ('', info_line, graph_line, core_line, graph_line, ''):
|
|
print(line)
|
|
|
|
|
|
def f_bytes(byte_string):
|
|
"""Return human-readable representation of a byte value in
|
|
powers of 2 (eg "KiB" for "kibibytes", etc) to two decimal
|
|
points. Values smaller than one KiB are returned without
|
|
decimal points. Note "bytes" is a reserved keyword.
|
|
"""
|
|
|
|
prefixes = ([2**80, "YiB"], # yobibytes (yotta)
|
|
[2**70, "ZiB"], # zebibytes (zetta)
|
|
[2**60, "EiB"], # exbibytes (exa)
|
|
[2**50, "PiB"], # pebibytes (peta)
|
|
[2**40, "TiB"], # tebibytes (tera)
|
|
[2**30, "GiB"], # gibibytes (giga)
|
|
[2**20, "MiB"], # mebibytes (mega)
|
|
[2**10, "KiB"]) # kibibytes (kilo)
|
|
|
|
bites = int(byte_string)
|
|
|
|
if bites >= 2**10:
|
|
for limit, unit in prefixes:
|
|
|
|
if bites >= limit:
|
|
value = bites / limit
|
|
break
|
|
|
|
result = '{0:.1f} {1}'.format(value, unit)
|
|
else:
|
|
result = '{0} Bytes'.format(bites)
|
|
|
|
return result
|
|
|
|
|
|
def f_hits(hits_string):
|
|
"""Create a human-readable representation of the number of hits.
|
|
The single-letter symbols used are SI to avoid the confusion caused
|
|
by the different "short scale" and "long scale" representations in
|
|
English, which use the same words for different values. See
|
|
https://en.wikipedia.org/wiki/Names_of_large_numbers and:
|
|
https://physics.nist.gov/cuu/Units/prefixes.html
|
|
"""
|
|
|
|
numbers = ([10**24, 'Y'], # yotta (septillion)
|
|
[10**21, 'Z'], # zetta (sextillion)
|
|
[10**18, 'E'], # exa (quintrillion)
|
|
[10**15, 'P'], # peta (quadrillion)
|
|
[10**12, 'T'], # tera (trillion)
|
|
[10**9, 'G'], # giga (billion)
|
|
[10**6, 'M'], # mega (million)
|
|
[10**3, 'k']) # kilo (thousand)
|
|
|
|
hits = int(hits_string)
|
|
|
|
if hits >= 1000:
|
|
for limit, symbol in numbers:
|
|
|
|
if hits >= limit:
|
|
value = hits/limit
|
|
break
|
|
|
|
result = "%0.1f%s" % (value, symbol)
|
|
else:
|
|
result = "%d" % hits
|
|
|
|
return result
|
|
|
|
|
|
def f_perc(value1, value2):
|
|
"""Calculate percentage and return in human-readable form. If
|
|
rounding produces the result '0.0' though the first number is
|
|
not zero, include a 'less-than' symbol to avoid confusion.
|
|
Division by zero is handled by returning 'n/a'; no error
|
|
is called.
|
|
"""
|
|
|
|
v1 = float(value1)
|
|
v2 = float(value2)
|
|
|
|
try:
|
|
perc = 100 * v1/v2
|
|
except ZeroDivisionError:
|
|
result = 'n/a'
|
|
else:
|
|
result = '{0:0.1f} %'.format(perc)
|
|
|
|
if result == '0.0 %' and v1 > 0:
|
|
result = '< 0.1 %'
|
|
|
|
return result
|
|
|
|
|
|
def format_raw_line(name, value):
|
|
"""For the --raw option for the tunable and SPL outputs, decide on the
|
|
correct formatting based on the --alternate flag.
|
|
"""
|
|
|
|
if ARGS.alt:
|
|
result = '{0}{1}={2}'.format(INDENT, name, value)
|
|
else:
|
|
# Right-align the value within the line length if it fits,
|
|
# otherwise just separate it from the name by a single space.
|
|
fit = LINE_LENGTH - len(INDENT) - len(name)
|
|
overflow = len(value) + 1
|
|
w = max(fit, overflow)
|
|
result = '{0}{1}{2:>{w}}'.format(INDENT, name, value, w=w)
|
|
|
|
return result
|
|
|
|
|
|
def get_kstats():
|
|
"""Collect information on the ZFS subsystem. The step does not perform any
|
|
further processing, giving us the option to only work on what is actually
|
|
needed. The name "kstat" is a holdover from the Solaris utility of the same
|
|
name.
|
|
"""
|
|
|
|
result = {}
|
|
|
|
for section in SECTION_PATHS.values():
|
|
if section not in result:
|
|
result[section] = load_kstats(section)
|
|
|
|
return result
|
|
|
|
|
|
def get_version(request):
|
|
"""Get the version number of ZFS or SPL on this machine for header.
|
|
Returns an error string, but does not raise an error, if we can't
|
|
get the ZFS/SPL version.
|
|
"""
|
|
|
|
if request not in ('spl', 'zfs'):
|
|
error_msg = '(ERROR: "{0}" requested)'.format(request)
|
|
return error_msg
|
|
|
|
return get_version_impl(request)
|
|
|
|
|
|
def print_header():
|
|
"""Print the initial heading with date and time as well as info on the
|
|
kernel and ZFS versions. This is not called for the graph.
|
|
"""
|
|
|
|
# datetime is now recommended over time but we keep the exact formatting
|
|
# from the older version of arc_summary in case there are scripts
|
|
# that expect it in this way
|
|
daydate = time.strftime(DATE_FORMAT)
|
|
spc_date = LINE_LENGTH-len(daydate)
|
|
sys_version = os.uname()
|
|
|
|
sys_msg = sys_version.sysname+' '+sys_version.release
|
|
zfs = get_version('zfs')
|
|
spc_zfs = LINE_LENGTH-len(zfs)
|
|
|
|
machine_msg = 'Machine: '+sys_version.nodename+' ('+sys_version.machine+')'
|
|
spl = get_version('spl')
|
|
spc_spl = LINE_LENGTH-len(spl)
|
|
|
|
print('\n'+('-'*LINE_LENGTH))
|
|
print('{0:<{spc}}{1}'.format(TITLE, daydate, spc=spc_date))
|
|
print('{0:<{spc}}{1}'.format(sys_msg, zfs, spc=spc_zfs))
|
|
print('{0:<{spc}}{1}\n'.format(machine_msg, spl, spc=spc_spl))
|
|
|
|
|
|
def print_raw(kstats_dict):
|
|
"""Print all available data from the system in a minimally sorted format.
|
|
This can be used as a source to be piped through 'grep'.
|
|
"""
|
|
|
|
sections = sorted(kstats_dict.keys())
|
|
|
|
for section in sections:
|
|
|
|
print('\n{0}:'.format(section.upper()))
|
|
lines = sorted(kstats_dict[section])
|
|
|
|
for line in lines:
|
|
name, value = cleanup_line(line)
|
|
print(format_raw_line(name, value))
|
|
|
|
# Tunables and SPL must be handled separately because they come from a
|
|
# different source and have descriptions the user might request
|
|
print()
|
|
section_spl()
|
|
section_tunables()
|
|
|
|
|
|
def isolate_section(section_name, kstats_dict):
|
|
"""From the complete information on all sections, retrieve only those
|
|
for one section.
|
|
"""
|
|
|
|
try:
|
|
section_data = kstats_dict[section_name]
|
|
except KeyError:
|
|
print('ERROR: Data on {0} not available'.format(section_data))
|
|
sys.exit(1)
|
|
|
|
section_dict = dict(cleanup_line(l) for l in section_data)
|
|
|
|
return section_dict
|
|
|
|
|
|
# Formatted output helper functions
|
|
|
|
|
|
def prt_1(text, value):
|
|
"""Print text and one value, no indent"""
|
|
spc = ' '*(LINE_LENGTH-(len(text)+len(value)))
|
|
print('{0}{spc}{1}'.format(text, value, spc=spc))
|
|
|
|
|
|
def prt_i1(text, value):
|
|
"""Print text and one value, with indent"""
|
|
spc = ' '*(LINE_LENGTH-(len(INDENT)+len(text)+len(value)))
|
|
print(INDENT+'{0}{spc}{1}'.format(text, value, spc=spc))
|
|
|
|
|
|
def prt_2(text, value1, value2):
|
|
"""Print text and two values, no indent"""
|
|
values = '{0:>9} {1:>9}'.format(value1, value2)
|
|
spc = ' '*(LINE_LENGTH-(len(text)+len(values)+2))
|
|
print('{0}{spc} {1}'.format(text, values, spc=spc))
|
|
|
|
|
|
def prt_i2(text, value1, value2):
|
|
"""Print text and two values, with indent"""
|
|
values = '{0:>9} {1:>9}'.format(value1, value2)
|
|
spc = ' '*(LINE_LENGTH-(len(INDENT)+len(text)+len(values)+2))
|
|
print(INDENT+'{0}{spc} {1}'.format(text, values, spc=spc))
|
|
|
|
|
|
# The section output concentrates on important parameters instead of
|
|
# being exhaustive (that is what the --raw parameter is for)
|
|
|
|
|
|
def section_arc(kstats_dict):
|
|
"""Give basic information on the ARC, MRU and MFU. This is the first
|
|
and most used section.
|
|
"""
|
|
|
|
arc_stats = isolate_section('arcstats', kstats_dict)
|
|
|
|
memory_all = arc_stats['memory_all_bytes']
|
|
memory_free = arc_stats['memory_free_bytes']
|
|
memory_avail = arc_stats['memory_available_bytes']
|
|
arc_size = arc_stats['size']
|
|
arc_target_size = arc_stats['c']
|
|
arc_max = arc_stats['c_max']
|
|
arc_min = arc_stats['c_min']
|
|
dnode_limit = arc_stats['arc_dnode_limit']
|
|
|
|
print('ARC status:')
|
|
prt_i1('Total memory size:', f_bytes(memory_all))
|
|
prt_i2('Min target size:', f_perc(arc_min, memory_all), f_bytes(arc_min))
|
|
prt_i2('Max target size:', f_perc(arc_max, memory_all), f_bytes(arc_max))
|
|
prt_i2('Target size (adaptive):',
|
|
f_perc(arc_size, arc_max), f_bytes(arc_target_size))
|
|
prt_i2('Current size:', f_perc(arc_size, arc_max), f_bytes(arc_size))
|
|
prt_i1('Free memory size:', f_bytes(memory_free))
|
|
prt_i1('Available memory size:', f_bytes(memory_avail))
|
|
print()
|
|
|
|
compressed_size = arc_stats['compressed_size']
|
|
overhead_size = arc_stats['overhead_size']
|
|
bonus_size = arc_stats['bonus_size']
|
|
dnode_size = arc_stats['dnode_size']
|
|
dbuf_size = arc_stats['dbuf_size']
|
|
hdr_size = arc_stats['hdr_size']
|
|
l2_hdr_size = arc_stats['l2_hdr_size']
|
|
abd_chunk_waste_size = arc_stats['abd_chunk_waste_size']
|
|
|
|
prt_1('ARC structal breakdown (current size):', f_bytes(arc_size))
|
|
prt_i2('Compressed size:',
|
|
f_perc(compressed_size, arc_size), f_bytes(compressed_size))
|
|
prt_i2('Overhead size:',
|
|
f_perc(overhead_size, arc_size), f_bytes(overhead_size))
|
|
prt_i2('Bonus size:',
|
|
f_perc(bonus_size, arc_size), f_bytes(bonus_size))
|
|
prt_i2('Dnode size:',
|
|
f_perc(dnode_size, arc_size), f_bytes(dnode_size))
|
|
prt_i2('Dbuf size:',
|
|
f_perc(dbuf_size, arc_size), f_bytes(dbuf_size))
|
|
prt_i2('Header size:',
|
|
f_perc(hdr_size, arc_size), f_bytes(hdr_size))
|
|
prt_i2('L2 header size:',
|
|
f_perc(l2_hdr_size, arc_size), f_bytes(l2_hdr_size))
|
|
prt_i2('ABD chunk waste size:',
|
|
f_perc(abd_chunk_waste_size, arc_size), f_bytes(abd_chunk_waste_size))
|
|
print()
|
|
|
|
meta = arc_stats['meta']
|
|
pd = arc_stats['pd']
|
|
pm = arc_stats['pm']
|
|
data_size = arc_stats['data_size']
|
|
metadata_size = arc_stats['metadata_size']
|
|
anon_data = arc_stats['anon_data']
|
|
anon_metadata = arc_stats['anon_metadata']
|
|
mfu_data = arc_stats['mfu_data']
|
|
mfu_metadata = arc_stats['mfu_metadata']
|
|
mfu_edata = arc_stats['mfu_evictable_data']
|
|
mfu_emetadata = arc_stats['mfu_evictable_metadata']
|
|
mru_data = arc_stats['mru_data']
|
|
mru_metadata = arc_stats['mru_metadata']
|
|
mru_edata = arc_stats['mru_evictable_data']
|
|
mru_emetadata = arc_stats['mru_evictable_metadata']
|
|
mfug_data = arc_stats['mfu_ghost_data']
|
|
mfug_metadata = arc_stats['mfu_ghost_metadata']
|
|
mrug_data = arc_stats['mru_ghost_data']
|
|
mrug_metadata = arc_stats['mru_ghost_metadata']
|
|
unc_data = arc_stats['uncached_data']
|
|
unc_metadata = arc_stats['uncached_metadata']
|
|
caches_size = int(anon_data)+int(anon_metadata)+\
|
|
int(mfu_data)+int(mfu_metadata)+int(mru_data)+int(mru_metadata)+\
|
|
int(unc_data)+int(unc_metadata)
|
|
|
|
prt_1('ARC types breakdown (compressed + overhead):', f_bytes(caches_size))
|
|
prt_i2('Data size:',
|
|
f_perc(data_size, caches_size), f_bytes(data_size))
|
|
prt_i2('Metadata size:',
|
|
f_perc(metadata_size, caches_size), f_bytes(metadata_size))
|
|
print()
|
|
|
|
prt_1('ARC states breakdown (compressed + overhead):', f_bytes(caches_size))
|
|
prt_i2('Anonymous data size:',
|
|
f_perc(anon_data, caches_size), f_bytes(anon_data))
|
|
prt_i2('Anonymous metadata size:',
|
|
f_perc(anon_metadata, caches_size), f_bytes(anon_metadata))
|
|
s = 4294967296
|
|
v = (s-int(pd))*(s-int(meta))/s
|
|
prt_i2('MFU data target:', f_perc(v, s),
|
|
f_bytes(v / 65536 * caches_size / 65536))
|
|
prt_i2('MFU data size:',
|
|
f_perc(mfu_data, caches_size), f_bytes(mfu_data))
|
|
prt_i2('MFU evictable data size:',
|
|
f_perc(mfu_edata, caches_size), f_bytes(mfu_edata))
|
|
prt_i1('MFU ghost data size:', f_bytes(mfug_data))
|
|
v = (s-int(pm))*int(meta)/s
|
|
prt_i2('MFU metadata target:', f_perc(v, s),
|
|
f_bytes(v / 65536 * caches_size / 65536))
|
|
prt_i2('MFU metadata size:',
|
|
f_perc(mfu_metadata, caches_size), f_bytes(mfu_metadata))
|
|
prt_i2('MFU evictable metadata size:',
|
|
f_perc(mfu_emetadata, caches_size), f_bytes(mfu_emetadata))
|
|
prt_i1('MFU ghost metadata size:', f_bytes(mfug_metadata))
|
|
v = int(pd)*(s-int(meta))/s
|
|
prt_i2('MRU data target:', f_perc(v, s),
|
|
f_bytes(v / 65536 * caches_size / 65536))
|
|
prt_i2('MRU data size:',
|
|
f_perc(mru_data, caches_size), f_bytes(mru_data))
|
|
prt_i2('MRU evictable data size:',
|
|
f_perc(mru_edata, caches_size), f_bytes(mru_edata))
|
|
prt_i1('MRU ghost data size:', f_bytes(mrug_data))
|
|
v = int(pm)*int(meta)/s
|
|
prt_i2('MRU metadata target:', f_perc(v, s),
|
|
f_bytes(v / 65536 * caches_size / 65536))
|
|
prt_i2('MRU metadata size:',
|
|
f_perc(mru_metadata, caches_size), f_bytes(mru_metadata))
|
|
prt_i2('MRU evictable metadata size:',
|
|
f_perc(mru_emetadata, caches_size), f_bytes(mru_emetadata))
|
|
prt_i1('MRU ghost metadata size:', f_bytes(mrug_metadata))
|
|
prt_i2('Uncached data size:',
|
|
f_perc(unc_data, caches_size), f_bytes(unc_data))
|
|
prt_i2('Uncached metadata size:',
|
|
f_perc(unc_metadata, caches_size), f_bytes(unc_metadata))
|
|
print()
|
|
|
|
print('ARC hash breakdown:')
|
|
prt_i1('Elements max:', f_hits(arc_stats['hash_elements_max']))
|
|
prt_i2('Elements current:',
|
|
f_perc(arc_stats['hash_elements'], arc_stats['hash_elements_max']),
|
|
f_hits(arc_stats['hash_elements']))
|
|
prt_i1('Collisions:', f_hits(arc_stats['hash_collisions']))
|
|
|
|
prt_i1('Chain max:', f_hits(arc_stats['hash_chain_max']))
|
|
prt_i1('Chains:', f_hits(arc_stats['hash_chains']))
|
|
print()
|
|
|
|
print('ARC misc:')
|
|
prt_i1('Memory throttles:', arc_stats['memory_throttle_count'])
|
|
prt_i1('Memory direct reclaims:', arc_stats['memory_direct_count'])
|
|
prt_i1('Memory indirect reclaims:', arc_stats['memory_indirect_count'])
|
|
prt_i1('Deleted:', f_hits(arc_stats['deleted']))
|
|
prt_i1('Mutex misses:', f_hits(arc_stats['mutex_miss']))
|
|
prt_i1('Eviction skips:', f_hits(arc_stats['evict_skip']))
|
|
prt_i1('Eviction skips due to L2 writes:',
|
|
f_hits(arc_stats['evict_l2_skip']))
|
|
prt_i1('L2 cached evictions:', f_bytes(arc_stats['evict_l2_cached']))
|
|
prt_i1('L2 eligible evictions:', f_bytes(arc_stats['evict_l2_eligible']))
|
|
prt_i2('L2 eligible MFU evictions:',
|
|
f_perc(arc_stats['evict_l2_eligible_mfu'],
|
|
arc_stats['evict_l2_eligible']),
|
|
f_bytes(arc_stats['evict_l2_eligible_mfu']))
|
|
prt_i2('L2 eligible MRU evictions:',
|
|
f_perc(arc_stats['evict_l2_eligible_mru'],
|
|
arc_stats['evict_l2_eligible']),
|
|
f_bytes(arc_stats['evict_l2_eligible_mru']))
|
|
prt_i1('L2 ineligible evictions:',
|
|
f_bytes(arc_stats['evict_l2_ineligible']))
|
|
print()
|
|
|
|
|
|
def section_archits(kstats_dict):
|
|
"""Print information on how the caches are accessed ("arc hits").
|
|
"""
|
|
|
|
arc_stats = isolate_section('arcstats', kstats_dict)
|
|
all_accesses = int(arc_stats['hits'])+int(arc_stats['iohits'])+\
|
|
int(arc_stats['misses'])
|
|
|
|
prt_1('ARC total accesses:', f_hits(all_accesses))
|
|
ta_todo = (('Total hits:', arc_stats['hits']),
|
|
('Total I/O hits:', arc_stats['iohits']),
|
|
('Total misses:', arc_stats['misses']))
|
|
for title, value in ta_todo:
|
|
prt_i2(title, f_perc(value, all_accesses), f_hits(value))
|
|
print()
|
|
|
|
dd_total = int(arc_stats['demand_data_hits']) +\
|
|
int(arc_stats['demand_data_iohits']) +\
|
|
int(arc_stats['demand_data_misses'])
|
|
prt_2('ARC demand data accesses:', f_perc(dd_total, all_accesses),
|
|
f_hits(dd_total))
|
|
dd_todo = (('Demand data hits:', arc_stats['demand_data_hits']),
|
|
('Demand data I/O hits:', arc_stats['demand_data_iohits']),
|
|
('Demand data misses:', arc_stats['demand_data_misses']))
|
|
for title, value in dd_todo:
|
|
prt_i2(title, f_perc(value, dd_total), f_hits(value))
|
|
print()
|
|
|
|
dm_total = int(arc_stats['demand_metadata_hits']) +\
|
|
int(arc_stats['demand_metadata_iohits']) +\
|
|
int(arc_stats['demand_metadata_misses'])
|
|
prt_2('ARC demand metadata accesses:', f_perc(dm_total, all_accesses),
|
|
f_hits(dm_total))
|
|
dm_todo = (('Demand metadata hits:', arc_stats['demand_metadata_hits']),
|
|
('Demand metadata I/O hits:',
|
|
arc_stats['demand_metadata_iohits']),
|
|
('Demand metadata misses:', arc_stats['demand_metadata_misses']))
|
|
for title, value in dm_todo:
|
|
prt_i2(title, f_perc(value, dm_total), f_hits(value))
|
|
print()
|
|
|
|
pd_total = int(arc_stats['prefetch_data_hits']) +\
|
|
int(arc_stats['prefetch_data_iohits']) +\
|
|
int(arc_stats['prefetch_data_misses'])
|
|
prt_2('ARC prefetch data accesses:', f_perc(pd_total, all_accesses),
|
|
f_hits(pd_total))
|
|
pd_todo = (('Prefetch data hits:', arc_stats['prefetch_data_hits']),
|
|
('Prefetch data I/O hits:', arc_stats['prefetch_data_iohits']),
|
|
('Prefetch data misses:', arc_stats['prefetch_data_misses']))
|
|
for title, value in pd_todo:
|
|
prt_i2(title, f_perc(value, pd_total), f_hits(value))
|
|
print()
|
|
|
|
pm_total = int(arc_stats['prefetch_metadata_hits']) +\
|
|
int(arc_stats['prefetch_metadata_iohits']) +\
|
|
int(arc_stats['prefetch_metadata_misses'])
|
|
prt_2('ARC prefetch metadata accesses:', f_perc(pm_total, all_accesses),
|
|
f_hits(pm_total))
|
|
pm_todo = (('Prefetch metadata hits:',
|
|
arc_stats['prefetch_metadata_hits']),
|
|
('Prefetch metadata I/O hits:',
|
|
arc_stats['prefetch_metadata_iohits']),
|
|
('Prefetch metadata misses:',
|
|
arc_stats['prefetch_metadata_misses']))
|
|
for title, value in pm_todo:
|
|
prt_i2(title, f_perc(value, pm_total), f_hits(value))
|
|
print()
|
|
|
|
all_prefetches = int(arc_stats['predictive_prefetch'])+\
|
|
int(arc_stats['prescient_prefetch'])
|
|
prt_2('ARC predictive prefetches:',
|
|
f_perc(arc_stats['predictive_prefetch'], all_prefetches),
|
|
f_hits(arc_stats['predictive_prefetch']))
|
|
prt_i2('Demand hits after predictive:',
|
|
f_perc(arc_stats['demand_hit_predictive_prefetch'],
|
|
arc_stats['predictive_prefetch']),
|
|
f_hits(arc_stats['demand_hit_predictive_prefetch']))
|
|
prt_i2('Demand I/O hits after predictive:',
|
|
f_perc(arc_stats['demand_iohit_predictive_prefetch'],
|
|
arc_stats['predictive_prefetch']),
|
|
f_hits(arc_stats['demand_iohit_predictive_prefetch']))
|
|
never = int(arc_stats['predictive_prefetch']) -\
|
|
int(arc_stats['demand_hit_predictive_prefetch']) -\
|
|
int(arc_stats['demand_iohit_predictive_prefetch'])
|
|
prt_i2('Never demanded after predictive:',
|
|
f_perc(never, arc_stats['predictive_prefetch']),
|
|
f_hits(never))
|
|
print()
|
|
|
|
prt_2('ARC prescient prefetches:',
|
|
f_perc(arc_stats['prescient_prefetch'], all_prefetches),
|
|
f_hits(arc_stats['prescient_prefetch']))
|
|
prt_i2('Demand hits after prescient:',
|
|
f_perc(arc_stats['demand_hit_prescient_prefetch'],
|
|
arc_stats['prescient_prefetch']),
|
|
f_hits(arc_stats['demand_hit_prescient_prefetch']))
|
|
prt_i2('Demand I/O hits after prescient:',
|
|
f_perc(arc_stats['demand_iohit_prescient_prefetch'],
|
|
arc_stats['prescient_prefetch']),
|
|
f_hits(arc_stats['demand_iohit_prescient_prefetch']))
|
|
never = int(arc_stats['prescient_prefetch'])-\
|
|
int(arc_stats['demand_hit_prescient_prefetch'])-\
|
|
int(arc_stats['demand_iohit_prescient_prefetch'])
|
|
prt_i2('Never demanded after prescient:',
|
|
f_perc(never, arc_stats['prescient_prefetch']),
|
|
f_hits(never))
|
|
print()
|
|
|
|
print('ARC states hits of all accesses:')
|
|
cl_todo = (('Most frequently used (MFU):', arc_stats['mfu_hits']),
|
|
('Most recently used (MRU):', arc_stats['mru_hits']),
|
|
('Most frequently used (MFU) ghost:',
|
|
arc_stats['mfu_ghost_hits']),
|
|
('Most recently used (MRU) ghost:',
|
|
arc_stats['mru_ghost_hits']),
|
|
('Uncached:', arc_stats['uncached_hits']))
|
|
for title, value in cl_todo:
|
|
prt_i2(title, f_perc(value, all_accesses), f_hits(value))
|
|
print()
|
|
|
|
|
|
def section_dmu(kstats_dict):
|
|
"""Collect information on the DMU"""
|
|
|
|
zfetch_stats = isolate_section('zfetchstats', kstats_dict)
|
|
|
|
zfetch_access_total = int(zfetch_stats['hits']) +\
|
|
int(zfetch_stats['future']) + int(zfetch_stats['stride']) +\
|
|
int(zfetch_stats['past']) + int(zfetch_stats['misses'])
|
|
|
|
prt_1('DMU predictive prefetcher calls:', f_hits(zfetch_access_total))
|
|
prt_i2('Stream hits:',
|
|
f_perc(zfetch_stats['hits'], zfetch_access_total),
|
|
f_hits(zfetch_stats['hits']))
|
|
future = int(zfetch_stats['future']) + int(zfetch_stats['stride'])
|
|
prt_i2('Hits ahead of stream:', f_perc(future, zfetch_access_total),
|
|
f_hits(future))
|
|
prt_i2('Hits behind stream:',
|
|
f_perc(zfetch_stats['past'], zfetch_access_total),
|
|
f_hits(zfetch_stats['past']))
|
|
prt_i2('Stream misses:',
|
|
f_perc(zfetch_stats['misses'], zfetch_access_total),
|
|
f_hits(zfetch_stats['misses']))
|
|
prt_i2('Streams limit reached:',
|
|
f_perc(zfetch_stats['max_streams'], zfetch_stats['misses']),
|
|
f_hits(zfetch_stats['max_streams']))
|
|
prt_i1('Stream strides:', f_hits(zfetch_stats['stride']))
|
|
prt_i1('Prefetches issued', f_hits(zfetch_stats['io_issued']))
|
|
print()
|
|
|
|
|
|
def section_l2arc(kstats_dict):
|
|
"""Collect information on L2ARC device if present. If not, tell user
|
|
that we're skipping the section.
|
|
"""
|
|
|
|
# The L2ARC statistics live in the same section as the normal ARC stuff
|
|
arc_stats = isolate_section('arcstats', kstats_dict)
|
|
|
|
if arc_stats['l2_size'] == '0':
|
|
print('L2ARC not detected, skipping section\n')
|
|
return
|
|
|
|
l2_errors = int(arc_stats['l2_writes_error']) +\
|
|
int(arc_stats['l2_cksum_bad']) +\
|
|
int(arc_stats['l2_io_error'])
|
|
|
|
l2_access_total = int(arc_stats['l2_hits'])+int(arc_stats['l2_misses'])
|
|
health = 'HEALTHY'
|
|
|
|
if l2_errors > 0:
|
|
health = 'DEGRADED'
|
|
|
|
prt_1('L2ARC status:', health)
|
|
|
|
l2_todo = (('Low memory aborts:', 'l2_abort_lowmem'),
|
|
('Free on write:', 'l2_free_on_write'),
|
|
('R/W clashes:', 'l2_rw_clash'),
|
|
('Bad checksums:', 'l2_cksum_bad'),
|
|
('Read errors:', 'l2_io_error'),
|
|
('Write errors:', 'l2_writes_error'))
|
|
|
|
for title, value in l2_todo:
|
|
prt_i1(title, f_hits(arc_stats[value]))
|
|
|
|
print()
|
|
prt_1('L2ARC size (adaptive):', f_bytes(arc_stats['l2_size']))
|
|
prt_i2('Compressed:', f_perc(arc_stats['l2_asize'], arc_stats['l2_size']),
|
|
f_bytes(arc_stats['l2_asize']))
|
|
prt_i2('Header size:',
|
|
f_perc(arc_stats['l2_hdr_size'], arc_stats['l2_size']),
|
|
f_bytes(arc_stats['l2_hdr_size']))
|
|
prt_i2('MFU allocated size:',
|
|
f_perc(arc_stats['l2_mfu_asize'], arc_stats['l2_asize']),
|
|
f_bytes(arc_stats['l2_mfu_asize']))
|
|
prt_i2('MRU allocated size:',
|
|
f_perc(arc_stats['l2_mru_asize'], arc_stats['l2_asize']),
|
|
f_bytes(arc_stats['l2_mru_asize']))
|
|
prt_i2('Prefetch allocated size:',
|
|
f_perc(arc_stats['l2_prefetch_asize'], arc_stats['l2_asize']),
|
|
f_bytes(arc_stats['l2_prefetch_asize']))
|
|
prt_i2('Data (buffer content) allocated size:',
|
|
f_perc(arc_stats['l2_bufc_data_asize'], arc_stats['l2_asize']),
|
|
f_bytes(arc_stats['l2_bufc_data_asize']))
|
|
prt_i2('Metadata (buffer content) allocated size:',
|
|
f_perc(arc_stats['l2_bufc_metadata_asize'], arc_stats['l2_asize']),
|
|
f_bytes(arc_stats['l2_bufc_metadata_asize']))
|
|
|
|
print()
|
|
prt_1('L2ARC breakdown:', f_hits(l2_access_total))
|
|
prt_i2('Hit ratio:',
|
|
f_perc(arc_stats['l2_hits'], l2_access_total),
|
|
f_hits(arc_stats['l2_hits']))
|
|
prt_i2('Miss ratio:',
|
|
f_perc(arc_stats['l2_misses'], l2_access_total),
|
|
f_hits(arc_stats['l2_misses']))
|
|
|
|
print()
|
|
print('L2ARC I/O:')
|
|
prt_i2('Reads:',
|
|
f_bytes(arc_stats['l2_read_bytes']),
|
|
f_hits(arc_stats['l2_hits']))
|
|
prt_i2('Writes:',
|
|
f_bytes(arc_stats['l2_write_bytes']),
|
|
f_hits(arc_stats['l2_writes_sent']))
|
|
|
|
print()
|
|
print('L2ARC evicts:')
|
|
prt_i1('L1 cached:', f_hits(arc_stats['l2_evict_l1cached']))
|
|
prt_i1('While reading:', f_hits(arc_stats['l2_evict_reading']))
|
|
print()
|
|
|
|
|
|
def section_spl(*_):
|
|
"""Print the SPL parameters, if requested with alternative format
|
|
and/or descriptions. This does not use kstats.
|
|
"""
|
|
|
|
if sys.platform.startswith('freebsd'):
|
|
# No SPL support in FreeBSD
|
|
return
|
|
|
|
spls = get_spl_params()
|
|
keylist = sorted(spls.keys())
|
|
print('Solaris Porting Layer (SPL):')
|
|
|
|
if ARGS.desc:
|
|
descriptions = get_descriptions('spl')
|
|
|
|
for key in keylist:
|
|
value = spls[key]
|
|
|
|
if ARGS.desc:
|
|
try:
|
|
print(INDENT+'#', descriptions[key])
|
|
except KeyError:
|
|
print(INDENT+'# (No description found)') # paranoid
|
|
|
|
print(format_raw_line(key, value))
|
|
|
|
print()
|
|
|
|
|
|
def section_tunables(*_):
|
|
"""Print the tunables, if requested with alternative format and/or
|
|
descriptions. This does not use kstasts.
|
|
"""
|
|
|
|
tunables = get_tunable_params()
|
|
keylist = sorted(tunables.keys())
|
|
print('Tunables:')
|
|
|
|
if ARGS.desc:
|
|
descriptions = get_descriptions('zfs')
|
|
|
|
for key in keylist:
|
|
value = tunables[key]
|
|
|
|
if ARGS.desc:
|
|
try:
|
|
print(INDENT+'#', descriptions[key])
|
|
except KeyError:
|
|
print(INDENT+'# (No description found)') # paranoid
|
|
|
|
print(format_raw_line(key, value))
|
|
|
|
print()
|
|
|
|
|
|
def section_zil(kstats_dict):
|
|
"""Collect information on the ZFS Intent Log. Some of the information
|
|
taken from https://github.com/openzfs/zfs/blob/master/include/sys/zil.h
|
|
"""
|
|
|
|
zil_stats = isolate_section('zil', kstats_dict)
|
|
|
|
prt_1('ZIL committed transactions:',
|
|
f_hits(zil_stats['zil_itx_count']))
|
|
prt_i1('Commit requests:', f_hits(zil_stats['zil_commit_count']))
|
|
prt_i1('Flushes to stable storage:',
|
|
f_hits(zil_stats['zil_commit_writer_count']))
|
|
prt_i2('Transactions to SLOG storage pool:',
|
|
f_bytes(zil_stats['zil_itx_metaslab_slog_bytes']),
|
|
f_hits(zil_stats['zil_itx_metaslab_slog_count']))
|
|
prt_i2('Transactions to non-SLOG storage pool:',
|
|
f_bytes(zil_stats['zil_itx_metaslab_normal_bytes']),
|
|
f_hits(zil_stats['zil_itx_metaslab_normal_count']))
|
|
print()
|
|
|
|
|
|
section_calls = {'arc': section_arc,
|
|
'archits': section_archits,
|
|
'dmu': section_dmu,
|
|
'l2arc': section_l2arc,
|
|
'spl': section_spl,
|
|
'tunables': section_tunables,
|
|
'zil': section_zil}
|
|
|
|
|
|
def main():
|
|
"""Run program. The options to draw a graph and to print all data raw are
|
|
treated separately because they come with their own call.
|
|
"""
|
|
|
|
kstats = get_kstats()
|
|
|
|
if ARGS.graph:
|
|
draw_graph(kstats)
|
|
sys.exit(0)
|
|
|
|
print_header()
|
|
|
|
if ARGS.raw:
|
|
print_raw(kstats)
|
|
|
|
elif ARGS.section:
|
|
|
|
try:
|
|
section_calls[ARGS.section](kstats)
|
|
except KeyError:
|
|
print('Error: Section "{0}" unknown'.format(ARGS.section))
|
|
sys.exit(1)
|
|
|
|
elif ARGS.page:
|
|
print('WARNING: Pages are deprecated, please use "--section"\n')
|
|
|
|
pages_to_calls = {1: 'arc',
|
|
2: 'archits',
|
|
3: 'l2arc',
|
|
4: 'dmu',
|
|
5: 'vdev',
|
|
6: 'tunables'}
|
|
|
|
try:
|
|
call = pages_to_calls[ARGS.page]
|
|
except KeyError:
|
|
print('Error: Page "{0}" not supported'.format(ARGS.page))
|
|
sys.exit(1)
|
|
else:
|
|
section_calls[call](kstats)
|
|
|
|
else:
|
|
# If no parameters were given, we print all sections. We might want to
|
|
# change the sequence by hand
|
|
calls = sorted(section_calls.keys())
|
|
|
|
for section in calls:
|
|
section_calls[section](kstats)
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|