diff --git a/.gitignore b/.gitignore index f8b7f6f834827cd5670c95645e845000a1e856ca..e42641f290d11a1746393cf344cabe088c0672b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ __pycache__/ *.pyc .kdev4/ *.kdev4 +cpunetlog.egg-info/ +dist/ +build/ diff --git a/LICENSE.txt b/LICENSE.txt index 270393d3e7d5a385a55c7c81690c354e28774ef2..91311b9f1afe4df1a56de828a9464c3f846c157d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -23,7 +23,7 @@ and is licensed under the "BSD 2-Clause License": -------------------------------------------------------------------------------- -Copyright (c) 2014, +Copyright (c) 2014-2018, Karlsruhe Institute of Technology, Institute of Telematics Redistribution and use in source and binary forms, with or without modification, diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..97bd2dd7539339fdc34eced8175c468e9c60a3ec --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +CPUnetLOG - Display, log and plot CPU utilization and network throughput. +================================================================================ +CPUnetLOG is an open source software that can: + +- Display +- Log +- Plot + +CPU utilization and network throughput. + +![CPUnetLOG screenshot](screenshot.png) + + +Installation of CPUnetLOG +-------------------------------------------------------------------------------- +* system-wide installation (from PIP): + * sudo pip3 install cpunetlog +* local installation (from PIP) (places binary in ~/.local/bin --> check your $PATH): + * pip3 install --user cpunetlog +* system-wide installation (from source repo): + * sudo pip3 install . +* local installation (from source repo) (places binary in ~/.local/bin --> check your $PATH): + * pip3 install --user . + + +Running CPUnetLOG +-------------------------------------------------------------------------------- +* ./cpunetlog.py OR (in source repo) +* cpunetlog (after installation via pip) + + +Requirements +-------------------------------------------------------------------------------- +CPUnetLOG is based on Python3. + +Requires the following python3 modules: +- psutil +- netifaces + +Installation on Ubuntu 16.04: +```bash +sudo apt-get install python3 +sudo apt-get install python3-psutil +sudo apt-get install python3-netifaces +``` diff --git a/README.txt b/README.txt deleted file mode 100644 index 986adce52c3b8e1a8b1cdff8e9cd604bcf1b6e21..0000000000000000000000000000000000000000 --- a/README.txt +++ /dev/null @@ -1,8 +0,0 @@ -CPUnetLOG ist based on Python3. - -## install (Ubuntu 16.04) -# Requirements: - -sudo apt-get install python3 -sudo apt-get install python3-psutil -sudo apt-get install python3-netifaces diff --git a/__init__.py b/__init__.py deleted file mode 100755 index bf80342a53ca3d0f934943d5edb883b89584494c..0000000000000000000000000000000000000000 --- a/__init__.py +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- - -# Copyright (c) 2014, -# Karlsruhe Institute of Technology, Institute of Telematics -# -# This code is provided under the BSD 2-Clause License. -# Please refer to the LICENSE.txt file for further information. -# -# Author: Mario Hock - - -import os -import psutil -import time -import sys -import traceback -import math -import json - -from collections import namedtuple - -import helpers -import curses_display as ui -from logging import LoggingManager -from psutil_functions import calculate_cpu_times_percent - - -def get_time(): - """ Unified/comparable clock access """ - return time.time() - - -## XXX for interactive debugging only -def RELOAD(): - print ("import importlib") - print ("importlib.reload(cpunetlog)") - - -MEASUREMENT_INTERVAL = 0.2 - -nic_speeds = helpers.get_nic_speeds() -nics = list( nic_speeds.keys() ) - - -class Reading: - """ A single reading of various CPU, NET, ... values. --> Building block for the »Measurement« class.""" - - def __init__(self): - ## * measurements * - self.timestamp = get_time() - #self.cpu_util = psutil.cpu_percent(interval=0, percpu=True) ## XXX - #self.cpu_times_percent = psutil.cpu_times_percent(interval=0, percpu=True) ## XXX - self.cpu_times = psutil.cpu_times(percpu=True) - self.memory = psutil.virtual_memory() - self.net_io = psutil.net_io_counters(pernic=True) - - def __str__(self): - ## •‣∘⁕∗◘☉☀★◾☞☛⦿ - return "◘ Timespan: " + str(self.timespan) + \ - "\n◘ CPU utilization: " + str(self.cpu_util) + \ - "\n◘ CPU times: " + str(self.cpu_times) + \ - "\n◘ RAM: " + str(self.memory) + \ - "\n◘ NET: " + str(self.net_io) - - - -class NetworkTraffic: - """ Utility class for calculating and storing network traffic: Total amount (during a timespan) and ratio. """ - - def __init__(self, older_counters, younger_counters, timespan): - self.total = dict() - self.ratio = dict() - - for field in older_counters._fields: - field_delta = getattr(younger_counters, field) - getattr(older_counters, field) - - self.total[field] = field_delta - self.ratio[field] = field_delta / timespan - - def __str__(self): - return "Total (bytes):" + str(self.total) + "; Ratio (bytes/s)" + str(self.ratio) - - - -class Measurement: - """ Calculates and stores CPU utilization, network traffic, ... during a timespan. Based two »Readings«. """ - - def __init__(self, reading1, reading2): - self.r1 = reading1 - self.r2 = reading2 - - ## calculate differences - self.timespan = self.r2.timestamp - self.r1.timestamp - self.cpu_times_percent = calculate_cpu_times_percent(self.r1.cpu_times, self.r2.cpu_times, percpu=True) - self.net_io = self._calculate_net_io() - - - def _calculate_net_io(self): - ret = dict() - - for nic in self.r1.net_io.keys(): - ret[nic] = NetworkTraffic(self.r1.net_io[nic], self.r2.net_io[nic], self.timespan) - - return ret - - def get_begin(self): - return self.r1.timestamp - - def get_end(self): - return self.r2.timestamp - - - -def measure(interval = MEASUREMENT_INTERVAL): - """ Convenience function to perform one »Measurement« """ - - r1 = Reading() - time.sleep(interval) - r2 = Reading() - - m = Measurement(r1, r2) - - return m - - - - -def main_loop(): - """ Main Loop: - - Sets up curses-display - - Takes a reading every second - - Displays the measurements - - Logs the measurements with the LoggingManager - """ - - ## TODO this should be configurable by command line options - sample_interval = float(args.interval) - display_interval = float(args.displayinterval) - - display_skip = max(display_interval / sample_interval, 1) - - err = None - - try: - # Set up (curses) UI. - ui.nics = nics - ui.nic_speeds = nic_speeds - ui.logging_manager = logging_manager - if not args.headless: - ui.init() - - # Take an initial reading. - old_reading = Reading() - - # Sleep till the next "full" second begins. (In order to roughly synchronize with other instances.) - now = time.time() - time.sleep(math.ceil(now)-now) - - display_skip_counter = 0 - running = True - while running: - # Take a new reading. - new_reading = Reading() - - # Calculate the measurement from the last two readings. - measurement = Measurement(old_reading, new_reading) - - # Display/log the measurement. - running &= logging_manager.log(measurement) - if ( display_skip_counter % display_skip < 1 ) and not args.headless: # the display may skip some samples - running = ui.display( measurement ) - display_skip_counter = 0 - display_skip_counter += 1 - - # Store the last reading as |old_reading|. - old_reading = new_reading - - time.sleep(sample_interval) - ## XXX TODO We could calculating the remaining waiting-time here. - - - - except KeyboardInterrupt: - # Quit gracefully on Ctrl-C - pass - except Exception as e: - # On error: Store stack trace for later processing. - err = e - exc_type, exc_value, exc_traceback = sys.exc_info() - finally: - # Tear down the UI. - if not args.headless: - ui.close() - logging_manager.close() - - ## On error: Print error message *after* curses has quit. - if ( err ): - print( "Unexpected exception happened: '" + str(err) + "'" ) - print - - traceback.print_exception(exc_type, exc_value, exc_traceback, file=sys.stdout) - - print - print( "QUIT." ) - - - - -## MAIN ## -if __name__ == "__main__": - - ## Command line arguments - import argparse -# - parser = argparse.ArgumentParser() - - ## Logging - parser.add_argument("-l", "--logging", action="store_true", - help="Enables logging.") - parser.add_argument("-A", "--autologging", action="store_true", - help="Enables auto-logging. (Log only on network activity. Implies --logging)") - parser.add_argument("-W", "--watch", - help="Store the command-line of the given program as log-comment. (Use together with --autologging.)") - parser.add_argument("-c", "--comment", - help="A comment that is stored in the logfile. (See --logging.)") - parser.add_argument("--path", default="/tmp/cpunetlog", - help="Path where the log files are stored in. (See --logging.)") - parser.add_argument("-e", "--environment", - help="JSON file that holds arbitrary environment context. (This can be seen as a structured comment field.)") - parser.add_argument("-i", "--interval", default="0.5", - help="Time between two samples (in seconds). [Default = 0.5]") - parser.add_argument("-d", "--displayinterval", default="1", - help="Time between two display updates (in seconds). [Default = 1]") - - - # NICs - parser.add_argument("--nics", nargs='+', - help="The network interfaces that should be displayed (and logged, see --logging).") - - parser.add_argument("-q", "--headless", action="store_true", - help="Run in quiet/headless mode without GUI") - - args = parser.parse_args() - - - - ## NICs - monitored_nics = nics - if ( args.nics ): - #assert( set(nics).issuperset(args.nics) ) - monitored_nics = args.nics - - ## --autologging implies --logging - if ( args.autologging ): - args.logging = True - - ## compensate psutil version incompatibilities - try: - num_cpus = psutil.cpu_count() - except: - num_cpus = psutil.NUM_CPUS - - ## Logging - logging_manager = LoggingManager( num_cpus, monitored_nics, helpers.get_sysinfo(), args.environment, - args.comment, args.path, args.autologging, args.watch ) - if args.logging: - logging_manager.enable_measurement_logger() - - - # Run the main loop. - main_loop() - diff --git a/__init__.py b/__init__.py new file mode 120000 index 0000000000000000000000000000000000000000..cd1ec8734c758d27e36a04909dca8b7ff2066eb8 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +cpunetlog.py \ No newline at end of file diff --git a/bash_aliases.txt b/bash_aliases.txt index ec5464edc75ab30645f21ffbfcc75b0617119c4b..936618862fb8e8aee655172005fc8a935548f265 100644 --- a/bash_aliases.txt +++ b/bash_aliases.txt @@ -2,4 +2,4 @@ BASE="" # <-- Please modify to fit your installation. -alias cpunetlog="$BASE/cpunetlog/__init__.py" +alias cpunetlog="$BASE/cpunetlog/cpunetlog.py" diff --git a/cpunetlog.py b/cpunetlog.py new file mode 100755 index 0000000000000000000000000000000000000000..2480dd11e12fe5c732935910834cf8e35451615b --- /dev/null +++ b/cpunetlog.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""Convenience wrapper for running CPUnetLOG directly from source tree.""" + +import sys +sys.settrace + +from cpunetlog.main import main + +if __name__ == '__main__': + main() diff --git a/cpunetlog/__init__.py b/cpunetlog/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fdffa2a0fd7bfbca016990e698d6ea642934bb55 --- /dev/null +++ b/cpunetlog/__init__.py @@ -0,0 +1 @@ +# placeholder diff --git a/cpunetlog/__main__.py b/cpunetlog/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..663ae287d0a397c30761d1103dd61aa385f6d53f --- /dev/null +++ b/cpunetlog/__main__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from cpunetlog.main import main +main() diff --git a/curses_display.py b/cpunetlog/curses_display.py similarity index 97% rename from curses_display.py rename to cpunetlog/curses_display.py index d35599ca8d55ad276a83f8d1eb10edb8818ba365..ce494e91036887900c297c5fcf28a8b48570f68f 100644 --- a/curses_display.py +++ b/cpunetlog/curses_display.py @@ -25,7 +25,7 @@ Curses display for »cpunetlog«. ''' import curses import time -import helpers +import cpunetlog.helpers ## XXX disable colors disablecolorskipped = True @@ -146,7 +146,7 @@ def _display_cpu_bar(y, x, cpu): # Prepare text. text = '{0:.2%}'.format((cpu_util)/100.0) - split_text = helpers.split_proprtionally(text, proportions, 20) + split_text = cpunetlog.helpers.split_proprtionally(text, proportions, 20) # Write text on screen (curses). stdscr.move(y,x) @@ -171,7 +171,7 @@ def init(): global nic_speeds if not nic_speeds: - nic_speeds = helpers.get_nic_speeds() + nic_speeds = cpunetlog.helpers.get_nic_speeds() stdscr = curses.initscr() curses.noecho() @@ -209,7 +209,7 @@ def init(): def display(measurement): try: return _display(measurement) - except: + except Exception as e: print( "\nDisplay Error! (Check terminal-size)" ) return True @@ -252,7 +252,7 @@ def _display(measurement): _display_cpu_bar( y, LABEL_CPU_UTIL+6, cpu ) # user/system - cpu_sorted = helpers.sort_named_tuple(cpu, skip="idle") + cpu_sorted = cpunetlog.helpers.sort_named_tuple(cpu, skip="idle") t = '{0: >8}'.format( CPU_TYPE_LABELS[cpu_sorted[0][0]] ) stdscr.addstr(y, LABEL_CPU_1, t, curses.color_pair(4)) stdscr.addstr("{:>5.2f}%".format(cpu_sorted[0][1]), curses.color_pair(3)) diff --git a/helpers.py b/cpunetlog/helpers.py similarity index 100% rename from helpers.py rename to cpunetlog/helpers.py diff --git a/history_store.py b/cpunetlog/history_store.py similarity index 100% rename from history_store.py rename to cpunetlog/history_store.py diff --git a/logging.py b/cpunetlog/logging.py similarity index 99% rename from logging.py rename to cpunetlog/logging.py index 0b35361bd4258efd5c5ad5df6b9c0e6c3f74f466..3df17800f458542cfbf1a26a8ce97b7e4c57b4b3 100644 --- a/logging.py +++ b/cpunetlog/logging.py @@ -18,7 +18,7 @@ import psutil #import subprocess #import signal -from history_store import HistoryStore +from cpunetlog.history_store import HistoryStore class LoggingClass: diff --git a/cpunetlog/main.py b/cpunetlog/main.py new file mode 100644 index 0000000000000000000000000000000000000000..fc234611b5c1b2eff8e255ed8f0802a60f363b8a --- /dev/null +++ b/cpunetlog/main.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# Copyright (c) 2014, +# Karlsruhe Institute of Technology, Institute of Telematics +# +# This code is provided under the BSD 2-Clause License. +# Please refer to the LICENSE.txt file for further information. +# +# Author: Mario Hock + + +import os +import psutil +import time +import sys +import traceback +import math +import json + +from collections import namedtuple + +import cpunetlog.helpers +import cpunetlog.curses_display as ui +from cpunetlog.logging import LoggingManager +from cpunetlog.psutil_functions import calculate_cpu_times_percent + + +def get_time(): + """ Unified/comparable clock access """ + return time.time() + + +## XXX for interactive debugging only +def RELOAD(): + print ("import importlib") + print ("importlib.reload(cpunetlog)") + + +MEASUREMENT_INTERVAL = 0.2 + +nic_speeds = cpunetlog.helpers.get_nic_speeds() +nics = list( nic_speeds.keys() ) + + +class Reading: + """ A single reading of various CPU, NET, ... values. --> Building block for the »Measurement« class.""" + + def __init__(self): + ## * measurements * + self.timestamp = get_time() + #self.cpu_util = psutil.cpu_percent(interval=0, percpu=True) ## XXX + #self.cpu_times_percent = psutil.cpu_times_percent(interval=0, percpu=True) ## XXX + self.cpu_times = psutil.cpu_times(percpu=True) + self.memory = psutil.virtual_memory() + self.net_io = psutil.net_io_counters(pernic=True) + + def __str__(self): + ## •‣∘⁕∗◘☉☀★◾☞☛⦿ + return "◘ Timespan: " + str(self.timespan) + \ + "\n◘ CPU utilization: " + str(self.cpu_util) + \ + "\n◘ CPU times: " + str(self.cpu_times) + \ + "\n◘ RAM: " + str(self.memory) + \ + "\n◘ NET: " + str(self.net_io) + + + +class NetworkTraffic: + """ Utility class for calculating and storing network traffic: Total amount (during a timespan) and ratio. """ + + def __init__(self, older_counters, younger_counters, timespan): + self.total = dict() + self.ratio = dict() + + for field in older_counters._fields: + field_delta = getattr(younger_counters, field) - getattr(older_counters, field) + + self.total[field] = field_delta + self.ratio[field] = field_delta / timespan + + def __str__(self): + return "Total (bytes):" + str(self.total) + "; Ratio (bytes/s)" + str(self.ratio) + + + +class Measurement: + """ Calculates and stores CPU utilization, network traffic, ... during a timespan. Based two »Readings«. """ + + def __init__(self, reading1, reading2): + self.r1 = reading1 + self.r2 = reading2 + + ## calculate differences + self.timespan = self.r2.timestamp - self.r1.timestamp + self.cpu_times_percent = calculate_cpu_times_percent(self.r1.cpu_times, self.r2.cpu_times, percpu=True) + self.net_io = self._calculate_net_io() + + + def _calculate_net_io(self): + ret = dict() + + for nic in self.r1.net_io.keys(): + ret[nic] = NetworkTraffic(self.r1.net_io[nic], self.r2.net_io[nic], self.timespan) + + return ret + + def get_begin(self): + return self.r1.timestamp + + def get_end(self): + return self.r2.timestamp + + + +def measure(interval = MEASUREMENT_INTERVAL): + """ Convenience function to perform one »Measurement« """ + + r1 = Reading() + time.sleep(interval) + r2 = Reading() + + m = Measurement(r1, r2) + + return m + + + + +def main_loop(args, logging_manager): + """ Main Loop: + - Sets up curses-display + - Takes a reading every second + - Displays the measurements + - Logs the measurements with the LoggingManager + """ + + ## TODO this should be configurable by command line options + sample_interval = float(args.interval) + display_interval = float(args.displayinterval) + + display_skip = max(display_interval / sample_interval, 1) + + err = None + + try: + # Set up (curses) UI. + ui.nics = nics + ui.nic_speeds = nic_speeds + ui.logging_manager = logging_manager + if not args.headless: + ui.init() + + # Take an initial reading. + old_reading = Reading() + + # Sleep till the next "full" second begins. (In order to roughly synchronize with other instances.) + now = time.time() + time.sleep(math.ceil(now)-now) + + display_skip_counter = 0 + running = True + while running: + # Take a new reading. + new_reading = Reading() + + # Calculate the measurement from the last two readings. + measurement = Measurement(old_reading, new_reading) + + # Display/log the measurement. + running &= logging_manager.log(measurement) + if ( display_skip_counter % display_skip < 1 ) and not args.headless: # the display may skip some samples + running = ui.display( measurement ) + display_skip_counter = 0 + display_skip_counter += 1 + + # Store the last reading as |old_reading|. + old_reading = new_reading + + time.sleep(sample_interval) + ## XXX TODO We could calculating the remaining waiting-time here. + + + + except KeyboardInterrupt: + # Quit gracefully on Ctrl-C + pass + except Exception as e: + # On error: Store stack trace for later processing. + err = e + exc_type, exc_value, exc_traceback = sys.exc_info() + finally: + # Tear down the UI. + if not args.headless: + ui.close() + if args.logging: + logging_manager.close() + + ## On error: Print error message *after* curses has quit. + if ( err ): + print( "Unexpected exception happened: '" + str(err) + "'" ) + print + + traceback.print_exception(exc_type, exc_value, exc_traceback, file=sys.stdout) + + print + print( "QUIT." ) + + + + +def main(): + ## Command line arguments + import argparse +# + parser = argparse.ArgumentParser() + + ## Logging + parser.add_argument("-l", "--logging", action="store_true", + help="Enables logging.") + parser.add_argument("-A", "--autologging", action="store_true", + help="Enables auto-logging. (Log only on network activity. Implies --logging)") + parser.add_argument("-W", "--watch", + help="Store the command-line of the given program as log-comment. (Use together with --autologging.)") + parser.add_argument("-c", "--comment", + help="A comment that is stored in the logfile. (See --logging.)") + parser.add_argument("--path", default="/tmp/cpunetlog", + help="Path where the log files are stored in. (See --logging.)") + parser.add_argument("-e", "--environment", + help="JSON file that holds arbitrary environment context. (This can be seen as a structured comment field.)") + parser.add_argument("-i", "--interval", default="0.5", + help="Time between two samples (in seconds). [Default = 0.5]") + parser.add_argument("-d", "--displayinterval", default="1", + help="Time between two display updates (in seconds). [Default = 1]") + + + # NICs + parser.add_argument("--nics", nargs='+', + help="The network interfaces that should be displayed (and logged, see --logging).") + + parser.add_argument("-q", "--headless", action="store_true", + help="Run in quiet/headless mode without GUI") + + args = parser.parse_args() + + + + ## NICs + monitored_nics = nics + if ( args.nics ): + #assert( set(nics).issuperset(args.nics) ) + monitored_nics = args.nics + + ## --autologging implies --logging + if ( args.autologging ): + args.logging = True + + ## compensate psutil version incompatibilities + try: + num_cpus = psutil.cpu_count() + except: + num_cpus = psutil.NUM_CPUS + + ## Logging + logging_manager = LoggingManager( num_cpus, monitored_nics, cpunetlog.helpers.get_sysinfo(), args.environment, + args.comment, args.path, args.autologging, args.watch ) + if args.logging: + logging_manager.enable_measurement_logger() + + + # Run the main loop. + main_loop(args, logging_manager) + +## MAIN ## +if __name__ == "__main__": + main() diff --git a/psutil_functions.py b/cpunetlog/psutil_functions.py similarity index 100% rename from psutil_functions.py rename to cpunetlog/psutil_functions.py diff --git a/testing.py b/cpunetlog/testing.py similarity index 100% rename from testing.py rename to cpunetlog/testing.py diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..13c940e3c646a98a9705fb38241f605a743fa5e1 Binary files /dev/null and b/screenshot.png differ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..b88034e414bc7b80d686e3c94d516305348053ea --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100755 index 0000000000000000000000000000000000000000..f88e77fcc7259c5d0b18f86aa6dd7f324a52f6b1 --- /dev/null +++ b/setup.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from setuptools import setup + +version = "1.1.1" +desc = "Display, log and plot CPU utilization and network throughput" +longdesc = desc +author_name = "Mario Hock" +author_email_addr = "mario.hock@kit.edu" + + +setup( + name = "cpunetlog", + packages = [ + "cpunetlog" + ], + entry_points = { + "console_scripts": [ + 'cpunetlog = cpunetlog.main:main' + ], + }, + version = version, + description = desc, + long_description = longdesc, + author = author_name, + author_email = author_email_addr, + maintainer = author_name, + maintainer_email = author_email_addr, + url = "https://git.scc.kit.edu/CPUnetLOG/CPUnetLOG", + license = "BSD", + platforms = "Linux", + zip_safe = False, + install_requires = [ + 'psutil', + 'netifaces' + ], + keywords = [ + 'cpu', + 'throughput', + 'utilization', + 'plot', + 'log', + 'display', + 'analyze', + 'network', + 'traffic' + ], + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python :: 3', + 'Operating System :: POSIX :: Linux', + 'Environment :: Console', + 'Environment :: Console :: Curses', + 'Natural Language :: English', + 'Intended Audience :: Education', + 'Intended Audience :: Information Technology', + 'Intended Audience :: Science/Research', + 'Intended Audience :: System Administrators', + 'Intended Audience :: Telecommunications Industry', + 'Topic :: Scientific/Engineering', + 'Topic :: Internet', + 'Topic :: System :: Logging', + 'Topic :: System :: Networking', + 'Topic :: System :: Networking :: Monitoring', + 'Topic :: System :: Operating System Kernels :: Linux', + 'Topic :: Utilities' + ] + )