Refactor plugin folder in the same way than exports modules

This commit is contained in:
nicolargo 2023-10-15 18:05:18 +02:00
parent 30cc9f3e0e
commit 48251c8271
64 changed files with 7374 additions and 7390 deletions

View File

@ -6,23 +6,16 @@ This is the Glances plugins folder.
A Glances plugin is a Python module hosted in a folder.
It should be based on the MVC model.
- model: data model (where the stats will be updated)
- view: input for UI (where the stats are displayed)
- controler: output from UI (where the stats are controled)
It should implement a Class named PluginModel (inherited from GlancesPluginModel).
////
TODO
////
This class should be based on the MVC model.
- model: where the stats are updated (update method)
- view: where the stats are prepare to be displayed (update_views)
- controler: where the stats are displayed (msg_curse method)
A plugin should define the following global variables:
- fields_description: a dict twith the field description/option
- items_history_list: define items history
- items_history_list (optional): define items history
A plugin should implement the following methods:
- update(): update the self.stats variable (most of the time a dict or a list of dict)
- msg_curse(): return a list of messages to display in UI
Have a look of all Glances plugin's methods in the plugin.py file.
Have a look of all Glances plugin's methods in the plugin folder (where the GlancesPluginModel is defined).

View File

@ -0,0 +1,251 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Alert plugin."""
from datetime import datetime
from glances.logger import logger
from glances.events import glances_events
from glances.thresholds import glances_thresholds
# from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# Static decision tree for the global alert message
# - msg: Message to be displayed (result of the decision tree)
# - thresholds: a list of stats to take into account
# - thresholds_min: minimal value of the thresholds sum
# - 0: OK
# - 1: CAREFUL
# - 2: WARNING
# - 3: CRITICAL
tree = [
{'msg': 'No warning or critical alert detected', 'thresholds': [], 'thresholds_min': 0},
{'msg': 'High CPU user mode', 'thresholds': ['cpu_user'], 'thresholds_min': 2},
{'msg': 'High CPU kernel usage', 'thresholds': ['cpu_system'], 'thresholds_min': 2},
{'msg': 'High CPU I/O waiting', 'thresholds': ['cpu_iowait'], 'thresholds_min': 2},
{
'msg': 'Large CPU stolen time. System running the hypervisor is too busy.',
'thresholds': ['cpu_steal'],
'thresholds_min': 2,
},
{'msg': 'High CPU niced value', 'thresholds': ['cpu_niced'], 'thresholds_min': 2},
{'msg': 'System overloaded in the last 5 minutes', 'thresholds': ['load'], 'thresholds_min': 2},
{'msg': 'High swap (paging) usage', 'thresholds': ['memswap'], 'thresholds_min': 2},
{'msg': 'High memory consumption', 'thresholds': ['mem'], 'thresholds_min': 2},
]
# TODO: change the algo to use the following decision tree
# Source: Inspire by https://scoutapm.com/blog/slow_server_flow_chart
# _yes means threshold >= 2
# _no means threshold < 2
# With threshold:
# - 0: OK
# - 1: CAREFUL
# - 2: WARNING
# - 3: CRITICAL
tree_new = {
'cpu_iowait': {
'_yes': {
'memswap': {
'_yes': {
'mem': {
'_yes': {
# Once you've identified the offenders, the resolution will again
# depend on whether their memory usage seems business-as-usual or not.
# For example, a memory leak can be satisfactorily addressed by a one-time
# or periodic restart of the process.
# - if memory usage seems anomalous: kill the offending processes.
# - if memory usage seems business-as-usual: add RAM to the server,
# or split high-memory using services to other servers.
'_msg': "Memory issue"
},
'_no': {
# ???
'_msg': "Swap issue"
},
}
},
'_no': {
# Low swap means you have a "real" IO wait problem. The next step is to see what's hogging your IO.
# iotop is an awesome tool for identifying io offenders. Two things to note:
# unless you've already installed iotop, it's probably not already on your system.
# Recommendation: install it before you need it - - it's no fun trying to install a troubleshooting
# tool on an overloaded machine (iotop requires a Linux of 2.62 or above)
'_msg': "I/O issue"
},
}
},
'_no': {
'cpu_total': {
'_yes': {
'cpu_user': {
'_yes': {
# We expect the user-time percentage to be high.
# There's most likely a program or service you've configured on you server that's
# hogging CPU.
# Checking the % user time just confirms this. When you see that the % user-time is high,
# it's time to see what executable is monopolizing the CPU
# Once you've confirmed that the % usertime is high, check the process list(also provided
# by top).
# Be default, top sorts the process list by % CPU, so you can just look at the top process
# or processes.
# If there's a single process hogging the CPU in a way that seems abnormal, it's an
# anomalous situation
# that a service restart can fix. If there are are multiple processes taking up CPU
# resources, or it
# there's one process that takes lots of resources while otherwise functioning normally,
# than your setup
# may just be underpowered. You'll need to upgrade your server(add more cores),
# or split services out onto
# other boxes. In either case, you have a resolution:
# - if situation seems anomalous: kill the offending processes.
# - if situation seems typical given history: upgrade server or add more servers.
'_msg': "CPU issue with user process(es)"
},
'_no': {
'cpu_steal': {
'_yes': {
'_msg': "CPU issue with stolen time. System running the hypervisor may be too busy."
},
'_no': {'_msg': "CPU issue with system process(es)"},
}
},
}
},
'_no': {
'_yes': {
# ???
'_msg': "Memory issue"
},
'_no': {
# Your slowness isn't due to CPU or IO problems, so it's likely an application-specific issue.
# It's also possible that the slowness is being caused by another server in your cluster, or
# by an external service you rely on.
# start by checking important applications for uncharacteristic slowness(the DB is a good place
# to start), think through which parts of your infrastructure could be slowed down externally.
# For example, do you use an externally hosted email service that could slow down critical
# parts of your application ?
# If you suspect another server in your cluster, strace and lsof can provide information on
# what the process is doing or waiting on. Strace will show you which file descriptors are
# being read or written to (or being attempted to be read from) and lsof can give you a
# mapping of those file descriptors to network connections.
'_msg': "External issue"
},
},
}
},
}
}
def global_message():
"""Parse the decision tree and return the message.
Note: message corresponding to the current thresholds values
"""
# Compute the weight for each item in the tree
current_thresholds = glances_thresholds.get()
for i in tree:
i['weight'] = sum([current_thresholds[t].value() for t in i['thresholds'] if t in current_thresholds])
themax = max(tree, key=lambda d: d['weight'])
if themax['weight'] >= themax['thresholds_min']:
# Check if the weight is > to the minimal threshold value
return themax['msg']
else:
return tree[0]['msg']
class PluginModel(GlancesPluginModel):
"""Glances alert plugin.
Only for display.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args,
config=config,
stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'bottom'
# Set the maximum number of events to display
if config is not None and (config.has_section('alert') or config.has_section('alerts')):
glances_events.set_max_events(config.get_int_value('alert', 'max_events'))
def update(self):
"""Nothing to do here. Just return the global glances_log."""
# Set the stats to the glances_events
self.stats = glances_events.get()
# Define the global message thanks to the current thresholds
# and the decision tree
# !!! Call directly in the msg_curse function
# global_message()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Build the string message
# Header
ret.append(self.curse_add_line(global_message(), "TITLE"))
# Loop over alerts
for alert in self.stats:
# New line
ret.append(self.curse_new_line())
# Start
msg = str(datetime.fromtimestamp(alert[0]))
ret.append(self.curse_add_line(msg))
# Duration
if alert[1] > 0:
# If finished display duration
msg = ' ({})'.format(datetime.fromtimestamp(alert[1]) - datetime.fromtimestamp(alert[0]))
else:
msg = ' (ongoing)'
ret.append(self.curse_add_line(msg))
ret.append(self.curse_add_line(" - "))
# Infos
if alert[1] > 0:
# If finished do not display status
msg = '{} on {}'.format(alert[2], alert[3])
ret.append(self.curse_add_line(msg))
else:
msg = str(alert[3])
ret.append(self.curse_add_line(msg, decoration=alert[2]))
# Min / Mean / Max
if self.approx_equal(alert[6], alert[4], tolerance=0.1):
msg = ' ({:.1f})'.format(alert[5])
else:
msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert[6], alert[5], alert[4])
ret.append(self.curse_add_line(msg))
# Top processes
top_process = ', '.join([p['name'] for p in alert[9]])
if top_process != '':
msg = ': {}'.format(top_process)
ret.append(self.curse_add_line(msg))
return ret
def approx_equal(self, a, b, tolerance=0.0):
"""Compare a with b using the tolerance (if numerical)."""
if str(int(a)).isdigit() and str(int(b)).isdigit():
return abs(a - b) <= max(abs(a), abs(b)) * tolerance
else:
return a == b

View File

@ -1,251 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Alert plugin."""
from datetime import datetime
from glances.logger import logger
from glances.events import glances_events
from glances.thresholds import glances_thresholds
# from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# Static decision tree for the global alert message
# - msg: Message to be displayed (result of the decision tree)
# - thresholds: a list of stats to take into account
# - thresholds_min: minimal value of the thresholds sum
# - 0: OK
# - 1: CAREFUL
# - 2: WARNING
# - 3: CRITICAL
tree = [
{'msg': 'No warning or critical alert detected', 'thresholds': [], 'thresholds_min': 0},
{'msg': 'High CPU user mode', 'thresholds': ['cpu_user'], 'thresholds_min': 2},
{'msg': 'High CPU kernel usage', 'thresholds': ['cpu_system'], 'thresholds_min': 2},
{'msg': 'High CPU I/O waiting', 'thresholds': ['cpu_iowait'], 'thresholds_min': 2},
{
'msg': 'Large CPU stolen time. System running the hypervisor is too busy.',
'thresholds': ['cpu_steal'],
'thresholds_min': 2,
},
{'msg': 'High CPU niced value', 'thresholds': ['cpu_niced'], 'thresholds_min': 2},
{'msg': 'System overloaded in the last 5 minutes', 'thresholds': ['load'], 'thresholds_min': 2},
{'msg': 'High swap (paging) usage', 'thresholds': ['memswap'], 'thresholds_min': 2},
{'msg': 'High memory consumption', 'thresholds': ['mem'], 'thresholds_min': 2},
]
# TODO: change the algo to use the following decision tree
# Source: Inspire by https://scoutapm.com/blog/slow_server_flow_chart
# _yes means threshold >= 2
# _no means threshold < 2
# With threshold:
# - 0: OK
# - 1: CAREFUL
# - 2: WARNING
# - 3: CRITICAL
tree_new = {
'cpu_iowait': {
'_yes': {
'memswap': {
'_yes': {
'mem': {
'_yes': {
# Once you've identified the offenders, the resolution will again
# depend on whether their memory usage seems business-as-usual or not.
# For example, a memory leak can be satisfactorily addressed by a one-time
# or periodic restart of the process.
# - if memory usage seems anomalous: kill the offending processes.
# - if memory usage seems business-as-usual: add RAM to the server,
# or split high-memory using services to other servers.
'_msg': "Memory issue"
},
'_no': {
# ???
'_msg': "Swap issue"
},
}
},
'_no': {
# Low swap means you have a "real" IO wait problem. The next step is to see what's hogging your IO.
# iotop is an awesome tool for identifying io offenders. Two things to note:
# unless you've already installed iotop, it's probably not already on your system.
# Recommendation: install it before you need it - - it's no fun trying to install a troubleshooting
# tool on an overloaded machine (iotop requires a Linux of 2.62 or above)
'_msg': "I/O issue"
},
}
},
'_no': {
'cpu_total': {
'_yes': {
'cpu_user': {
'_yes': {
# We expect the user-time percentage to be high.
# There's most likely a program or service you've configured on you server that's
# hogging CPU.
# Checking the % user time just confirms this. When you see that the % user-time is high,
# it's time to see what executable is monopolizing the CPU
# Once you've confirmed that the % usertime is high, check the process list(also provided
# by top).
# Be default, top sorts the process list by % CPU, so you can just look at the top process
# or processes.
# If there's a single process hogging the CPU in a way that seems abnormal, it's an
# anomalous situation
# that a service restart can fix. If there are are multiple processes taking up CPU
# resources, or it
# there's one process that takes lots of resources while otherwise functioning normally,
# than your setup
# may just be underpowered. You'll need to upgrade your server(add more cores),
# or split services out onto
# other boxes. In either case, you have a resolution:
# - if situation seems anomalous: kill the offending processes.
# - if situation seems typical given history: upgrade server or add more servers.
'_msg': "CPU issue with user process(es)"
},
'_no': {
'cpu_steal': {
'_yes': {
'_msg': "CPU issue with stolen time. System running the hypervisor may be too busy."
},
'_no': {'_msg': "CPU issue with system process(es)"},
}
},
}
},
'_no': {
'_yes': {
# ???
'_msg': "Memory issue"
},
'_no': {
# Your slowness isn't due to CPU or IO problems, so it's likely an application-specific issue.
# It's also possible that the slowness is being caused by another server in your cluster, or
# by an external service you rely on.
# start by checking important applications for uncharacteristic slowness(the DB is a good place
# to start), think through which parts of your infrastructure could be slowed down externally.
# For example, do you use an externally hosted email service that could slow down critical
# parts of your application ?
# If you suspect another server in your cluster, strace and lsof can provide information on
# what the process is doing or waiting on. Strace will show you which file descriptors are
# being read or written to (or being attempted to be read from) and lsof can give you a
# mapping of those file descriptors to network connections.
'_msg': "External issue"
},
},
}
},
}
}
def global_message():
"""Parse the decision tree and return the message.
Note: message corresponding to the current thresholds values
"""
# Compute the weight for each item in the tree
current_thresholds = glances_thresholds.get()
for i in tree:
i['weight'] = sum([current_thresholds[t].value() for t in i['thresholds'] if t in current_thresholds])
themax = max(tree, key=lambda d: d['weight'])
if themax['weight'] >= themax['thresholds_min']:
# Check if the weight is > to the minimal threshold value
return themax['msg']
else:
return tree[0]['msg']
class PluginModel(GlancesPluginModel):
"""Glances alert plugin.
Only for display.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args,
config=config,
stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'bottom'
# Set the maximum number of events to display
if config is not None and (config.has_section('alert') or config.has_section('alerts')):
glances_events.set_max_events(config.get_int_value('alert', 'max_events'))
def update(self):
"""Nothing to do here. Just return the global glances_log."""
# Set the stats to the glances_events
self.stats = glances_events.get()
# Define the global message thanks to the current thresholds
# and the decision tree
# !!! Call directly in the msg_curse function
# global_message()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Build the string message
# Header
ret.append(self.curse_add_line(global_message(), "TITLE"))
# Loop over alerts
for alert in self.stats:
# New line
ret.append(self.curse_new_line())
# Start
msg = str(datetime.fromtimestamp(alert[0]))
ret.append(self.curse_add_line(msg))
# Duration
if alert[1] > 0:
# If finished display duration
msg = ' ({})'.format(datetime.fromtimestamp(alert[1]) - datetime.fromtimestamp(alert[0]))
else:
msg = ' (ongoing)'
ret.append(self.curse_add_line(msg))
ret.append(self.curse_add_line(" - "))
# Infos
if alert[1] > 0:
# If finished do not display status
msg = '{} on {}'.format(alert[2], alert[3])
ret.append(self.curse_add_line(msg))
else:
msg = str(alert[3])
ret.append(self.curse_add_line(msg, decoration=alert[2]))
# Min / Mean / Max
if self.approx_equal(alert[6], alert[4], tolerance=0.1):
msg = ' ({:.1f})'.format(alert[5])
else:
msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert[6], alert[5], alert[4])
ret.append(self.curse_add_line(msg))
# Top processes
top_process = ', '.join([p['name'] for p in alert[9]])
if top_process != '':
msg = ': {}'.format(top_process)
ret.append(self.curse_add_line(msg))
return ret
def approx_equal(self, a, b, tolerance=0.0):
"""Compare a with b using the tolerance (if numerical)."""
if str(int(a)).isdigit() and str(int(b)).isdigit():
return abs(a - b) <= max(abs(a), abs(b)) * tolerance
else:
return a == b

View File

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Monitor plugin."""
from glances.globals import iteritems
from glances.amps_list import AmpsList as glancesAmpsList
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances AMPs plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the list of AMP (classes define in the glances/amps_list.py script)
self.glances_amps = glancesAmpsList(self.args, self.config)
def get_key(self):
"""Return the key of the list."""
return 'name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the AMP list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
for k, v in iteritems(self.glances_amps.update()):
stats.append(
{
'key': self.get_key(),
'name': v.NAME,
'result': v.result(),
'refresh': v.refresh(),
'timer': v.time_until_refresh(),
'count': v.count(),
'countmin': v.count_min(),
'countmax': v.count_max(),
'regex': v.regex() is not None,
},
)
else:
# Not available in SNMP mode
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, nbprocess=0, countmin=None, countmax=None, header="", log=False):
"""Return the alert status relative to the process number."""
if nbprocess is None:
return 'OK'
if countmin is None:
countmin = nbprocess
if countmax is None:
countmax = nbprocess
if nbprocess > 0:
if int(countmin) <= int(nbprocess) <= int(countmax):
return 'OK'
else:
return 'WARNING'
else:
if int(countmin) == 0:
return 'OK'
else:
return 'CRITICAL'
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
# Only process if stats exist and display plugin enable...
ret = []
if not self.stats or args.disable_process or self.is_disabled():
return ret
# Build the string message
for m in self.stats:
# Only display AMP if a result exist
if m['result'] is None:
continue
# Display AMP
first_column = '{}'.format(m['name'])
first_column_style = self.get_alert(m['count'], m['countmin'], m['countmax'])
second_column = '{}'.format(m['count'] if m['regex'] else '')
for line in m['result'].split('\n'):
# Display first column with the process name...
msg = '{:<16} '.format(first_column)
ret.append(self.curse_add_line(msg, first_column_style))
# ... and second column with the number of matching processes...
msg = '{:<4} '.format(second_column)
ret.append(self.curse_add_line(msg))
# ... only on the first line
first_column = second_column = ''
# Display AMP result in the third column
ret.append(self.curse_add_line(line, splittable=True))
ret.append(self.curse_new_line())
# Delete the last empty line
try:
ret.pop()
except IndexError:
pass
return ret

View File

@ -1,123 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Monitor plugin."""
from glances.globals import iteritems
from glances.amps_list import AmpsList as glancesAmpsList
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances AMPs plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the list of AMP (classes define in the glances/amps_list.py script)
self.glances_amps = glancesAmpsList(self.args, self.config)
def get_key(self):
"""Return the key of the list."""
return 'name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the AMP list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
for k, v in iteritems(self.glances_amps.update()):
stats.append(
{
'key': self.get_key(),
'name': v.NAME,
'result': v.result(),
'refresh': v.refresh(),
'timer': v.time_until_refresh(),
'count': v.count(),
'countmin': v.count_min(),
'countmax': v.count_max(),
'regex': v.regex() is not None,
},
)
else:
# Not available in SNMP mode
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, nbprocess=0, countmin=None, countmax=None, header="", log=False):
"""Return the alert status relative to the process number."""
if nbprocess is None:
return 'OK'
if countmin is None:
countmin = nbprocess
if countmax is None:
countmax = nbprocess
if nbprocess > 0:
if int(countmin) <= int(nbprocess) <= int(countmax):
return 'OK'
else:
return 'WARNING'
else:
if int(countmin) == 0:
return 'OK'
else:
return 'CRITICAL'
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
# Only process if stats exist and display plugin enable...
ret = []
if not self.stats or args.disable_process or self.is_disabled():
return ret
# Build the string message
for m in self.stats:
# Only display AMP if a result exist
if m['result'] is None:
continue
# Display AMP
first_column = '{}'.format(m['name'])
first_column_style = self.get_alert(m['count'], m['countmin'], m['countmax'])
second_column = '{}'.format(m['count'] if m['regex'] else '')
for line in m['result'].split('\n'):
# Display first column with the process name...
msg = '{:<16} '.format(first_column)
ret.append(self.curse_add_line(msg, first_column_style))
# ... and second column with the number of matching processes...
msg = '{:<4} '.format(second_column)
ret.append(self.curse_add_line(msg))
# ... only on the first line
first_column = second_column = ''
# Display AMP result in the third column
ret.append(self.curse_add_line(line, splittable=True))
ret.append(self.curse_new_line())
# Delete the last empty line
try:
ret.pop()
except IndexError:
pass
return ret

View File

@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Cloud plugin.
Supported Cloud API:
- OpenStack meta data (class ThreadOpenStack) - Vanilla OpenStack
- OpenStackEC2 meta data (class ThreadOpenStackEC2) - Amazon EC2 compatible
"""
import threading
from glances.globals import iteritems, to_ascii
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
# Import plugin specific dependency
try:
import requests
except ImportError as e:
import_error_tag = True
# Display debug message if import error
logger.warning("Missing Python Lib ({}), Cloud plugin is disabled".format(e))
else:
import_error_tag = False
class PluginModel(GlancesPluginModel):
"""Glances' cloud plugin.
The goal of this plugin is to retrieve additional information
concerning the datacenter where the host is connected.
See https://github.com/nicolargo/glances/issues/1029
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Init the stats
self.reset()
# Init thread to grab OpenStack stats asynchronously
self.OPENSTACK = ThreadOpenStack()
self.OPENSTACKEC2 = ThreadOpenStackEC2()
# Run the thread
self.OPENSTACK.start()
self.OPENSTACKEC2.start()
def exit(self):
"""Overwrite the exit method to close threads."""
self.OPENSTACK.stop()
self.OPENSTACKEC2.stop()
# Call the father class
super(PluginModel, self).exit()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the cloud stats.
Return the stats (dict)
"""
# Init new stats
stats = self.get_init_value()
# Requests lib is needed to get stats from the Cloud API
if import_error_tag:
return stats
# Update the stats
if self.input_method == 'local':
stats = self.OPENSTACK.stats
if not stats:
stats = self.OPENSTACKEC2.stats
# Example:
# Uncomment to test on physical computer (only for test purpose)
# stats = {'id': 'ami-id',
# 'name': 'My VM',
# 'type': 'Gold',
# 'region': 'France',
# 'platform': 'OpenStack'}
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
if not self.stats or self.stats == {} or self.is_disabled():
return ret
# Generate the output
msg = self.stats.get('platform', 'Unknown')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {} instance {} ({})'.format(
self.stats.get('type', 'Unknown'), self.stats.get('name', 'Unknown'), self.stats.get('region', 'Unknown')
)
ret.append(self.curse_add_line(msg))
# Return the message with decoration
# logger.info(ret)
return ret
class ThreadOpenStack(threading.Thread):
"""
Specific thread to grab OpenStack stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "OpenStack"
OPENSTACK_API_URL = 'http://169.254.169.254/openstack/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'project_id',
'name': 'name',
'type': 'meta/role',
'region': 'availability_zone',
}
def __init__(self):
"""Init the class."""
logger.debug("cloud plugin - Create thread for OpenStack metadata")
super(ThreadOpenStack, self).__init__()
# Event needed to stop properly the thread
self._stopper = threading.Event()
# The class return the stats as a dict
self._stats = {}
def run(self):
"""Grab plugin's stats.
Infinite loop, should be stopped by calling the stop() method
"""
if import_error_tag:
self.stop()
return False
for k, v in iteritems(self.OPENSTACK_API_METADATA):
r_url = '{}/{}'.format(self.OPENSTACK_API_URL, v)
try:
# Local request, a timeout of 3 seconds is OK
r = requests.get(r_url, timeout=3)
except Exception as e:
logger.debug('cloud plugin - Cannot connect to the OpenStack metadata API {}: {}'.format(r_url, e))
break
else:
if r.ok:
self._stats[k] = to_ascii(r.content)
else:
# No break during the loop, so we can set the platform
self._stats['platform'] = self.OPENSTACK_PLATFORM
return True
@property
def stats(self):
"""Stats getter."""
return self._stats
@stats.setter
def stats(self, value):
"""Stats setter."""
self._stats = value
def stop(self, timeout=None):
"""Stop the thread."""
logger.debug("cloud plugin - Close thread for OpenStack metadata")
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()
class ThreadOpenStackEC2(ThreadOpenStack):
"""
Specific thread to grab OpenStack EC2 (Amazon cloud) stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "Amazon EC2"
OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'ami-id',
'name': 'instance-id',
'type': 'instance-type',
'region': 'placement/availability-zone',
}

View File

@ -1,220 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Cloud plugin.
Supported Cloud API:
- OpenStack meta data (class ThreadOpenStack) - Vanilla OpenStack
- OpenStackEC2 meta data (class ThreadOpenStackEC2) - Amazon EC2 compatible
"""
import threading
from glances.globals import iteritems, to_ascii
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
# Import plugin specific dependency
try:
import requests
except ImportError as e:
import_error_tag = True
# Display debug message if import error
logger.warning("Missing Python Lib ({}), Cloud plugin is disabled".format(e))
else:
import_error_tag = False
class PluginModel(GlancesPluginModel):
"""Glances' cloud plugin.
The goal of this plugin is to retrieve additional information
concerning the datacenter where the host is connected.
See https://github.com/nicolargo/glances/issues/1029
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Init the stats
self.reset()
# Init thread to grab OpenStack stats asynchronously
self.OPENSTACK = ThreadOpenStack()
self.OPENSTACKEC2 = ThreadOpenStackEC2()
# Run the thread
self.OPENSTACK.start()
self.OPENSTACKEC2.start()
def exit(self):
"""Overwrite the exit method to close threads."""
self.OPENSTACK.stop()
self.OPENSTACKEC2.stop()
# Call the father class
super(PluginModel, self).exit()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the cloud stats.
Return the stats (dict)
"""
# Init new stats
stats = self.get_init_value()
# Requests lib is needed to get stats from the Cloud API
if import_error_tag:
return stats
# Update the stats
if self.input_method == 'local':
stats = self.OPENSTACK.stats
if not stats:
stats = self.OPENSTACKEC2.stats
# Example:
# Uncomment to test on physical computer (only for test purpose)
# stats = {'id': 'ami-id',
# 'name': 'My VM',
# 'type': 'Gold',
# 'region': 'France',
# 'platform': 'OpenStack'}
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
if not self.stats or self.stats == {} or self.is_disabled():
return ret
# Generate the output
msg = self.stats.get('platform', 'Unknown')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {} instance {} ({})'.format(
self.stats.get('type', 'Unknown'), self.stats.get('name', 'Unknown'), self.stats.get('region', 'Unknown')
)
ret.append(self.curse_add_line(msg))
# Return the message with decoration
# logger.info(ret)
return ret
class ThreadOpenStack(threading.Thread):
"""
Specific thread to grab OpenStack stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "OpenStack"
OPENSTACK_API_URL = 'http://169.254.169.254/openstack/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'project_id',
'name': 'name',
'type': 'meta/role',
'region': 'availability_zone',
}
def __init__(self):
"""Init the class."""
logger.debug("cloud plugin - Create thread for OpenStack metadata")
super(ThreadOpenStack, self).__init__()
# Event needed to stop properly the thread
self._stopper = threading.Event()
# The class return the stats as a dict
self._stats = {}
def run(self):
"""Grab plugin's stats.
Infinite loop, should be stopped by calling the stop() method
"""
if import_error_tag:
self.stop()
return False
for k, v in iteritems(self.OPENSTACK_API_METADATA):
r_url = '{}/{}'.format(self.OPENSTACK_API_URL, v)
try:
# Local request, a timeout of 3 seconds is OK
r = requests.get(r_url, timeout=3)
except Exception as e:
logger.debug('cloud plugin - Cannot connect to the OpenStack metadata API {}: {}'.format(r_url, e))
break
else:
if r.ok:
self._stats[k] = to_ascii(r.content)
else:
# No break during the loop, so we can set the platform
self._stats['platform'] = self.OPENSTACK_PLATFORM
return True
@property
def stats(self):
"""Stats getter."""
return self._stats
@stats.setter
def stats(self, value):
"""Stats setter."""
self._stats = value
def stop(self, timeout=None):
"""Stop the thread."""
logger.debug("cloud plugin - Close thread for OpenStack metadata")
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()
class ThreadOpenStackEC2(ThreadOpenStack):
"""
Specific thread to grab OpenStack EC2 (Amazon cloud) stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "Amazon EC2"
OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'ami-id',
'name': 'instance-id',
'type': 'instance-type',
'region': 'placement/availability-zone',
}

View File

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Connections plugin."""
from __future__ import unicode_literals
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
from glances.globals import nativestr
import psutil
# Define the history items list
# items_history_list = [{'name': 'rx',
# 'description': 'Download rate per second',
# 'y_unit': 'bit/s'},
# {'name': 'tx',
# 'description': 'Upload rate per second',
# 'y_unit': 'bit/s'}]
class PluginModel(GlancesPluginModel):
"""Glances connections plugin.
stats is a dict
"""
status_list = [psutil.CONN_LISTEN, psutil.CONN_ESTABLISHED]
initiated_states = [psutil.CONN_SYN_SENT, psutil.CONN_SYN_RECV]
terminated_states = [
psutil.CONN_FIN_WAIT1,
psutil.CONN_FIN_WAIT2,
psutil.CONN_TIME_WAIT,
psutil.CONN_CLOSE,
psutil.CONN_CLOSE_WAIT,
psutil.CONN_LAST_ACK,
]
conntrack = {
'nf_conntrack_count': '/proc/sys/net/netfilter/nf_conntrack_count',
'nf_conntrack_max': '/proc/sys/net/netfilter/nf_conntrack_max',
}
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
# items_history_list=items_history_list,
stats_init_value={'net_connections_enabled': True, 'nf_conntrack_enabled': True},
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update connections stats using the input method.
Stats is a dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the PSUtils lib
# Grab network interface stat using the psutil net_connections method
if stats['net_connections_enabled']:
try:
net_connections = psutil.net_connections(kind="tcp")
except Exception as e:
logger.warning('Can not get network connections stats ({})'.format(e))
logger.info('Disable connections stats')
stats['net_connections_enabled'] = False
self.stats = stats
return self.stats
for s in self.status_list:
stats[s] = len([c for c in net_connections if c.status == s])
initiated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
initiated += stats[s]
stats['initiated'] = initiated
terminated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
terminated += stats[s]
stats['terminated'] = terminated
if stats['nf_conntrack_enabled']:
# Grab connections track directly from the /proc file
for i in self.conntrack:
try:
with open(self.conntrack[i], 'r') as f:
stats[i] = float(f.readline().rstrip("\n"))
except (IOError, FileNotFoundError) as e:
logger.warning('Can not get network connections track ({})'.format(e))
logger.info('Disable connections track')
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
if 'nf_conntrack_max' in stats and 'nf_conntrack_count' in stats:
stats['nf_conntrack_percent'] = stats['nf_conntrack_count'] * 100 / stats['nf_conntrack_max']
else:
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
elif self.input_method == 'snmp':
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specific information
try:
# Alert and log
if self.stats['nf_conntrack_enabled']:
self.views['nf_conntrack_percent']['decoration'] = self.get_alert(header='nf_conntrack_percent')
except KeyError:
# try/except mandatory for Windows compatibility (no conntrack stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Header
if self.stats['net_connections_enabled'] or self.stats['nf_conntrack_enabled']:
msg = '{}'.format('TCP CONNECTIONS')
ret.append(self.curse_add_line(msg, "TITLE"))
# Connections status
if self.stats['net_connections_enabled']:
for s in [psutil.CONN_LISTEN, 'initiated', psutil.CONN_ESTABLISHED, 'terminated']:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(self.stats[s], width=max_width - len(s) + 2)
ret.append(self.curse_add_line(msg))
# Connections track
if (
self.stats['nf_conntrack_enabled']
and 'nf_conntrack_count' in self.stats
and 'nf_conntrack_max' in self.stats
):
s = 'Tracked'
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(
'{:0.0f}/{:0.0f}'.format(self.stats['nf_conntrack_count'], self.stats['nf_conntrack_max']),
width=max_width - len(s) + 2,
)
ret.append(self.curse_add_line(msg, self.get_views(key='nf_conntrack_percent', option='decoration')))
return ret

View File

@ -1,176 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Connections plugin."""
from __future__ import unicode_literals
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
from glances.globals import nativestr
import psutil
# Define the history items list
# items_history_list = [{'name': 'rx',
# 'description': 'Download rate per second',
# 'y_unit': 'bit/s'},
# {'name': 'tx',
# 'description': 'Upload rate per second',
# 'y_unit': 'bit/s'}]
class PluginModel(GlancesPluginModel):
"""Glances connections plugin.
stats is a dict
"""
status_list = [psutil.CONN_LISTEN, psutil.CONN_ESTABLISHED]
initiated_states = [psutil.CONN_SYN_SENT, psutil.CONN_SYN_RECV]
terminated_states = [
psutil.CONN_FIN_WAIT1,
psutil.CONN_FIN_WAIT2,
psutil.CONN_TIME_WAIT,
psutil.CONN_CLOSE,
psutil.CONN_CLOSE_WAIT,
psutil.CONN_LAST_ACK,
]
conntrack = {
'nf_conntrack_count': '/proc/sys/net/netfilter/nf_conntrack_count',
'nf_conntrack_max': '/proc/sys/net/netfilter/nf_conntrack_max',
}
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
# items_history_list=items_history_list,
stats_init_value={'net_connections_enabled': True, 'nf_conntrack_enabled': True},
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update connections stats using the input method.
Stats is a dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the PSUtils lib
# Grab network interface stat using the psutil net_connections method
if stats['net_connections_enabled']:
try:
net_connections = psutil.net_connections(kind="tcp")
except Exception as e:
logger.warning('Can not get network connections stats ({})'.format(e))
logger.info('Disable connections stats')
stats['net_connections_enabled'] = False
self.stats = stats
return self.stats
for s in self.status_list:
stats[s] = len([c for c in net_connections if c.status == s])
initiated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
initiated += stats[s]
stats['initiated'] = initiated
terminated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
terminated += stats[s]
stats['terminated'] = terminated
if stats['nf_conntrack_enabled']:
# Grab connections track directly from the /proc file
for i in self.conntrack:
try:
with open(self.conntrack[i], 'r') as f:
stats[i] = float(f.readline().rstrip("\n"))
except (IOError, FileNotFoundError) as e:
logger.warning('Can not get network connections track ({})'.format(e))
logger.info('Disable connections track')
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
if 'nf_conntrack_max' in stats and 'nf_conntrack_count' in stats:
stats['nf_conntrack_percent'] = stats['nf_conntrack_count'] * 100 / stats['nf_conntrack_max']
else:
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
elif self.input_method == 'snmp':
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specific information
try:
# Alert and log
if self.stats['nf_conntrack_enabled']:
self.views['nf_conntrack_percent']['decoration'] = self.get_alert(header='nf_conntrack_percent')
except KeyError:
# try/except mandatory for Windows compatibility (no conntrack stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Header
if self.stats['net_connections_enabled'] or self.stats['nf_conntrack_enabled']:
msg = '{}'.format('TCP CONNECTIONS')
ret.append(self.curse_add_line(msg, "TITLE"))
# Connections status
if self.stats['net_connections_enabled']:
for s in [psutil.CONN_LISTEN, 'initiated', psutil.CONN_ESTABLISHED, 'terminated']:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(self.stats[s], width=max_width - len(s) + 2)
ret.append(self.curse_add_line(msg))
# Connections track
if (
self.stats['nf_conntrack_enabled']
and 'nf_conntrack_count' in self.stats
and 'nf_conntrack_max' in self.stats
):
s = 'Tracked'
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(
'{:0.0f}/{:0.0f}'.format(self.stats['nf_conntrack_count'], self.stats['nf_conntrack_max']),
width=max_width - len(s) + 2,
)
ret.append(self.curse_add_line(msg, self.get_views(key='nf_conntrack_percent', option='decoration')))
return ret

View File

@ -0,0 +1,430 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Containers plugin."""
import os
from copy import deepcopy
from glances.logger import logger
from glances.plugins.containers.engines.docker import DockerContainersExtension, import_docker_error_tag
from glances.plugins.containers.engines.podman import PodmanContainersExtension, import_podman_error_tag
from glances.plugins.plugin.model import GlancesPluginModel
from glances.processes import glances_processes
from glances.processes import sort_stats as sort_stats_processes
# Define the items history list (list of items to add to history)
# TODO: For the moment limited to the CPU. Had to change the graph exports
# method to display one graph per container.
# items_history_list = [{'name': 'cpu_percent',
# 'description': 'Container CPU consumption in %',
# 'y_unit': '%'},
# {'name': 'memory_usage',
# 'description': 'Container memory usage in bytes',
# 'y_unit': 'B'},
# {'name': 'network_rx',
# 'description': 'Container network RX bitrate in bits per second',
# 'y_unit': 'bps'},
# {'name': 'network_tx',
# 'description': 'Container network TX bitrate in bits per second',
# 'y_unit': 'bps'},
# {'name': 'io_r',
# 'description': 'Container IO bytes read per second',
# 'y_unit': 'Bps'},
# {'name': 'io_w',
# 'description': 'Container IO bytes write per second',
# 'y_unit': 'Bps'}]
items_history_list = [{'name': 'cpu_percent', 'description': 'Container CPU consumption in %', 'y_unit': '%'}]
# List of key to remove before export
export_exclude_list = ['cpu', 'io', 'memory', 'network']
# Sort dictionary for human
sort_for_human = {
'io_counters': 'disk IO',
'cpu_percent': 'CPU consumption',
'memory_usage': 'memory consumption',
'cpu_times': 'uptime',
'name': 'container name',
None: 'None',
}
class PluginModel(GlancesPluginModel):
"""Glances Docker plugin.
stats is a dict: {'version': {...}, 'containers': [{}, {}]}
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
# The plugin can be disabled using: args.disable_docker
self.args = args
# Default config keys
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the Docker API
self.docker_extension = DockerContainersExtension() if not import_docker_error_tag else None
# Init the Podman API
if import_podman_error_tag:
self.podman_client = None
else:
self.podman_client = PodmanContainersExtension(podman_sock=self._podman_sock())
# Sort key
self.sort_key = None
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def _podman_sock(self):
"""Return the podman sock.
Could be desfined in the [docker] section thanks to the podman_sock option.
Default value: unix:///run/user/1000/podman/podman.sock
"""
conf_podman_sock = self.get_conf_value('podman_sock')
if len(conf_podman_sock) == 0:
return "unix:///run/user/1000/podman/podman.sock"
else:
return conf_podman_sock[0]
def exit(self):
"""Overwrite the exit method to close threads."""
if self.docker_extension:
self.docker_extension.stop()
if self.podman_client:
self.podman_client.stop()
# Call the father class
super(PluginModel, self).exit()
def get_key(self):
"""Return the key of the list."""
return 'name'
def get_export(self):
"""Overwrite the default export method.
- Only exports containers
- The key is the first container name
"""
try:
ret = deepcopy(self.stats['containers'])
except KeyError as e:
logger.debug("docker plugin - Docker export error {}".format(e))
ret = []
# Remove fields uses to compute rate
for container in ret:
for i in export_exclude_list:
container.pop(i)
return ret
def _all_tag(self):
"""Return the all tag of the Glances/Docker configuration file.
# By default, Glances only display running containers
# Set the following key to True to display all containers
all=True
"""
all_tag = self.get_conf_value('all')
if len(all_tag) == 0:
return False
else:
return all_tag[0].lower() == 'true'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update Docker and podman stats using the input method."""
# Connection should be ok
if self.docker_extension is None and self.podman_client is None:
return self.get_init_value()
if self.input_method == 'local':
# Update stats
stats_docker = self.update_docker() if self.docker_extension else {}
stats_podman = self.update_podman() if self.podman_client else {}
stats = {
'version': stats_docker.get('version', {}),
'version_podman': stats_podman.get('version', {}),
'containers': stats_docker.get('containers', []) + stats_podman.get('containers', []),
}
elif self.input_method == 'snmp':
# Update stats using SNMP
# Not available
pass
# Sort and update the stats
# @TODO: Have a look because sort did not work for the moment (need memory stats ?)
self.sort_key, self.stats = sort_docker_stats(stats)
return self.stats
def update_docker(self):
"""Update Docker stats using the input method."""
version, containers = self.docker_extension.update(all_tag=self._all_tag())
for container in containers:
container["engine"] = 'docker'
return {"version": version, "containers": containers}
def update_podman(self):
"""Update Podman stats."""
version, containers = self.podman_client.update(all_tag=self._all_tag())
for container in containers:
container["engine"] = 'podman'
return {"version": version, "containers": containers}
def get_user_ticks(self):
"""Return the user ticks by reading the environment variable."""
return os.sysconf(os.sysconf_names['SC_CLK_TCK'])
def get_stats_action(self):
"""Return stats for the action.
Docker will return self.stats['containers']
"""
return self.stats['containers']
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
if 'containers' not in self.stats:
return False
# Add specifics information
# Alert
for i in self.stats['containers']:
# Init the views for the current container (key = container name)
self.views[i[self.get_key()]] = {'cpu': {}, 'mem': {}}
# CPU alert
if 'cpu' in i and 'total' in i['cpu']:
# Looking for specific CPU container threshold in the conf file
alert = self.get_alert(i['cpu']['total'], header=i['name'] + '_cpu', action_key=i['name'])
if alert == 'DEFAULT':
# Not found ? Get back to default CPU threshold value
alert = self.get_alert(i['cpu']['total'], header='cpu')
self.views[i[self.get_key()]]['cpu']['decoration'] = alert
# MEM alert
if 'memory' in i and 'usage' in i['memory']:
# Looking for specific MEM container threshold in the conf file
alert = self.get_alert(
i['memory']['usage'], maximum=i['memory']['limit'], header=i['name'] + '_mem', action_key=i['name']
)
if alert == 'DEFAULT':
# Not found ? Get back to default MEM threshold value
alert = self.get_alert(i['memory']['usage'], maximum=i['memory']['limit'], header='mem')
self.views[i[self.get_key()]]['mem']['decoration'] = alert
return True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist (and non null) and display plugin enable...
if not self.stats or 'containers' not in self.stats or len(self.stats['containers']) == 0 or self.is_disabled():
return ret
show_pod_name = False
if any(ct.get("pod_name") for ct in self.stats["containers"]):
show_pod_name = True
show_engine_name = False
if len(set(ct["engine"] for ct in self.stats["containers"])) > 1:
show_engine_name = True
# Build the string message
# Title
msg = '{}'.format('CONTAINERS')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {}'.format(len(self.stats['containers']))
ret.append(self.curse_add_line(msg))
msg = ' sorted by {}'.format(sort_for_human[self.sort_key])
ret.append(self.curse_add_line(msg))
# msg = ' (served by Docker {})'.format(self.stats['version']["Version"])
# ret.append(self.curse_add_line(msg))
ret.append(self.curse_new_line())
# Header
ret.append(self.curse_new_line())
# Get the maximum containers name
# Max size is configurable. See feature request #1723.
name_max_width = min(
self.config.get_int_value('containers', 'max_name_size', default=20) if self.config is not None else 20,
len(max(self.stats['containers'], key=lambda x: len(x['name']))['name']),
)
if show_engine_name:
msg = ' {:{width}}'.format('Engine', width=6)
ret.append(self.curse_add_line(msg))
if show_pod_name:
msg = ' {:{width}}'.format('Pod', width=12)
ret.append(self.curse_add_line(msg))
msg = ' {:{width}}'.format('Name', width=name_max_width)
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'name' else 'DEFAULT'))
msg = '{:>10}'.format('Status')
ret.append(self.curse_add_line(msg))
msg = '{:>10}'.format('Uptime')
ret.append(self.curse_add_line(msg))
msg = '{:>6}'.format('CPU%')
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'cpu_percent' else 'DEFAULT'))
msg = '{:>7}'.format('MEM')
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'memory_usage' else 'DEFAULT'))
msg = '/{:<7}'.format('MAX')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('IOR/s')
ret.append(self.curse_add_line(msg))
msg = ' {:<7}'.format('IOW/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Rx/s')
ret.append(self.curse_add_line(msg))
msg = ' {:<7}'.format('Tx/s')
ret.append(self.curse_add_line(msg))
msg = ' {:8}'.format('Command')
ret.append(self.curse_add_line(msg))
# Data
for container in self.stats['containers']:
ret.append(self.curse_new_line())
if show_engine_name:
ret.append(self.curse_add_line(' {:{width}}'.format(container["engine"], width=6)))
if show_pod_name:
ret.append(self.curse_add_line(' {:{width}}'.format(container.get("pod_id", "-"), width=12)))
# Name
ret.append(self.curse_add_line(self._msg_name(container=container, max_width=name_max_width)))
# Status
status = self.container_alert(container['Status'])
msg = '{:>10}'.format(container['Status'][0:10])
ret.append(self.curse_add_line(msg, status))
# Uptime
if container['Uptime']:
msg = '{:>10}'.format(container['Uptime'])
else:
msg = '{:>10}'.format('_')
ret.append(self.curse_add_line(msg))
# CPU
try:
msg = '{:>6.1f}'.format(container['cpu']['total'])
except KeyError:
msg = '{:>6}'.format('_')
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='cpu', option='decoration')))
# MEM
try:
msg = '{:>7}'.format(self.auto_unit(container['memory']['usage']))
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='mem', option='decoration')))
try:
msg = '/{:<7}'.format(self.auto_unit(container['memory']['limit']))
except KeyError:
msg = '/{:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# IO R/W
unit = 'B'
try:
value = self.auto_unit(int(container['io']['ior'] // container['io']['time_since_update'])) + unit
msg = '{:>7}'.format(value)
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg))
try:
value = self.auto_unit(int(container['io']['iow'] // container['io']['time_since_update'])) + unit
msg = ' {:<7}'.format(value)
except KeyError:
msg = ' {:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# NET RX/TX
if args.byte:
# Bytes per second (for dummy)
to_bit = 1
unit = ''
else:
# Bits per second (for real network administrator | Default)
to_bit = 8
unit = 'b'
try:
value = (
self.auto_unit(
int(container['network']['rx'] // container['network']['time_since_update'] * to_bit)
)
+ unit
)
msg = '{:>7}'.format(value)
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg))
try:
value = (
self.auto_unit(
int(container['network']['tx'] // container['network']['time_since_update'] * to_bit)
)
+ unit
)
msg = ' {:<7}'.format(value)
except KeyError:
msg = ' {:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# Command
if container['Command'] is not None:
msg = ' {}'.format(' '.join(container['Command']))
else:
msg = ' {}'.format('_')
ret.append(self.curse_add_line(msg, splittable=True))
return ret
def _msg_name(self, container, max_width):
"""Build the container name."""
name = container['name'][:max_width]
return ' {:{width}}'.format(name, width=max_width)
def container_alert(self, status):
"""Analyse the container status."""
if status == 'running':
return 'OK'
elif status == 'exited':
return 'WARNING'
elif status == 'dead':
return 'CRITICAL'
else:
return 'CAREFUL'
def sort_docker_stats(stats):
# Sort Docker stats using the same function than processes
sort_by = glances_processes.sort_key
sort_by_secondary = 'memory_usage'
if sort_by == 'memory_percent':
sort_by = 'memory_usage'
sort_by_secondary = 'cpu_percent'
elif sort_by in ['username', 'io_counters', 'cpu_times']:
sort_by = 'cpu_percent'
# Sort docker stats
sort_stats_processes(
stats['containers'],
sorted_by=sort_by,
sorted_by_secondary=sort_by_secondary,
# Reverse for all but name
reverse=glances_processes.sort_key != 'name',
)
# Return the main sort key and the sorted stats
return sort_by, stats

View File

@ -1,430 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Containers plugin."""
import os
from copy import deepcopy
from glances.logger import logger
from glances.plugins.containers.engines.docker import DockerContainersExtension, import_docker_error_tag
from glances.plugins.containers.engines.podman import PodmanContainersExtension, import_podman_error_tag
from glances.plugins.plugin.model import GlancesPluginModel
from glances.processes import glances_processes
from glances.processes import sort_stats as sort_stats_processes
# Define the items history list (list of items to add to history)
# TODO: For the moment limited to the CPU. Had to change the graph exports
# method to display one graph per container.
# items_history_list = [{'name': 'cpu_percent',
# 'description': 'Container CPU consumption in %',
# 'y_unit': '%'},
# {'name': 'memory_usage',
# 'description': 'Container memory usage in bytes',
# 'y_unit': 'B'},
# {'name': 'network_rx',
# 'description': 'Container network RX bitrate in bits per second',
# 'y_unit': 'bps'},
# {'name': 'network_tx',
# 'description': 'Container network TX bitrate in bits per second',
# 'y_unit': 'bps'},
# {'name': 'io_r',
# 'description': 'Container IO bytes read per second',
# 'y_unit': 'Bps'},
# {'name': 'io_w',
# 'description': 'Container IO bytes write per second',
# 'y_unit': 'Bps'}]
items_history_list = [{'name': 'cpu_percent', 'description': 'Container CPU consumption in %', 'y_unit': '%'}]
# List of key to remove before export
export_exclude_list = ['cpu', 'io', 'memory', 'network']
# Sort dictionary for human
sort_for_human = {
'io_counters': 'disk IO',
'cpu_percent': 'CPU consumption',
'memory_usage': 'memory consumption',
'cpu_times': 'uptime',
'name': 'container name',
None: 'None',
}
class PluginModel(GlancesPluginModel):
"""Glances Docker plugin.
stats is a dict: {'version': {...}, 'containers': [{}, {}]}
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
# The plugin can be disabled using: args.disable_docker
self.args = args
# Default config keys
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the Docker API
self.docker_extension = DockerContainersExtension() if not import_docker_error_tag else None
# Init the Podman API
if import_podman_error_tag:
self.podman_client = None
else:
self.podman_client = PodmanContainersExtension(podman_sock=self._podman_sock())
# Sort key
self.sort_key = None
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def _podman_sock(self):
"""Return the podman sock.
Could be desfined in the [docker] section thanks to the podman_sock option.
Default value: unix:///run/user/1000/podman/podman.sock
"""
conf_podman_sock = self.get_conf_value('podman_sock')
if len(conf_podman_sock) == 0:
return "unix:///run/user/1000/podman/podman.sock"
else:
return conf_podman_sock[0]
def exit(self):
"""Overwrite the exit method to close threads."""
if self.docker_extension:
self.docker_extension.stop()
if self.podman_client:
self.podman_client.stop()
# Call the father class
super(PluginModel, self).exit()
def get_key(self):
"""Return the key of the list."""
return 'name'
def get_export(self):
"""Overwrite the default export method.
- Only exports containers
- The key is the first container name
"""
try:
ret = deepcopy(self.stats['containers'])
except KeyError as e:
logger.debug("docker plugin - Docker export error {}".format(e))
ret = []
# Remove fields uses to compute rate
for container in ret:
for i in export_exclude_list:
container.pop(i)
return ret
def _all_tag(self):
"""Return the all tag of the Glances/Docker configuration file.
# By default, Glances only display running containers
# Set the following key to True to display all containers
all=True
"""
all_tag = self.get_conf_value('all')
if len(all_tag) == 0:
return False
else:
return all_tag[0].lower() == 'true'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update Docker and podman stats using the input method."""
# Connection should be ok
if self.docker_extension is None and self.podman_client is None:
return self.get_init_value()
if self.input_method == 'local':
# Update stats
stats_docker = self.update_docker() if self.docker_extension else {}
stats_podman = self.update_podman() if self.podman_client else {}
stats = {
'version': stats_docker.get('version', {}),
'version_podman': stats_podman.get('version', {}),
'containers': stats_docker.get('containers', []) + stats_podman.get('containers', []),
}
elif self.input_method == 'snmp':
# Update stats using SNMP
# Not available
pass
# Sort and update the stats
# @TODO: Have a look because sort did not work for the moment (need memory stats ?)
self.sort_key, self.stats = sort_docker_stats(stats)
return self.stats
def update_docker(self):
"""Update Docker stats using the input method."""
version, containers = self.docker_extension.update(all_tag=self._all_tag())
for container in containers:
container["engine"] = 'docker'
return {"version": version, "containers": containers}
def update_podman(self):
"""Update Podman stats."""
version, containers = self.podman_client.update(all_tag=self._all_tag())
for container in containers:
container["engine"] = 'podman'
return {"version": version, "containers": containers}
def get_user_ticks(self):
"""Return the user ticks by reading the environment variable."""
return os.sysconf(os.sysconf_names['SC_CLK_TCK'])
def get_stats_action(self):
"""Return stats for the action.
Docker will return self.stats['containers']
"""
return self.stats['containers']
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
if 'containers' not in self.stats:
return False
# Add specifics information
# Alert
for i in self.stats['containers']:
# Init the views for the current container (key = container name)
self.views[i[self.get_key()]] = {'cpu': {}, 'mem': {}}
# CPU alert
if 'cpu' in i and 'total' in i['cpu']:
# Looking for specific CPU container threshold in the conf file
alert = self.get_alert(i['cpu']['total'], header=i['name'] + '_cpu', action_key=i['name'])
if alert == 'DEFAULT':
# Not found ? Get back to default CPU threshold value
alert = self.get_alert(i['cpu']['total'], header='cpu')
self.views[i[self.get_key()]]['cpu']['decoration'] = alert
# MEM alert
if 'memory' in i and 'usage' in i['memory']:
# Looking for specific MEM container threshold in the conf file
alert = self.get_alert(
i['memory']['usage'], maximum=i['memory']['limit'], header=i['name'] + '_mem', action_key=i['name']
)
if alert == 'DEFAULT':
# Not found ? Get back to default MEM threshold value
alert = self.get_alert(i['memory']['usage'], maximum=i['memory']['limit'], header='mem')
self.views[i[self.get_key()]]['mem']['decoration'] = alert
return True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist (and non null) and display plugin enable...
if not self.stats or 'containers' not in self.stats or len(self.stats['containers']) == 0 or self.is_disabled():
return ret
show_pod_name = False
if any(ct.get("pod_name") for ct in self.stats["containers"]):
show_pod_name = True
show_engine_name = False
if len(set(ct["engine"] for ct in self.stats["containers"])) > 1:
show_engine_name = True
# Build the string message
# Title
msg = '{}'.format('CONTAINERS')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {}'.format(len(self.stats['containers']))
ret.append(self.curse_add_line(msg))
msg = ' sorted by {}'.format(sort_for_human[self.sort_key])
ret.append(self.curse_add_line(msg))
# msg = ' (served by Docker {})'.format(self.stats['version']["Version"])
# ret.append(self.curse_add_line(msg))
ret.append(self.curse_new_line())
# Header
ret.append(self.curse_new_line())
# Get the maximum containers name
# Max size is configurable. See feature request #1723.
name_max_width = min(
self.config.get_int_value('containers', 'max_name_size', default=20) if self.config is not None else 20,
len(max(self.stats['containers'], key=lambda x: len(x['name']))['name']),
)
if show_engine_name:
msg = ' {:{width}}'.format('Engine', width=6)
ret.append(self.curse_add_line(msg))
if show_pod_name:
msg = ' {:{width}}'.format('Pod', width=12)
ret.append(self.curse_add_line(msg))
msg = ' {:{width}}'.format('Name', width=name_max_width)
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'name' else 'DEFAULT'))
msg = '{:>10}'.format('Status')
ret.append(self.curse_add_line(msg))
msg = '{:>10}'.format('Uptime')
ret.append(self.curse_add_line(msg))
msg = '{:>6}'.format('CPU%')
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'cpu_percent' else 'DEFAULT'))
msg = '{:>7}'.format('MEM')
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'memory_usage' else 'DEFAULT'))
msg = '/{:<7}'.format('MAX')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('IOR/s')
ret.append(self.curse_add_line(msg))
msg = ' {:<7}'.format('IOW/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Rx/s')
ret.append(self.curse_add_line(msg))
msg = ' {:<7}'.format('Tx/s')
ret.append(self.curse_add_line(msg))
msg = ' {:8}'.format('Command')
ret.append(self.curse_add_line(msg))
# Data
for container in self.stats['containers']:
ret.append(self.curse_new_line())
if show_engine_name:
ret.append(self.curse_add_line(' {:{width}}'.format(container["engine"], width=6)))
if show_pod_name:
ret.append(self.curse_add_line(' {:{width}}'.format(container.get("pod_id", "-"), width=12)))
# Name
ret.append(self.curse_add_line(self._msg_name(container=container, max_width=name_max_width)))
# Status
status = self.container_alert(container['Status'])
msg = '{:>10}'.format(container['Status'][0:10])
ret.append(self.curse_add_line(msg, status))
# Uptime
if container['Uptime']:
msg = '{:>10}'.format(container['Uptime'])
else:
msg = '{:>10}'.format('_')
ret.append(self.curse_add_line(msg))
# CPU
try:
msg = '{:>6.1f}'.format(container['cpu']['total'])
except KeyError:
msg = '{:>6}'.format('_')
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='cpu', option='decoration')))
# MEM
try:
msg = '{:>7}'.format(self.auto_unit(container['memory']['usage']))
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='mem', option='decoration')))
try:
msg = '/{:<7}'.format(self.auto_unit(container['memory']['limit']))
except KeyError:
msg = '/{:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# IO R/W
unit = 'B'
try:
value = self.auto_unit(int(container['io']['ior'] // container['io']['time_since_update'])) + unit
msg = '{:>7}'.format(value)
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg))
try:
value = self.auto_unit(int(container['io']['iow'] // container['io']['time_since_update'])) + unit
msg = ' {:<7}'.format(value)
except KeyError:
msg = ' {:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# NET RX/TX
if args.byte:
# Bytes per second (for dummy)
to_bit = 1
unit = ''
else:
# Bits per second (for real network administrator | Default)
to_bit = 8
unit = 'b'
try:
value = (
self.auto_unit(
int(container['network']['rx'] // container['network']['time_since_update'] * to_bit)
)
+ unit
)
msg = '{:>7}'.format(value)
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg))
try:
value = (
self.auto_unit(
int(container['network']['tx'] // container['network']['time_since_update'] * to_bit)
)
+ unit
)
msg = ' {:<7}'.format(value)
except KeyError:
msg = ' {:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# Command
if container['Command'] is not None:
msg = ' {}'.format(' '.join(container['Command']))
else:
msg = ' {}'.format('_')
ret.append(self.curse_add_line(msg, splittable=True))
return ret
def _msg_name(self, container, max_width):
"""Build the container name."""
name = container['name'][:max_width]
return ' {:{width}}'.format(name, width=max_width)
def container_alert(self, status):
"""Analyse the container status."""
if status == 'running':
return 'OK'
elif status == 'exited':
return 'WARNING'
elif status == 'dead':
return 'CRITICAL'
else:
return 'CAREFUL'
def sort_docker_stats(stats):
# Sort Docker stats using the same function than processes
sort_by = glances_processes.sort_key
sort_by_secondary = 'memory_usage'
if sort_by == 'memory_percent':
sort_by = 'memory_usage'
sort_by_secondary = 'cpu_percent'
elif sort_by in ['username', 'io_counters', 'cpu_times']:
sort_by = 'cpu_percent'
# Sort docker stats
sort_stats_processes(
stats['containers'],
sorted_by=sort_by,
sorted_by_secondary=sort_by_secondary,
# Reverse for all but name
reverse=glances_processes.sort_key != 'name',
)
# Return the main sort key and the sorted stats
return sort_by, stats

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU core plugin."""
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'phys': {'description': 'Number of physical cores (hyper thread CPUs are excluded).', 'unit': 'number'},
'log': {
'description': 'Number of logical CPUs. A logical CPU is the number of \
physical cores multiplied by the number of threads that can run on each core.',
'unit': 'number',
},
}
class PluginModel(GlancesPluginModel):
"""Glances CPU core plugin.
Get stats about CPU core number.
stats is integer (number of core)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, fields_description=fields_description)
# We dot not want to display the stat in the curse interface
# The core number is displayed by the load plugin
self.display_curse = False
# Do *NOT* uncomment the following line
# @GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update core stats.
Stats is a dict (with both physical and log cpu number) instead of a integer.
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# The psutil 2.0 include psutil.cpu_count() and psutil.cpu_count(logical=False)
# Return a dict with:
# - phys: physical cores only (hyper thread CPUs are excluded)
# - log: logical CPUs in the system
# Return None if undefined
try:
stats["phys"] = psutil.cpu_count(logical=False)
stats["log"] = psutil.cpu_count()
except NameError:
self.reset()
elif self.input_method == 'snmp':
# Update stats using SNMP
# http://stackoverflow.com/questions/5662467/how-to-find-out-the-number-of-cpus-using-snmp
pass
# Update the stats
self.stats = stats
return self.stats

View File

@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU core plugin."""
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'phys': {'description': 'Number of physical cores (hyper thread CPUs are excluded).', 'unit': 'number'},
'log': {
'description': 'Number of logical CPUs. A logical CPU is the number of \
physical cores multiplied by the number of threads that can run on each core.',
'unit': 'number',
},
}
class PluginModel(GlancesPluginModel):
"""Glances CPU core plugin.
Get stats about CPU core number.
stats is integer (number of core)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, fields_description=fields_description)
# We dot not want to display the stat in the curse interface
# The core number is displayed by the load plugin
self.display_curse = False
# Do *NOT* uncomment the following line
# @GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update core stats.
Stats is a dict (with both physical and log cpu number) instead of a integer.
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# The psutil 2.0 include psutil.cpu_count() and psutil.cpu_count(logical=False)
# Return a dict with:
# - phys: physical cores only (hyper thread CPUs are excluded)
# - log: logical CPUs in the system
# Return None if undefined
try:
stats["phys"] = psutil.cpu_count(logical=False)
stats["log"] = psutil.cpu_count()
except NameError:
self.reset()
elif self.input_method == 'snmp':
# Update stats using SNMP
# http://stackoverflow.com/questions/5662467/how-to-find-out-the-number-of-cpus-using-snmp
pass
# Update the stats
self.stats = stats
return self.stats

View File

@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU plugin."""
from glances.timer import getTimeSinceLastUpdate
from glances.globals import LINUX, WINDOWS, SUNOS, iterkeys
from glances.cpu_percent import cpu_percent
from glances.plugins.core import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
# description: human readable description
# short_name: shortname to use un UI
# unit: unit type
# rate: is it a rate ? If yes, // by time_since_update when displayed,
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
fields_description = {
'total': {'description': 'Sum of all CPU percentages (except idle).', 'unit': 'percent'},
'system': {
'description': 'percent time spent in kernel space. System CPU time is the \
time spent running code in the Operating System kernel.',
'unit': 'percent',
},
'user': {
'description': 'CPU percent time spent in user space. \
User CPU time is the time spent on the processor running your program\'s code (or code in libraries).',
'unit': 'percent',
},
'iowait': {
'description': '*(Linux)*: percent time spent by the CPU waiting for I/O \
operations to complete.',
'unit': 'percent',
},
'dpc': {
'description': '*(Windows)*: time spent servicing deferred procedure calls (DPCs)',
'unit': 'percent',
},
'idle': {
'description': 'percent of CPU used by any program. Every program or task \
that runs on a computer system occupies a certain amount of processing \
time on the CPU. If the CPU has completed all tasks it is idle.',
'unit': 'percent',
},
'irq': {
'description': '*(Linux and BSD)*: percent time spent servicing/handling \
hardware/software interrupts. Time servicing interrupts (hardware + \
software).',
'unit': 'percent',
},
'nice': {
'description': '*(Unix)*: percent time occupied by user level processes with \
a positive nice value. The time the CPU has spent running users\' \
processes that have been *niced*.',
'unit': 'percent',
},
'steal': {
'description': '*(Linux)*: percentage of time a virtual CPU waits for a real \
CPU while the hypervisor is servicing another virtual processor.',
'unit': 'percent',
},
'ctx_switches': {
'description': 'number of context switches (voluntary + involuntary) per \
second. A context switch is a procedure that a computer\'s CPU (central \
processing unit) follows to change from one task (or process) to \
another while ensuring that the tasks do not conflict.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'ctx_sw',
},
'interrupts': {
'description': 'number of interrupts per second.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'inter',
},
'soft_interrupts': {
'description': 'number of software interrupts per second. Always set to \
0 on Windows and SunOS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sw_int',
},
'syscalls': {
'description': 'number of system calls per second. Always 0 on Linux OS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sys_call',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# percentage of user CPU time: .1.3.6.1.4.1.2021.11.9.0
# percentages of system CPU time: .1.3.6.1.4.1.2021.11.10.0
# percentages of idle CPU time: .1.3.6.1.4.1.2021.11.11.0
snmp_oid = {
'default': {
'user': '1.3.6.1.4.1.2021.11.9.0',
'system': '1.3.6.1.4.1.2021.11.10.0',
'idle': '1.3.6.1.4.1.2021.11.11.0',
},
'windows': {'percent': '1.3.6.1.2.1.25.3.3.1.2'},
'esxi': {'percent': '1.3.6.1.2.1.25.3.3.1.2'},
'netapp': {
'system': '1.3.6.1.4.1.789.1.2.1.3.0',
'idle': '1.3.6.1.4.1.789.1.2.1.5.0',
'cpucore': '1.3.6.1.4.1.789.1.2.1.6.0',
},
}
# Define the history items list
# - 'name' define the stat identifier
# - 'y_unit' define the Y label
items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
{'name': 'system', 'description': 'System CPU usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances CPU plugin.
'stats' is a dictionary that contains the system-wide CPU utilization as a
percentage.
"""
def __init__(self, args=None, config=None):
"""Init the CPU plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
# Call CorePluginModel in order to display the core number
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception:
self.nb_log_core = 1
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update CPU stats using the input method."""
# Grab stats into self.stats
if self.input_method == 'local':
stats = self.update_local()
elif self.input_method == 'snmp':
stats = self.update_snmp()
else:
stats = self.get_init_value()
# Update the stats
self.stats = stats
return self.stats
def update_local(self):
"""Update CPU stats using psutil."""
# Grab CPU stats using psutil's cpu_percent and cpu_times_percent
# Get all possible values for CPU stats: user, system, idle,
# nice (UNIX), iowait (Linux), irq (Linux, FreeBSD), steal (Linux 2.6.11+)
# The following stats are returned by the API but not displayed in the UI:
# softirq (Linux), guest (Linux 2.6.24+), guest_nice (Linux 3.2.0+)
# Init new stats
stats = self.get_init_value()
stats['total'] = cpu_percent.get()
# Standards stats
# - user: time spent by normal processes executing in user mode; on Linux this also includes guest time
# - system: time spent by processes executing in kernel mode
# - idle: time spent doing nothing
# - nice (UNIX): time spent by niced (prioritized) processes executing in user mode
# on Linux this also includes guest_nice time
# - iowait (Linux): time spent waiting for I/O to complete.
# This is not accounted in idle time counter.
# - irq (Linux, BSD): time spent for servicing hardware interrupts
# - softirq (Linux): time spent for servicing software interrupts
# - steal (Linux 2.6.11+): time spent by other operating systems running in a virtualized environment
# - guest (Linux 2.6.24+): time spent running a virtual CPU for guest operating systems under
# the control of the Linux kernel
# - guest_nice (Linux 3.2.0+): time spent running a niced guest (virtual CPU for guest operating systems
# under the control of the Linux kernel)
# - interrupt (Windows): time spent for servicing hardware interrupts ( similar to “irq” on UNIX)
# - dpc (Windows): time spent servicing deferred procedure calls (DPCs)
cpu_times_percent = psutil.cpu_times_percent(interval=0.0)
for stat in cpu_times_percent._fields:
stats[stat] = getattr(cpu_times_percent, stat)
# Additional CPU stats (number of events not as a %; psutil>=4.1.0)
# - ctx_switches: number of context switches (voluntary + involuntary) since boot.
# - interrupts: number of interrupts since boot.
# - soft_interrupts: number of software interrupts since boot. Always set to 0 on Windows and SunOS.
# - syscalls: number of system calls since boot. Always set to 0 on Linux.
cpu_stats = psutil.cpu_stats()
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
stats['time_since_update'] = getTimeSinceLastUpdate('cpu')
# Core number is needed to compute the CTX switch limit
stats['cpucore'] = self.nb_log_core
# Previous CPU stats are stored in the cpu_stats_old variable
if not hasattr(self, 'cpu_stats_old'):
# Init the stats (needed to have the key name for export)
for stat in cpu_stats._fields:
# @TODO: better to set it to None but should refactor views and UI...
stats[stat] = 0
else:
# Others calls...
for stat in cpu_stats._fields:
if getattr(cpu_stats, stat) is not None:
stats[stat] = getattr(cpu_stats, stat) - getattr(self.cpu_stats_old, stat)
# Save stats to compute next step
self.cpu_stats_old = cpu_stats
return stats
def update_snmp(self):
"""Update CPU stats using SNMP."""
# Init new stats
stats = self.get_init_value()
# Update stats using SNMP
if self.short_system_name in ('windows', 'esxi'):
# Windows or VMWare ESXi
# You can find the CPU utilization of windows system by querying the oid
# Give also the number of core (number of element in the table)
try:
cpu_stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
# Iter through CPU and compute the idle CPU stats
stats['nb_log_core'] = 0
stats['idle'] = 0
for c in cpu_stats:
if c.startswith('percent'):
stats['idle'] += float(cpu_stats['percent.3'])
stats['nb_log_core'] += 1
if stats['nb_log_core'] > 0:
stats['idle'] = stats['idle'] / stats['nb_log_core']
stats['idle'] = 100 - stats['idle']
stats['total'] = 100 - stats['idle']
else:
# Default behavior
try:
stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name])
except KeyError:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['idle'] == '':
self.reset()
return self.stats
# Convert SNMP stats to float
for key in iterkeys(stats):
stats[key] = float(stats[key])
stats['total'] = 100 - stats['idle']
return stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
for key in ['user', 'system', 'iowait', 'dpc', 'total']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert_log(self.stats[key], header=key)
# Alert only
for key in ['steal']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
# Alert only but depend on Core number
for key in ['ctx_switches']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(
self.stats[key], maximum=100 * self.stats['cpucore'], header=key
)
# Optional
for key in ['nice', 'irq', 'idle', 'steal', 'ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the UI."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disable
if not self.stats or self.args.percpu or self.is_disabled():
return ret
# Some tag to enable/disable stats (example: idle_tag triggered on Windows OS)
idle_tag = 'user' not in self.stats
# First line
# Total + (idle) + ctx_sw
msg = '{}'.format('CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
trend_user = self.get_trend('user')
trend_system = self.get_trend('system')
if trend_user is None or trend_user is None:
trend_cpu = None
else:
trend_cpu = trend_user + trend_system
msg = ' {:4}'.format(self.trend_msg(trend_cpu))
ret.append(self.curse_add_line(msg))
# Total CPU usage
msg = '{:5.1f}%'.format(self.stats['total'])
ret.append(self.curse_add_line(msg, self.get_views(key='total', option='decoration')))
# Idle CPU
if 'idle' in self.stats and not idle_tag:
msg = ' {:8}'.format('idle')
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
msg = '{:4.1f}%'.format(self.stats['idle'])
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
# ctx_switches
# On WINDOWS/SUNOS the ctx_switches is displayed in the third line
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Second line
# user|idle + irq + interrupts
ret.append(self.curse_new_line())
# User CPU
if not idle_tag:
ret.extend(self.curse_add_stat('user', width=15))
elif 'idle' in self.stats:
ret.extend(self.curse_add_stat('idle', width=15))
# IRQ CPU
ret.extend(self.curse_add_stat('irq', width=14, header=' '))
# interrupts
ret.extend(self.curse_add_stat('interrupts', width=15, header=' '))
# Third line
# system|core + nice + sw_int
ret.append(self.curse_new_line())
# System CPU
if not idle_tag:
ret.extend(self.curse_add_stat('system', width=15))
else:
ret.extend(self.curse_add_stat('core', width=15))
# Nice CPU
ret.extend(self.curse_add_stat('nice', width=14, header=' '))
# soft_interrupts
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('soft_interrupts', width=15, header=' '))
else:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Fourth line
# iowait + steal + syscalls
ret.append(self.curse_new_line())
if 'iowait' in self.stats:
# IOWait CPU
ret.extend(self.curse_add_stat('iowait', width=15))
elif 'dpc' in self.stats:
# DPC CPU
ret.extend(self.curse_add_stat('dpc', width=15))
# Steal CPU usage
ret.extend(self.curse_add_stat('steal', width=14, header=' '))
# syscalls: number of system calls since boot. Always set to 0 on Linux. (do not display)
if not LINUX:
ret.extend(self.curse_add_stat('syscalls', width=15, header=' '))
# Return the message with decoration
return ret

View File

@ -1,392 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU plugin."""
from glances.timer import getTimeSinceLastUpdate
from glances.globals import LINUX, WINDOWS, SUNOS, iterkeys
from glances.cpu_percent import cpu_percent
from glances.plugins.core.model import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
# description: human readable description
# short_name: shortname to use un UI
# unit: unit type
# rate: is it a rate ? If yes, // by time_since_update when displayed,
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
fields_description = {
'total': {'description': 'Sum of all CPU percentages (except idle).', 'unit': 'percent'},
'system': {
'description': 'percent time spent in kernel space. System CPU time is the \
time spent running code in the Operating System kernel.',
'unit': 'percent',
},
'user': {
'description': 'CPU percent time spent in user space. \
User CPU time is the time spent on the processor running your program\'s code (or code in libraries).',
'unit': 'percent',
},
'iowait': {
'description': '*(Linux)*: percent time spent by the CPU waiting for I/O \
operations to complete.',
'unit': 'percent',
},
'dpc': {
'description': '*(Windows)*: time spent servicing deferred procedure calls (DPCs)',
'unit': 'percent',
},
'idle': {
'description': 'percent of CPU used by any program. Every program or task \
that runs on a computer system occupies a certain amount of processing \
time on the CPU. If the CPU has completed all tasks it is idle.',
'unit': 'percent',
},
'irq': {
'description': '*(Linux and BSD)*: percent time spent servicing/handling \
hardware/software interrupts. Time servicing interrupts (hardware + \
software).',
'unit': 'percent',
},
'nice': {
'description': '*(Unix)*: percent time occupied by user level processes with \
a positive nice value. The time the CPU has spent running users\' \
processes that have been *niced*.',
'unit': 'percent',
},
'steal': {
'description': '*(Linux)*: percentage of time a virtual CPU waits for a real \
CPU while the hypervisor is servicing another virtual processor.',
'unit': 'percent',
},
'ctx_switches': {
'description': 'number of context switches (voluntary + involuntary) per \
second. A context switch is a procedure that a computer\'s CPU (central \
processing unit) follows to change from one task (or process) to \
another while ensuring that the tasks do not conflict.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'ctx_sw',
},
'interrupts': {
'description': 'number of interrupts per second.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'inter',
},
'soft_interrupts': {
'description': 'number of software interrupts per second. Always set to \
0 on Windows and SunOS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sw_int',
},
'syscalls': {
'description': 'number of system calls per second. Always 0 on Linux OS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sys_call',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# percentage of user CPU time: .1.3.6.1.4.1.2021.11.9.0
# percentages of system CPU time: .1.3.6.1.4.1.2021.11.10.0
# percentages of idle CPU time: .1.3.6.1.4.1.2021.11.11.0
snmp_oid = {
'default': {
'user': '1.3.6.1.4.1.2021.11.9.0',
'system': '1.3.6.1.4.1.2021.11.10.0',
'idle': '1.3.6.1.4.1.2021.11.11.0',
},
'windows': {'percent': '1.3.6.1.2.1.25.3.3.1.2'},
'esxi': {'percent': '1.3.6.1.2.1.25.3.3.1.2'},
'netapp': {
'system': '1.3.6.1.4.1.789.1.2.1.3.0',
'idle': '1.3.6.1.4.1.789.1.2.1.5.0',
'cpucore': '1.3.6.1.4.1.789.1.2.1.6.0',
},
}
# Define the history items list
# - 'name' define the stat identifier
# - 'y_unit' define the Y label
items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
{'name': 'system', 'description': 'System CPU usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances CPU plugin.
'stats' is a dictionary that contains the system-wide CPU utilization as a
percentage.
"""
def __init__(self, args=None, config=None):
"""Init the CPU plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
# Call CorePluginModel in order to display the core number
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception:
self.nb_log_core = 1
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update CPU stats using the input method."""
# Grab stats into self.stats
if self.input_method == 'local':
stats = self.update_local()
elif self.input_method == 'snmp':
stats = self.update_snmp()
else:
stats = self.get_init_value()
# Update the stats
self.stats = stats
return self.stats
def update_local(self):
"""Update CPU stats using psutil."""
# Grab CPU stats using psutil's cpu_percent and cpu_times_percent
# Get all possible values for CPU stats: user, system, idle,
# nice (UNIX), iowait (Linux), irq (Linux, FreeBSD), steal (Linux 2.6.11+)
# The following stats are returned by the API but not displayed in the UI:
# softirq (Linux), guest (Linux 2.6.24+), guest_nice (Linux 3.2.0+)
# Init new stats
stats = self.get_init_value()
stats['total'] = cpu_percent.get()
# Standards stats
# - user: time spent by normal processes executing in user mode; on Linux this also includes guest time
# - system: time spent by processes executing in kernel mode
# - idle: time spent doing nothing
# - nice (UNIX): time spent by niced (prioritized) processes executing in user mode
# on Linux this also includes guest_nice time
# - iowait (Linux): time spent waiting for I/O to complete.
# This is not accounted in idle time counter.
# - irq (Linux, BSD): time spent for servicing hardware interrupts
# - softirq (Linux): time spent for servicing software interrupts
# - steal (Linux 2.6.11+): time spent by other operating systems running in a virtualized environment
# - guest (Linux 2.6.24+): time spent running a virtual CPU for guest operating systems under
# the control of the Linux kernel
# - guest_nice (Linux 3.2.0+): time spent running a niced guest (virtual CPU for guest operating systems
# under the control of the Linux kernel)
# - interrupt (Windows): time spent for servicing hardware interrupts ( similar to “irq” on UNIX)
# - dpc (Windows): time spent servicing deferred procedure calls (DPCs)
cpu_times_percent = psutil.cpu_times_percent(interval=0.0)
for stat in cpu_times_percent._fields:
stats[stat] = getattr(cpu_times_percent, stat)
# Additional CPU stats (number of events not as a %; psutil>=4.1.0)
# - ctx_switches: number of context switches (voluntary + involuntary) since boot.
# - interrupts: number of interrupts since boot.
# - soft_interrupts: number of software interrupts since boot. Always set to 0 on Windows and SunOS.
# - syscalls: number of system calls since boot. Always set to 0 on Linux.
cpu_stats = psutil.cpu_stats()
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
stats['time_since_update'] = getTimeSinceLastUpdate('cpu')
# Core number is needed to compute the CTX switch limit
stats['cpucore'] = self.nb_log_core
# Previous CPU stats are stored in the cpu_stats_old variable
if not hasattr(self, 'cpu_stats_old'):
# Init the stats (needed to have the key name for export)
for stat in cpu_stats._fields:
# @TODO: better to set it to None but should refactor views and UI...
stats[stat] = 0
else:
# Others calls...
for stat in cpu_stats._fields:
if getattr(cpu_stats, stat) is not None:
stats[stat] = getattr(cpu_stats, stat) - getattr(self.cpu_stats_old, stat)
# Save stats to compute next step
self.cpu_stats_old = cpu_stats
return stats
def update_snmp(self):
"""Update CPU stats using SNMP."""
# Init new stats
stats = self.get_init_value()
# Update stats using SNMP
if self.short_system_name in ('windows', 'esxi'):
# Windows or VMWare ESXi
# You can find the CPU utilization of windows system by querying the oid
# Give also the number of core (number of element in the table)
try:
cpu_stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
# Iter through CPU and compute the idle CPU stats
stats['nb_log_core'] = 0
stats['idle'] = 0
for c in cpu_stats:
if c.startswith('percent'):
stats['idle'] += float(cpu_stats['percent.3'])
stats['nb_log_core'] += 1
if stats['nb_log_core'] > 0:
stats['idle'] = stats['idle'] / stats['nb_log_core']
stats['idle'] = 100 - stats['idle']
stats['total'] = 100 - stats['idle']
else:
# Default behavior
try:
stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name])
except KeyError:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['idle'] == '':
self.reset()
return self.stats
# Convert SNMP stats to float
for key in iterkeys(stats):
stats[key] = float(stats[key])
stats['total'] = 100 - stats['idle']
return stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
for key in ['user', 'system', 'iowait', 'dpc', 'total']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert_log(self.stats[key], header=key)
# Alert only
for key in ['steal']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
# Alert only but depend on Core number
for key in ['ctx_switches']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(
self.stats[key], maximum=100 * self.stats['cpucore'], header=key
)
# Optional
for key in ['nice', 'irq', 'idle', 'steal', 'ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the UI."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disable
if not self.stats or self.args.percpu or self.is_disabled():
return ret
# Some tag to enable/disable stats (example: idle_tag triggered on Windows OS)
idle_tag = 'user' not in self.stats
# First line
# Total + (idle) + ctx_sw
msg = '{}'.format('CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
trend_user = self.get_trend('user')
trend_system = self.get_trend('system')
if trend_user is None or trend_user is None:
trend_cpu = None
else:
trend_cpu = trend_user + trend_system
msg = ' {:4}'.format(self.trend_msg(trend_cpu))
ret.append(self.curse_add_line(msg))
# Total CPU usage
msg = '{:5.1f}%'.format(self.stats['total'])
ret.append(self.curse_add_line(msg, self.get_views(key='total', option='decoration')))
# Idle CPU
if 'idle' in self.stats and not idle_tag:
msg = ' {:8}'.format('idle')
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
msg = '{:4.1f}%'.format(self.stats['idle'])
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
# ctx_switches
# On WINDOWS/SUNOS the ctx_switches is displayed in the third line
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Second line
# user|idle + irq + interrupts
ret.append(self.curse_new_line())
# User CPU
if not idle_tag:
ret.extend(self.curse_add_stat('user', width=15))
elif 'idle' in self.stats:
ret.extend(self.curse_add_stat('idle', width=15))
# IRQ CPU
ret.extend(self.curse_add_stat('irq', width=14, header=' '))
# interrupts
ret.extend(self.curse_add_stat('interrupts', width=15, header=' '))
# Third line
# system|core + nice + sw_int
ret.append(self.curse_new_line())
# System CPU
if not idle_tag:
ret.extend(self.curse_add_stat('system', width=15))
else:
ret.extend(self.curse_add_stat('core', width=15))
# Nice CPU
ret.extend(self.curse_add_stat('nice', width=14, header=' '))
# soft_interrupts
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('soft_interrupts', width=15, header=' '))
else:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Fourth line
# iowait + steal + syscalls
ret.append(self.curse_new_line())
if 'iowait' in self.stats:
# IOWait CPU
ret.extend(self.curse_add_stat('iowait', width=15))
elif 'dpc' in self.stats:
# DPC CPU
ret.extend(self.curse_add_stat('dpc', width=15))
# Steal CPU usage
ret.extend(self.curse_add_stat('steal', width=14, header=' '))
# syscalls: number of system calls since boot. Always set to 0 on Linux. (do not display)
if not LINUX:
ret.extend(self.curse_add_stat('syscalls', width=15, header=' '))
# Return the message with decoration
return ret

View File

@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Disk I/O plugin."""
from __future__ import unicode_literals
from glances.globals import nativestr
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Define the history items list
items_history_list = [
{'name': 'read_bytes', 'description': 'Bytes read per second', 'y_unit': 'B/s'},
{'name': 'write_bytes', 'description': 'Bytes write per second', 'y_unit': 'B/s'},
]
class PluginModel(GlancesPluginModel):
"""Glances disks I/O plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
# Hide stats if it has never been != 0
if config is not None:
self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
else:
self.hide_zero = False
self.hide_zero_fields = ['read_bytes', 'write_bytes']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def get_key(self):
"""Return the key of the list."""
return 'disk_name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update disk I/O stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab the stat using the psutil disk_io_counters method
# read_count: number of reads
# write_count: number of writes
# read_bytes: number of bytes read
# write_bytes: number of bytes written
# read_time: time spent reading from disk (in milliseconds)
# write_time: time spent writing to disk (in milliseconds)
try:
diskio = psutil.disk_io_counters(perdisk=True)
except Exception:
return stats
# Previous disk IO stats are stored in the diskio_old variable
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
time_since_update = getTimeSinceLastUpdate('disk')
diskio = diskio
for disk in diskio:
# By default, RamFS is not displayed (issue #714)
if self.args is not None and not self.args.diskio_show_ramfs and disk.startswith('ram'):
continue
# Shall we display the stats ?
if not self.is_display(disk):
continue
# Compute count and bit rate
try:
diskstat = {
'time_since_update': time_since_update,
'disk_name': disk,
'read_count': diskio[disk].read_count - self.diskio_old[disk].read_count,
'write_count': diskio[disk].write_count - self.diskio_old[disk].write_count,
'read_bytes': diskio[disk].read_bytes - self.diskio_old[disk].read_bytes,
'write_bytes': diskio[disk].write_bytes - self.diskio_old[disk].write_bytes,
}
except (KeyError, AttributeError):
diskstat = {
'time_since_update': time_since_update,
'disk_name': disk,
'read_count': 0,
'write_count': 0,
'read_bytes': 0,
'write_bytes': 0,
}
# Add alias if exist (define in the configuration file)
if self.has_alias(disk) is not None:
diskstat['alias'] = self.has_alias(disk)
# Add the dict key
diskstat['key'] = self.get_key()
# Add the current disk stat to the list
stats.append(diskstat)
# Save stats to compute next bitrate
try:
self.diskio_old = diskio
except (IOError, UnboundLocalError):
pass
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard way for the moment...
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Check if the stats should be hidden
self.update_views_hidden()
# Add specifics information
# Alert
for i in self.get_raw():
disk_real_name = i['disk_name']
self.views[i[self.get_key()]]['read_bytes']['decoration'] = self.get_alert(
int(i['read_bytes'] // i['time_since_update']), header=disk_real_name + '_rx'
)
self.views[i[self.get_key()]]['write_bytes']['decoration'] = self.get_alert(
int(i['write_bytes'] // i['time_since_update']), header=disk_real_name + '_tx'
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 13
# Header
msg = '{:{width}}'.format('DISK I/O', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.diskio_iops:
msg = '{:>8}'.format('IOR/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('IOW/s')
ret.append(self.curse_add_line(msg))
else:
msg = '{:>8}'.format('R/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('W/s')
ret.append(self.curse_add_line(msg))
# Disk list (sorted by name)
for i in self.sorted_stats():
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields]):
continue
# Is there an alias for the disk name ?
disk_name = self.has_alias(i['disk_name']) if self.has_alias(i['disk_name']) else i['disk_name']
# New line
ret.append(self.curse_new_line())
if len(disk_name) > name_max_width:
# Cut disk name if it is too long
disk_name = '_' + disk_name[-name_max_width + 1 :]
msg = '{:{width}}'.format(nativestr(disk_name), width=name_max_width + 1)
ret.append(self.curse_add_line(msg))
if args.diskio_iops:
# count
txps = self.auto_unit(int(i['read_count'] // i['time_since_update']))
rxps = self.auto_unit(int(i['write_count'] // i['time_since_update']))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='read_count', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='write_count', option='decoration')
)
)
else:
# Bitrate
txps = self.auto_unit(int(i['read_bytes'] // i['time_since_update']))
rxps = self.auto_unit(int(i['write_bytes'] // i['time_since_update']))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='read_bytes', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='write_bytes', option='decoration')
)
)
return ret

View File

@ -1,230 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Disk I/O plugin."""
from __future__ import unicode_literals
from glances.globals import nativestr
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Define the history items list
items_history_list = [
{'name': 'read_bytes', 'description': 'Bytes read per second', 'y_unit': 'B/s'},
{'name': 'write_bytes', 'description': 'Bytes write per second', 'y_unit': 'B/s'},
]
class PluginModel(GlancesPluginModel):
"""Glances disks I/O plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
# Hide stats if it has never been != 0
if config is not None:
self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
else:
self.hide_zero = False
self.hide_zero_fields = ['read_bytes', 'write_bytes']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def get_key(self):
"""Return the key of the list."""
return 'disk_name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update disk I/O stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab the stat using the psutil disk_io_counters method
# read_count: number of reads
# write_count: number of writes
# read_bytes: number of bytes read
# write_bytes: number of bytes written
# read_time: time spent reading from disk (in milliseconds)
# write_time: time spent writing to disk (in milliseconds)
try:
diskio = psutil.disk_io_counters(perdisk=True)
except Exception:
return stats
# Previous disk IO stats are stored in the diskio_old variable
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
time_since_update = getTimeSinceLastUpdate('disk')
diskio = diskio
for disk in diskio:
# By default, RamFS is not displayed (issue #714)
if self.args is not None and not self.args.diskio_show_ramfs and disk.startswith('ram'):
continue
# Shall we display the stats ?
if not self.is_display(disk):
continue
# Compute count and bit rate
try:
diskstat = {
'time_since_update': time_since_update,
'disk_name': disk,
'read_count': diskio[disk].read_count - self.diskio_old[disk].read_count,
'write_count': diskio[disk].write_count - self.diskio_old[disk].write_count,
'read_bytes': diskio[disk].read_bytes - self.diskio_old[disk].read_bytes,
'write_bytes': diskio[disk].write_bytes - self.diskio_old[disk].write_bytes,
}
except (KeyError, AttributeError):
diskstat = {
'time_since_update': time_since_update,
'disk_name': disk,
'read_count': 0,
'write_count': 0,
'read_bytes': 0,
'write_bytes': 0,
}
# Add alias if exist (define in the configuration file)
if self.has_alias(disk) is not None:
diskstat['alias'] = self.has_alias(disk)
# Add the dict key
diskstat['key'] = self.get_key()
# Add the current disk stat to the list
stats.append(diskstat)
# Save stats to compute next bitrate
try:
self.diskio_old = diskio
except (IOError, UnboundLocalError):
pass
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard way for the moment...
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Check if the stats should be hidden
self.update_views_hidden()
# Add specifics information
# Alert
for i in self.get_raw():
disk_real_name = i['disk_name']
self.views[i[self.get_key()]]['read_bytes']['decoration'] = self.get_alert(
int(i['read_bytes'] // i['time_since_update']), header=disk_real_name + '_rx'
)
self.views[i[self.get_key()]]['write_bytes']['decoration'] = self.get_alert(
int(i['write_bytes'] // i['time_since_update']), header=disk_real_name + '_tx'
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 13
# Header
msg = '{:{width}}'.format('DISK I/O', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.diskio_iops:
msg = '{:>8}'.format('IOR/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('IOW/s')
ret.append(self.curse_add_line(msg))
else:
msg = '{:>8}'.format('R/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('W/s')
ret.append(self.curse_add_line(msg))
# Disk list (sorted by name)
for i in self.sorted_stats():
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields]):
continue
# Is there an alias for the disk name ?
disk_name = self.has_alias(i['disk_name']) if self.has_alias(i['disk_name']) else i['disk_name']
# New line
ret.append(self.curse_new_line())
if len(disk_name) > name_max_width:
# Cut disk name if it is too long
disk_name = '_' + disk_name[-name_max_width + 1 :]
msg = '{:{width}}'.format(nativestr(disk_name), width=name_max_width + 1)
ret.append(self.curse_add_line(msg))
if args.diskio_iops:
# count
txps = self.auto_unit(int(i['read_count'] // i['time_since_update']))
rxps = self.auto_unit(int(i['write_count'] // i['time_since_update']))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='read_count', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='write_count', option='decoration')
)
)
else:
# Bitrate
txps = self.auto_unit(int(i['read_bytes'] // i['time_since_update']))
rxps = self.auto_unit(int(i['write_bytes'] // i['time_since_update']))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='read_bytes', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='write_bytes', option='decoration')
)
)
return ret

View File

@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Folder plugin."""
from __future__ import unicode_literals
import numbers
from glances.globals import nativestr
from glances.folder_list import FolderList as glancesFolderList
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances folder plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init stats
self.glances_folders = glancesFolderList(config)
def get_key(self):
"""Return the key of the list."""
return 'path'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the folders list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Folder list only available in a full Glances environment
# Check if the glances_folder instance is init
if self.glances_folders is None:
return self.stats
# Update the folders list (result of command)
self.glances_folders.update(key=self.get_key())
# Put it on the stats var
stats = self.glances_folders.get()
else:
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, stat, header=""):
"""Manage limits of the folder list."""
if stat['errno'] != 0:
ret = 'ERROR'
else:
ret = 'OK'
if stat['critical'] is not None and stat['size'] > int(stat['critical']) * 1000000:
ret = 'CRITICAL'
elif stat['warning'] is not None and stat['size'] > int(stat['warning']) * 1000000:
ret = 'WARNING'
elif stat['careful'] is not None and stat['size'] > int(stat['careful']) * 1000000:
ret = 'CAREFUL'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, stat[self.get_key()])
return ret
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Header
msg = '{:{width}}'.format('FOLDERS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
# Data
for i in self.stats:
ret.append(self.curse_new_line())
if len(i['path']) > name_max_width:
# Cut path if it is too long
path = '_' + i['path'][-name_max_width + 1:]
else:
path = i['path']
msg = '{:{width}}'.format(nativestr(path), width=name_max_width)
ret.append(self.curse_add_line(msg))
if i['errno'] != 0:
msg = '?{:>8}'.format(self.auto_unit(i['size']))
else:
msg = '{:>9}'.format(self.auto_unit(i['size']))
ret.append(self.curse_add_line(msg, self.get_alert(i, header='folder_' + i['indice'])))
return ret

View File

@ -1,122 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Folder plugin."""
from __future__ import unicode_literals
import numbers
from glances.globals import nativestr
from glances.folder_list import FolderList as glancesFolderList
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances folder plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init stats
self.glances_folders = glancesFolderList(config)
def get_key(self):
"""Return the key of the list."""
return 'path'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the folders list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Folder list only available in a full Glances environment
# Check if the glances_folder instance is init
if self.glances_folders is None:
return self.stats
# Update the folders list (result of command)
self.glances_folders.update(key=self.get_key())
# Put it on the stats var
stats = self.glances_folders.get()
else:
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, stat, header=""):
"""Manage limits of the folder list."""
if stat['errno'] != 0:
ret = 'ERROR'
else:
ret = 'OK'
if stat['critical'] is not None and stat['size'] > int(stat['critical']) * 1000000:
ret = 'CRITICAL'
elif stat['warning'] is not None and stat['size'] > int(stat['warning']) * 1000000:
ret = 'WARNING'
elif stat['careful'] is not None and stat['size'] > int(stat['careful']) * 1000000:
ret = 'CAREFUL'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, stat[self.get_key()])
return ret
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Header
msg = '{:{width}}'.format('FOLDERS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
# Data
for i in self.stats:
ret.append(self.curse_new_line())
if len(i['path']) > name_max_width:
# Cut path if it is too long
path = '_' + i['path'][-name_max_width + 1:]
else:
path = i['path']
msg = '{:{width}}'.format(nativestr(path), width=name_max_width)
ret.append(self.curse_add_line(msg))
if i['errno'] != 0:
msg = '?{:>8}'.format(self.auto_unit(i['size']))
else:
msg = '{:>9}'.format(self.auto_unit(i['size']))
ret.append(self.curse_add_line(msg, self.get_alert(i, header='folder_' + i['indice'])))
return ret

View File

@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""File system plugin."""
from __future__ import unicode_literals
import operator
from glances.globals import u, nativestr, PermissionError
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# SNMP OID
# The snmpd.conf needs to be edited.
# Add the following to enable it on all disk
# ...
# includeAllDisks 10%
# ...
# The OIDs are as follows (for the first disk)
# Path where the disk is mounted: .1.3.6.1.4.1.2021.9.1.2.1
# Path of the device for the partition: .1.3.6.1.4.1.2021.9.1.3.1
# Total size of the disk/partition (kBytes): .1.3.6.1.4.1.2021.9.1.6.1
# Available space on the disk: .1.3.6.1.4.1.2021.9.1.7.1
# Used space on the disk: .1.3.6.1.4.1.2021.9.1.8.1
# Percentage of space used on disk: .1.3.6.1.4.1.2021.9.1.9.1
# Percentage of inodes used on disk: .1.3.6.1.4.1.2021.9.1.10.1
snmp_oid = {
'default': {
'mnt_point': '1.3.6.1.4.1.2021.9.1.2',
'device_name': '1.3.6.1.4.1.2021.9.1.3',
'size': '1.3.6.1.4.1.2021.9.1.6',
'used': '1.3.6.1.4.1.2021.9.1.8',
'percent': '1.3.6.1.4.1.2021.9.1.9',
},
'windows': {
'mnt_point': '1.3.6.1.2.1.25.2.3.1.3',
'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4',
'size': '1.3.6.1.2.1.25.2.3.1.5',
'used': '1.3.6.1.2.1.25.2.3.1.6',
},
'netapp': {
'mnt_point': '1.3.6.1.4.1.789.1.5.4.1.2',
'device_name': '1.3.6.1.4.1.789.1.5.4.1.10',
'size': '1.3.6.1.4.1.789.1.5.4.1.3',
'used': '1.3.6.1.4.1.789.1.5.4.1.4',
'percent': '1.3.6.1.4.1.789.1.5.4.1.6',
},
}
snmp_oid['esxi'] = snmp_oid['windows']
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'File system usage in percent', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances file system plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
def get_key(self):
"""Return the key of the list."""
return 'mnt_point'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the FS stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab the stats using the psutil disk_partitions
# If 'all'=False return physical devices only (e.g. hard disks, cd-rom drives, USB keys)
# and ignore all others (e.g. memory partitions such as /dev/shm)
try:
fs_stat = psutil.disk_partitions(all=False)
except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil fetch failed")
return self.stats
# Optional hack to allow logical mounts points (issue #448)
allowed_fs_types = self.get_conf_value('allow')
if allowed_fs_types:
# Avoid Psutil call unless mounts need to be allowed
try:
all_mounted_fs = psutil.disk_partitions(all=True)
except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil extended fetch failed")
else:
# Discard duplicates (#2299) and add entries matching allowed fs types
tracked_mnt_points = set(f.mountpoint for f in fs_stat)
for f in all_mounted_fs:
if (
any(f.fstype.find(fs_type) >= 0 for fs_type in allowed_fs_types)
and f.mountpoint not in tracked_mnt_points
):
fs_stat.append(f)
# Loop over fs
for fs in fs_stat:
# Hide the stats if the mount point is in the exclude list
if not self.is_display(fs.mountpoint):
continue
# Grab the disk usage
try:
fs_usage = psutil.disk_usage(fs.mountpoint)
except OSError:
# Correct issue #346
# Disk is ejected during the command
continue
fs_current = {
'device_name': fs.device,
'fs_type': fs.fstype,
# Manage non breaking space (see issue #1065)
'mnt_point': u(fs.mountpoint).replace(u'\u00A0', ' '),
'size': fs_usage.total,
'used': fs_usage.used,
'free': fs_usage.free,
'percent': fs_usage.percent,
'key': self.get_key(),
}
# Hide the stats if the device name is in the exclude list
# Correct issue: glances.conf FS hide not applying #1666
if not self.is_display(fs_current['device_name']):
continue
stats.append(fs_current)
elif self.input_method == 'snmp':
# Update stats using SNMP
# SNMP bulk command to get all file system in one shot
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid['default'], bulk=True)
# Loop over fs
if self.short_system_name in ('windows', 'esxi'):
# Windows or ESXi tips
for fs in fs_stat:
# Memory stats are grabbed in the same OID table (ignore it)
if fs == 'Virtual Memory' or fs == 'Physical Memory' or fs == 'Real Memory':
continue
size = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
used = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
percent = float(used * 100 / size)
fs_current = {
'device_name': '',
'mnt_point': fs.partition(' ')[0],
'size': size,
'used': used,
'percent': percent,
'key': self.get_key(),
}
# Do not take hidden file system into account
if self.is_hide(fs_current['mnt_point']):
continue
else:
stats.append(fs_current)
else:
# Default behavior
for fs in fs_stat:
fs_current = {
'device_name': fs_stat[fs]['device_name'],
'mnt_point': fs,
'size': int(fs_stat[fs]['size']) * 1024,
'used': int(fs_stat[fs]['used']) * 1024,
'percent': float(fs_stat[fs]['percent']),
'key': self.get_key(),
}
# Do not take hidden file system into account
if self.is_hide(fs_current['mnt_point']) or self.is_hide(fs_current['device_name']):
continue
else:
stats.append(fs_current)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
self.views[i[self.get_key()]]['used']['decoration'] = self.get_alert(
current=i['size'] - i['free'], maximum=i['size'], header=i['mnt_point']
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Build the string message
# Header
msg = '{:{width}}'.format('FILE SYS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.fs_free_space:
msg = '{:>7}'.format('Free')
else:
msg = '{:>7}'.format('Used')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Total')
ret.append(self.curse_add_line(msg))
# Filesystem list (sorted by name)
for i in sorted(self.stats, key=operator.itemgetter(self.get_key())):
# New line
ret.append(self.curse_new_line())
if i['device_name'] == '' or i['device_name'] == 'none':
mnt_point = i['mnt_point'][-name_max_width + 1 :]
elif len(i['mnt_point']) + len(i['device_name'].split('/')[-1]) <= name_max_width - 3:
# If possible concatenate mode info... Glances touch inside :)
mnt_point = i['mnt_point'] + ' (' + i['device_name'].split('/')[-1] + ')'
elif len(i['mnt_point']) > name_max_width:
# Cut mount point name if it is too long
mnt_point = '_' + i['mnt_point'][-name_max_width + 1 :]
else:
mnt_point = i['mnt_point']
msg = '{:{width}}'.format(nativestr(mnt_point), width=name_max_width)
ret.append(self.curse_add_line(msg))
if args.fs_free_space:
msg = '{:>7}'.format(self.auto_unit(i['free']))
else:
msg = '{:>7}'.format(self.auto_unit(i['used']))
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='used', option='decoration'))
)
msg = '{:>7}'.format(self.auto_unit(i['size']))
ret.append(self.curse_add_line(msg))
return ret

View File

@ -1,268 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""File system plugin."""
from __future__ import unicode_literals
import operator
from glances.globals import u, nativestr, PermissionError
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# SNMP OID
# The snmpd.conf needs to be edited.
# Add the following to enable it on all disk
# ...
# includeAllDisks 10%
# ...
# The OIDs are as follows (for the first disk)
# Path where the disk is mounted: .1.3.6.1.4.1.2021.9.1.2.1
# Path of the device for the partition: .1.3.6.1.4.1.2021.9.1.3.1
# Total size of the disk/partition (kBytes): .1.3.6.1.4.1.2021.9.1.6.1
# Available space on the disk: .1.3.6.1.4.1.2021.9.1.7.1
# Used space on the disk: .1.3.6.1.4.1.2021.9.1.8.1
# Percentage of space used on disk: .1.3.6.1.4.1.2021.9.1.9.1
# Percentage of inodes used on disk: .1.3.6.1.4.1.2021.9.1.10.1
snmp_oid = {
'default': {
'mnt_point': '1.3.6.1.4.1.2021.9.1.2',
'device_name': '1.3.6.1.4.1.2021.9.1.3',
'size': '1.3.6.1.4.1.2021.9.1.6',
'used': '1.3.6.1.4.1.2021.9.1.8',
'percent': '1.3.6.1.4.1.2021.9.1.9',
},
'windows': {
'mnt_point': '1.3.6.1.2.1.25.2.3.1.3',
'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4',
'size': '1.3.6.1.2.1.25.2.3.1.5',
'used': '1.3.6.1.2.1.25.2.3.1.6',
},
'netapp': {
'mnt_point': '1.3.6.1.4.1.789.1.5.4.1.2',
'device_name': '1.3.6.1.4.1.789.1.5.4.1.10',
'size': '1.3.6.1.4.1.789.1.5.4.1.3',
'used': '1.3.6.1.4.1.789.1.5.4.1.4',
'percent': '1.3.6.1.4.1.789.1.5.4.1.6',
},
}
snmp_oid['esxi'] = snmp_oid['windows']
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'File system usage in percent', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances file system plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
def get_key(self):
"""Return the key of the list."""
return 'mnt_point'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the FS stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab the stats using the psutil disk_partitions
# If 'all'=False return physical devices only (e.g. hard disks, cd-rom drives, USB keys)
# and ignore all others (e.g. memory partitions such as /dev/shm)
try:
fs_stat = psutil.disk_partitions(all=False)
except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil fetch failed")
return self.stats
# Optional hack to allow logical mounts points (issue #448)
allowed_fs_types = self.get_conf_value('allow')
if allowed_fs_types:
# Avoid Psutil call unless mounts need to be allowed
try:
all_mounted_fs = psutil.disk_partitions(all=True)
except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil extended fetch failed")
else:
# Discard duplicates (#2299) and add entries matching allowed fs types
tracked_mnt_points = set(f.mountpoint for f in fs_stat)
for f in all_mounted_fs:
if (
any(f.fstype.find(fs_type) >= 0 for fs_type in allowed_fs_types)
and f.mountpoint not in tracked_mnt_points
):
fs_stat.append(f)
# Loop over fs
for fs in fs_stat:
# Hide the stats if the mount point is in the exclude list
if not self.is_display(fs.mountpoint):
continue
# Grab the disk usage
try:
fs_usage = psutil.disk_usage(fs.mountpoint)
except OSError:
# Correct issue #346
# Disk is ejected during the command
continue
fs_current = {
'device_name': fs.device,
'fs_type': fs.fstype,
# Manage non breaking space (see issue #1065)
'mnt_point': u(fs.mountpoint).replace(u'\u00A0', ' '),
'size': fs_usage.total,
'used': fs_usage.used,
'free': fs_usage.free,
'percent': fs_usage.percent,
'key': self.get_key(),
}
# Hide the stats if the device name is in the exclude list
# Correct issue: glances.conf FS hide not applying #1666
if not self.is_display(fs_current['device_name']):
continue
stats.append(fs_current)
elif self.input_method == 'snmp':
# Update stats using SNMP
# SNMP bulk command to get all file system in one shot
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid['default'], bulk=True)
# Loop over fs
if self.short_system_name in ('windows', 'esxi'):
# Windows or ESXi tips
for fs in fs_stat:
# Memory stats are grabbed in the same OID table (ignore it)
if fs == 'Virtual Memory' or fs == 'Physical Memory' or fs == 'Real Memory':
continue
size = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
used = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
percent = float(used * 100 / size)
fs_current = {
'device_name': '',
'mnt_point': fs.partition(' ')[0],
'size': size,
'used': used,
'percent': percent,
'key': self.get_key(),
}
# Do not take hidden file system into account
if self.is_hide(fs_current['mnt_point']):
continue
else:
stats.append(fs_current)
else:
# Default behavior
for fs in fs_stat:
fs_current = {
'device_name': fs_stat[fs]['device_name'],
'mnt_point': fs,
'size': int(fs_stat[fs]['size']) * 1024,
'used': int(fs_stat[fs]['used']) * 1024,
'percent': float(fs_stat[fs]['percent']),
'key': self.get_key(),
}
# Do not take hidden file system into account
if self.is_hide(fs_current['mnt_point']) or self.is_hide(fs_current['device_name']):
continue
else:
stats.append(fs_current)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
self.views[i[self.get_key()]]['used']['decoration'] = self.get_alert(
current=i['size'] - i['free'], maximum=i['size'], header=i['mnt_point']
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Build the string message
# Header
msg = '{:{width}}'.format('FILE SYS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.fs_free_space:
msg = '{:>7}'.format('Free')
else:
msg = '{:>7}'.format('Used')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Total')
ret.append(self.curse_add_line(msg))
# Filesystem list (sorted by name)
for i in sorted(self.stats, key=operator.itemgetter(self.get_key())):
# New line
ret.append(self.curse_new_line())
if i['device_name'] == '' or i['device_name'] == 'none':
mnt_point = i['mnt_point'][-name_max_width + 1 :]
elif len(i['mnt_point']) + len(i['device_name'].split('/')[-1]) <= name_max_width - 3:
# If possible concatenate mode info... Glances touch inside :)
mnt_point = i['mnt_point'] + ' (' + i['device_name'].split('/')[-1] + ')'
elif len(i['mnt_point']) > name_max_width:
# Cut mount point name if it is too long
mnt_point = '_' + i['mnt_point'][-name_max_width + 1 :]
else:
mnt_point = i['mnt_point']
msg = '{:{width}}'.format(nativestr(mnt_point), width=name_max_width)
ret.append(self.curse_add_line(msg))
if args.fs_free_space:
msg = '{:>7}'.format(self.auto_unit(i['free']))
else:
msg = '{:>7}'.format(self.auto_unit(i['used']))
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='used', option='decoration'))
)
msg = '{:>7}'.format(self.auto_unit(i['size']))
ret.append(self.curse_add_line(msg))
return ret

View File

@ -0,0 +1,347 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2020 Kirby Banman <kirby.banman@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""GPU plugin (limited to NVIDIA chipsets)."""
from glances.globals import nativestr, to_fahrenheit
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# In Glances 3.1.4 or higher, we use the py3nvml lib (see issue #1523)
try:
import py3nvml.py3nvml as pynvml
except Exception as e:
import_error_tag = True
# Display debug message if import KeyError
logger.warning("Missing Python Lib ({}), Nvidia GPU plugin is disabled".format(e))
else:
import_error_tag = False
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [
{'name': 'proc', 'description': 'GPU processor', 'y_unit': '%'},
{'name': 'mem', 'description': 'Memory consumption', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances GPU plugin (limited to NVIDIA chipsets).
stats is a list of dictionaries with one entry per GPU
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args,
config=config,
items_history_list=items_history_list,
stats_init_value=[])
# Init the Nvidia API
self.init_nvidia()
# We want to display the stat in the curse interface
self.display_curse = True
def init_nvidia(self):
"""Init the NVIDIA API."""
if import_error_tag:
self.nvml_ready = False
try:
pynvml.nvmlInit()
self.device_handles = get_device_handles()
self.nvml_ready = True
except Exception:
logger.debug("pynvml could not be initialized.")
self.nvml_ready = False
return self.nvml_ready
def get_key(self):
"""Return the key of the list."""
return 'gpu_id'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the GPU stats."""
# Init new stats
stats = self.get_init_value()
if not self.nvml_ready:
# !!!
# Uncomment to test on computer without GPU
# One GPU sample:
# self.stats = [
# {
# "key": "gpu_id",
# "gpu_id": 0,
# "name": "Fake GeForce GTX",
# "mem": 5.792331695556641,
# "proc": 4,
# "temperature": 26,
# "fan_speed": 30
# }
# ]
# Two GPU sample:
# self.stats = [
# {
# "key": "gpu_id",
# "gpu_id": 0,
# "name": "Fake GeForce GTX1",
# "mem": 5.792331695556641,
# "proc": 4,
# "temperature": 26,
# "fan_speed": 30
# },
# {
# "key": "gpu_id",
# "gpu_id": 1,
# "name": "Fake GeForce GTX2",
# "mem": 15,
# "proc": 8,
# "temperature": 65,
# "fan_speed": 75
# }
# ]
return self.stats
if self.input_method == 'local':
stats = self.get_device_stats()
elif self.input_method == 'snmp':
# not available
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
# Init the views for the current GPU
self.views[i[self.get_key()]] = {'proc': {}, 'mem': {}, 'temperature': {}}
# Processor alert
if 'proc' in i:
alert = self.get_alert(i['proc'], header='proc')
self.views[i[self.get_key()]]['proc']['decoration'] = alert
# Memory alert
if 'mem' in i:
alert = self.get_alert(i['mem'], header='mem')
self.views[i[self.get_key()]]['mem']['decoration'] = alert
# Temperature alert
if 'temperature' in i:
alert = self.get_alert(i['temperature'], header='temperature')
self.views[i[self.get_key()]]['temperature']['decoration'] = alert
return True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist, not empty (issue #871) and plugin not disabled
if not self.stats or (self.stats == []) or self.is_disabled():
return ret
# Check if all GPU have the same name
same_name = all(s['name'] == self.stats[0]['name'] for s in self.stats)
# gpu_stats contain the first GPU in the list
gpu_stats = self.stats[0]
# Header
header = ''
if len(self.stats) > 1:
header += '{} '.format(len(self.stats))
if same_name:
header += '{} {}'.format('GPU', gpu_stats['name'])
else:
header += '{}'.format('GPU')
msg = header[:17]
ret.append(self.curse_add_line(msg, "TITLE"))
# Build the string message
if len(self.stats) == 1 or args.meangpu:
# GPU stat summary or mono GPU
# New line
ret.append(self.curse_new_line())
# GPU PROC
try:
mean_proc = sum(s['proc'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_proc_msg = '{:>4}'.format('N/A')
else:
mean_proc_msg = '{:>3.0f}%'.format(mean_proc)
if len(self.stats) > 1:
msg = '{:13}'.format('proc mean:')
else:
msg = '{:13}'.format('proc:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_proc_msg, self.get_views(item=gpu_stats[self.get_key()], key='proc', option='decoration')
)
)
# New line
ret.append(self.curse_new_line())
# GPU MEM
try:
mean_mem = sum(s['mem'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_mem_msg = '{:>4}'.format('N/A')
else:
mean_mem_msg = '{:>3.0f}%'.format(mean_mem)
if len(self.stats) > 1:
msg = '{:13}'.format('mem mean:')
else:
msg = '{:13}'.format('mem:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_mem_msg, self.get_views(item=gpu_stats[self.get_key()], key='mem', option='decoration')
)
)
# New line
ret.append(self.curse_new_line())
# GPU TEMPERATURE
try:
mean_temperature = sum(s['temperature'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_temperature_msg = '{:>4}'.format('N/A')
else:
unit = 'C'
if args.fahrenheit:
mean_temperature = to_fahrenheit(mean_temperature)
unit = 'F'
mean_temperature_msg = '{:>3.0f}{}'.format(mean_temperature, unit)
if len(self.stats) > 1:
msg = '{:13}'.format('temp mean:')
else:
msg = '{:13}'.format('temperature:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_temperature_msg,
self.get_views(item=gpu_stats[self.get_key()], key='temperature', option='decoration'),
)
)
else:
# Multi GPU
# Temperature is not displayed in this mode...
for gpu_stats in self.stats:
# New line
ret.append(self.curse_new_line())
# GPU ID + PROC + MEM + TEMPERATURE
id_msg = '{}'.format(gpu_stats['gpu_id'])
try:
proc_msg = '{:>3.0f}%'.format(gpu_stats['proc'])
except (ValueError, TypeError):
proc_msg = '{:>4}'.format('N/A')
try:
mem_msg = '{:>3.0f}%'.format(gpu_stats['mem'])
except (ValueError, TypeError):
mem_msg = '{:>4}'.format('N/A')
msg = '{}: {} mem: {}'.format(id_msg, proc_msg, mem_msg)
ret.append(self.curse_add_line(msg))
return ret
def get_device_stats(self):
"""Get GPU stats."""
stats = []
for index, device_handle in enumerate(self.device_handles):
device_stats = dict()
# Dictionary key is the GPU_ID
device_stats['key'] = self.get_key()
# GPU id (for multiple GPU, start at 0)
device_stats['gpu_id'] = index
# GPU name
device_stats['name'] = get_device_name(device_handle)
# Memory consumption in % (not available on all GPU)
device_stats['mem'] = get_mem(device_handle)
# Processor consumption in %
device_stats['proc'] = get_proc(device_handle)
# Processor temperature in °C
device_stats['temperature'] = get_temperature(device_handle)
# Fan speed in %
device_stats['fan_speed'] = get_fan_speed(device_handle)
stats.append(device_stats)
return stats
def exit(self):
"""Overwrite the exit method to close the GPU API."""
if self.nvml_ready:
try:
pynvml.nvmlShutdown()
except Exception as e:
logger.debug("pynvml failed to shutdown correctly ({})".format(e))
# Call the father exit method
super(PluginModel, self).exit()
def get_device_handles():
"""Get a list of NVML device handles, one per device.
Can throw NVMLError.
"""
return [pynvml.nvmlDeviceGetHandleByIndex(i) for i in range(pynvml.nvmlDeviceGetCount())]
def get_device_name(device_handle):
"""Get GPU device name."""
try:
return nativestr(pynvml.nvmlDeviceGetName(device_handle))
except pynvml.NVMLError:
return "NVIDIA"
def get_mem(device_handle):
"""Get GPU device memory consumption in percent."""
try:
memory_info = pynvml.nvmlDeviceGetMemoryInfo(device_handle)
return memory_info.used * 100.0 / memory_info.total
except pynvml.NVMLError:
return None
def get_proc(device_handle):
"""Get GPU device CPU consumption in percent."""
try:
return pynvml.nvmlDeviceGetUtilizationRates(device_handle).gpu
except pynvml.NVMLError:
return None
def get_temperature(device_handle):
"""Get GPU device CPU temperature in Celsius."""
try:
return pynvml.nvmlDeviceGetTemperature(device_handle, pynvml.NVML_TEMPERATURE_GPU)
except pynvml.NVMLError:
return None
def get_fan_speed(device_handle):
"""Get GPU device fan speed in percent."""
try:
return pynvml.nvmlDeviceGetFanSpeed(device_handle)
except pynvml.NVMLError:
return None

View File

@ -1,347 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2020 Kirby Banman <kirby.banman@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""GPU plugin (limited to NVIDIA chipsets)."""
from glances.globals import nativestr, to_fahrenheit
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# In Glances 3.1.4 or higher, we use the py3nvml lib (see issue #1523)
try:
import py3nvml.py3nvml as pynvml
except Exception as e:
import_error_tag = True
# Display debug message if import KeyError
logger.warning("Missing Python Lib ({}), Nvidia GPU plugin is disabled".format(e))
else:
import_error_tag = False
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [
{'name': 'proc', 'description': 'GPU processor', 'y_unit': '%'},
{'name': 'mem', 'description': 'Memory consumption', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances GPU plugin (limited to NVIDIA chipsets).
stats is a list of dictionaries with one entry per GPU
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args,
config=config,
items_history_list=items_history_list,
stats_init_value=[])
# Init the Nvidia API
self.init_nvidia()
# We want to display the stat in the curse interface
self.display_curse = True
def init_nvidia(self):
"""Init the NVIDIA API."""
if import_error_tag:
self.nvml_ready = False
try:
pynvml.nvmlInit()
self.device_handles = get_device_handles()
self.nvml_ready = True
except Exception:
logger.debug("pynvml could not be initialized.")
self.nvml_ready = False
return self.nvml_ready
def get_key(self):
"""Return the key of the list."""
return 'gpu_id'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the GPU stats."""
# Init new stats
stats = self.get_init_value()
if not self.nvml_ready:
# !!!
# Uncomment to test on computer without GPU
# One GPU sample:
# self.stats = [
# {
# "key": "gpu_id",
# "gpu_id": 0,
# "name": "Fake GeForce GTX",
# "mem": 5.792331695556641,
# "proc": 4,
# "temperature": 26,
# "fan_speed": 30
# }
# ]
# Two GPU sample:
# self.stats = [
# {
# "key": "gpu_id",
# "gpu_id": 0,
# "name": "Fake GeForce GTX1",
# "mem": 5.792331695556641,
# "proc": 4,
# "temperature": 26,
# "fan_speed": 30
# },
# {
# "key": "gpu_id",
# "gpu_id": 1,
# "name": "Fake GeForce GTX2",
# "mem": 15,
# "proc": 8,
# "temperature": 65,
# "fan_speed": 75
# }
# ]
return self.stats
if self.input_method == 'local':
stats = self.get_device_stats()
elif self.input_method == 'snmp':
# not available
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
# Init the views for the current GPU
self.views[i[self.get_key()]] = {'proc': {}, 'mem': {}, 'temperature': {}}
# Processor alert
if 'proc' in i:
alert = self.get_alert(i['proc'], header='proc')
self.views[i[self.get_key()]]['proc']['decoration'] = alert
# Memory alert
if 'mem' in i:
alert = self.get_alert(i['mem'], header='mem')
self.views[i[self.get_key()]]['mem']['decoration'] = alert
# Temperature alert
if 'temperature' in i:
alert = self.get_alert(i['temperature'], header='temperature')
self.views[i[self.get_key()]]['temperature']['decoration'] = alert
return True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist, not empty (issue #871) and plugin not disabled
if not self.stats or (self.stats == []) or self.is_disabled():
return ret
# Check if all GPU have the same name
same_name = all(s['name'] == self.stats[0]['name'] for s in self.stats)
# gpu_stats contain the first GPU in the list
gpu_stats = self.stats[0]
# Header
header = ''
if len(self.stats) > 1:
header += '{} '.format(len(self.stats))
if same_name:
header += '{} {}'.format('GPU', gpu_stats['name'])
else:
header += '{}'.format('GPU')
msg = header[:17]
ret.append(self.curse_add_line(msg, "TITLE"))
# Build the string message
if len(self.stats) == 1 or args.meangpu:
# GPU stat summary or mono GPU
# New line
ret.append(self.curse_new_line())
# GPU PROC
try:
mean_proc = sum(s['proc'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_proc_msg = '{:>4}'.format('N/A')
else:
mean_proc_msg = '{:>3.0f}%'.format(mean_proc)
if len(self.stats) > 1:
msg = '{:13}'.format('proc mean:')
else:
msg = '{:13}'.format('proc:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_proc_msg, self.get_views(item=gpu_stats[self.get_key()], key='proc', option='decoration')
)
)
# New line
ret.append(self.curse_new_line())
# GPU MEM
try:
mean_mem = sum(s['mem'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_mem_msg = '{:>4}'.format('N/A')
else:
mean_mem_msg = '{:>3.0f}%'.format(mean_mem)
if len(self.stats) > 1:
msg = '{:13}'.format('mem mean:')
else:
msg = '{:13}'.format('mem:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_mem_msg, self.get_views(item=gpu_stats[self.get_key()], key='mem', option='decoration')
)
)
# New line
ret.append(self.curse_new_line())
# GPU TEMPERATURE
try:
mean_temperature = sum(s['temperature'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_temperature_msg = '{:>4}'.format('N/A')
else:
unit = 'C'
if args.fahrenheit:
mean_temperature = to_fahrenheit(mean_temperature)
unit = 'F'
mean_temperature_msg = '{:>3.0f}{}'.format(mean_temperature, unit)
if len(self.stats) > 1:
msg = '{:13}'.format('temp mean:')
else:
msg = '{:13}'.format('temperature:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_temperature_msg,
self.get_views(item=gpu_stats[self.get_key()], key='temperature', option='decoration'),
)
)
else:
# Multi GPU
# Temperature is not displayed in this mode...
for gpu_stats in self.stats:
# New line
ret.append(self.curse_new_line())
# GPU ID + PROC + MEM + TEMPERATURE
id_msg = '{}'.format(gpu_stats['gpu_id'])
try:
proc_msg = '{:>3.0f}%'.format(gpu_stats['proc'])
except (ValueError, TypeError):
proc_msg = '{:>4}'.format('N/A')
try:
mem_msg = '{:>3.0f}%'.format(gpu_stats['mem'])
except (ValueError, TypeError):
mem_msg = '{:>4}'.format('N/A')
msg = '{}: {} mem: {}'.format(id_msg, proc_msg, mem_msg)
ret.append(self.curse_add_line(msg))
return ret
def get_device_stats(self):
"""Get GPU stats."""
stats = []
for index, device_handle in enumerate(self.device_handles):
device_stats = dict()
# Dictionary key is the GPU_ID
device_stats['key'] = self.get_key()
# GPU id (for multiple GPU, start at 0)
device_stats['gpu_id'] = index
# GPU name
device_stats['name'] = get_device_name(device_handle)
# Memory consumption in % (not available on all GPU)
device_stats['mem'] = get_mem(device_handle)
# Processor consumption in %
device_stats['proc'] = get_proc(device_handle)
# Processor temperature in °C
device_stats['temperature'] = get_temperature(device_handle)
# Fan speed in %
device_stats['fan_speed'] = get_fan_speed(device_handle)
stats.append(device_stats)
return stats
def exit(self):
"""Overwrite the exit method to close the GPU API."""
if self.nvml_ready:
try:
pynvml.nvmlShutdown()
except Exception as e:
logger.debug("pynvml failed to shutdown correctly ({})".format(e))
# Call the father exit method
super(PluginModel, self).exit()
def get_device_handles():
"""Get a list of NVML device handles, one per device.
Can throw NVMLError.
"""
return [pynvml.nvmlDeviceGetHandleByIndex(i) for i in range(pynvml.nvmlDeviceGetCount())]
def get_device_name(device_handle):
"""Get GPU device name."""
try:
return nativestr(pynvml.nvmlDeviceGetName(device_handle))
except pynvml.NVMLError:
return "NVIDIA"
def get_mem(device_handle):
"""Get GPU device memory consumption in percent."""
try:
memory_info = pynvml.nvmlDeviceGetMemoryInfo(device_handle)
return memory_info.used * 100.0 / memory_info.total
except pynvml.NVMLError:
return None
def get_proc(device_handle):
"""Get GPU device CPU consumption in percent."""
try:
return pynvml.nvmlDeviceGetUtilizationRates(device_handle).gpu
except pynvml.NVMLError:
return None
def get_temperature(device_handle):
"""Get GPU device CPU temperature in Celsius."""
try:
return pynvml.nvmlDeviceGetTemperature(device_handle, pynvml.NVML_TEMPERATURE_GPU)
except pynvml.NVMLError:
return None
def get_fan_speed(device_handle):
"""Get GPU device fan speed in percent."""
try:
return pynvml.nvmlDeviceGetFanSpeed(device_handle)
except pynvml.NVMLError:
return None

View File

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""
Help plugin.
Just a stupid plugin to display the help screen.
"""
import sys
from glances.globals import iteritems
from glances import __version__, psutil_version
from glances.plugins.plugin.model import GlancesPluginModel
from itertools import chain
class PluginModel(GlancesPluginModel):
"""Glances help plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# Set the config instance
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# init data dictionary, to preserve insertion order
if sys.version_info < (3, 6):
from collections import OrderedDict
self.view_data = OrderedDict()
else:
self.view_data = {}
self.generate_view_data()
def reset(self):
"""No stats. It is just a plugin to display the help."""
def update(self):
"""No stats. It is just a plugin to display the help."""
def generate_view_data(self):
"""Generate the views."""
self.view_data['version'] = '{} {}'.format('Glances', __version__)
self.view_data['psutil_version'] = ' with psutil {}'.format(psutil_version)
try:
self.view_data['configuration_file'] = 'Configuration file: {}'.format(self.config.loaded_config_file)
except AttributeError:
pass
msg_col = ' {0:1} {1:34}'
msg_header = '{0:39}'
self.view_data.update(
[
# First column
#
('header_sort', msg_header.format('SORT PROCESSES:')),
('sort_auto', msg_col.format('a', 'Automatically')),
('sort_cpu', msg_col.format('c', 'CPU%')),
('sort_io_rate', msg_col.format('i', 'I/O rate')),
('sort_mem', msg_col.format('m', 'MEM%')),
('sort_process_name', msg_col.format('p', 'Process name')),
('sort_cpu_times', msg_col.format('t', 'TIME')),
('sort_user', msg_col.format('u', 'USER')),
('header_show_hide', msg_header.format('SHOW/HIDE SECTION:')),
('show_hide_application_monitoring', msg_col.format('A', 'Application monitoring')),
('show_hide_diskio', msg_col.format('d', 'Disk I/O')),
('show_hide_docker', msg_col.format('D', 'Docker')),
('show_hide_top_extended_stats', msg_col.format('e', 'Top extended stats')),
('show_hide_filesystem', msg_col.format('f', 'Filesystem')),
('show_hide_gpu', msg_col.format('G', 'GPU')),
('show_hide_ip', msg_col.format('I', 'IP')),
('show_hide_tcp_connection', msg_col.format('K', 'TCP')),
('show_hide_alert', msg_col.format('l', 'Alert logs')),
('show_hide_network', msg_col.format('n', 'Network')),
('show_hide_current_time', msg_col.format('N', 'Time')),
('show_hide_irq', msg_col.format('Q', 'IRQ')),
('show_hide_raid_plugin', msg_col.format('R', 'RAID')),
('show_hide_sensors', msg_col.format('s', 'Sensors')),
('show_hide_wifi_module', msg_col.format('W', 'Wifi')),
('show_hide_processes', msg_col.format('z', 'Processes')),
('show_hide_left_sidebar', msg_col.format('2', 'Left sidebar')),
# Second column
#
('show_hide_quick_look', msg_col.format('3', 'Quick Look')),
('show_hide_cpu_mem_swap', msg_col.format('4', 'CPU, MEM, and SWAP')),
('show_hide_all', msg_col.format('5', 'ALL')),
('header_toggle', msg_header.format('TOGGLE DATA TYPE:')),
('toggle_bits_bytes', msg_col.format('b', 'Network I/O, bits/bytes')),
('toggle_count_rate', msg_col.format('B', 'Disk I/O, count/rate')),
('toggle_used_free', msg_col.format('F', 'Filesystem space, used/free')),
('toggle_bar_sparkline', msg_col.format('S', 'Quick Look, bar/sparkline')),
('toggle_separate_combined', msg_col.format('T', 'Network I/O, separate/combined')),
('toggle_live_cumulative', msg_col.format('U', 'Network I/O, live/cumulative')),
('toggle_linux_percentage', msg_col.format('0', 'Load, Linux/percentage')),
('toggle_cpu_individual_combined', msg_col.format('1', 'CPU, individual/combined')),
('toggle_gpu_individual_combined', msg_col.format('6', 'GPU, individual/combined')),
('toggle_short_full', msg_col.format('/', 'Process names, short/full')),
('header_miscellaneous', msg_header.format('MISCELLANEOUS:')),
('misc_erase_process_filter', msg_col.format('E', 'Erase process filter')),
('misc_generate_history_graphs', msg_col.format('g', 'Generate history graphs')),
('misc_help', msg_col.format('h', 'HELP')),
('misc_accumulate_processes_by_program', msg_col.format('j', 'Display threads or programs')),
('misc_increase_nice_process', msg_col.format('+', 'Increase nice process')),
('misc_decrease_nice_process', msg_col.format('-', 'Decrease nice process (need admin rights)')),
('misc_kill_process', msg_col.format('k', 'Kill process')),
('misc_reset_processes_summary_min_max', msg_col.format('M', 'Reset processes summary min/max')),
('misc_quit', msg_col.format('q', 'QUIT (or Esc or Ctrl-C)')),
('misc_reset_history', msg_col.format('r', 'Reset history')),
('misc_delete_warning_alerts', msg_col.format('w', 'Delete warning alerts')),
('misc_delete_warning_and_critical_alerts', msg_col.format('x', 'Delete warning & critical alerts')),
('misc_theme_white', msg_col.format('9', 'Optimize colors for white background')),
('misc_edit_process_filter_pattern', ' ENTER: Edit process filter pattern'),
]
)
def get_view_data(self, args=None):
"""Return the view."""
return self.view_data
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the curse interface."""
# Init the return message
ret = []
# Build the header message
ret.append(self.curse_add_line(self.view_data['version'], 'TITLE'))
ret.append(self.curse_add_line(self.view_data['psutil_version']))
ret.append(self.curse_new_line())
# Build the configuration file path
if 'configuration_file' in self.view_data:
ret.append(self.curse_add_line(self.view_data['configuration_file']))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
# key-shortcuts
#
# Collect all values after the 1st key-msg
# in a list of curse-lines.
#
shortcuts = []
collecting = False
for k, v in iteritems(self.view_data):
if collecting:
pass
elif k == 'header_sort':
collecting = True
else:
continue
shortcuts.append(self.curse_add_line(v))
# Divide shortcuts into 2 columns
# and if number of schortcuts is even,
# make the 1st column taller (len+1).
#
nlines = (len(shortcuts) + 1) // 2
ret.extend(
msg
for triplet in zip(
iter(shortcuts[:nlines]),
chain(shortcuts[nlines:], iter(lambda: self.curse_add_line(''), None)),
iter(self.curse_new_line, None),
)
for msg in triplet
)
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('For an exhaustive list of key bindings:'))
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('https://glances.readthedocs.io/en/latest/cmds.html#interactive-commands'))
# Return the message with decoration
return ret

View File

@ -1,183 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""
Help plugin.
Just a stupid plugin to display the help screen.
"""
import sys
from glances.globals import iteritems
from glances import __version__, psutil_version
from glances.plugins.plugin.model import GlancesPluginModel
from itertools import chain
class PluginModel(GlancesPluginModel):
"""Glances help plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# Set the config instance
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# init data dictionary, to preserve insertion order
if sys.version_info < (3, 6):
from collections import OrderedDict
self.view_data = OrderedDict()
else:
self.view_data = {}
self.generate_view_data()
def reset(self):
"""No stats. It is just a plugin to display the help."""
def update(self):
"""No stats. It is just a plugin to display the help."""
def generate_view_data(self):
"""Generate the views."""
self.view_data['version'] = '{} {}'.format('Glances', __version__)
self.view_data['psutil_version'] = ' with psutil {}'.format(psutil_version)
try:
self.view_data['configuration_file'] = 'Configuration file: {}'.format(self.config.loaded_config_file)
except AttributeError:
pass
msg_col = ' {0:1} {1:34}'
msg_header = '{0:39}'
self.view_data.update(
[
# First column
#
('header_sort', msg_header.format('SORT PROCESSES:')),
('sort_auto', msg_col.format('a', 'Automatically')),
('sort_cpu', msg_col.format('c', 'CPU%')),
('sort_io_rate', msg_col.format('i', 'I/O rate')),
('sort_mem', msg_col.format('m', 'MEM%')),
('sort_process_name', msg_col.format('p', 'Process name')),
('sort_cpu_times', msg_col.format('t', 'TIME')),
('sort_user', msg_col.format('u', 'USER')),
('header_show_hide', msg_header.format('SHOW/HIDE SECTION:')),
('show_hide_application_monitoring', msg_col.format('A', 'Application monitoring')),
('show_hide_diskio', msg_col.format('d', 'Disk I/O')),
('show_hide_docker', msg_col.format('D', 'Docker')),
('show_hide_top_extended_stats', msg_col.format('e', 'Top extended stats')),
('show_hide_filesystem', msg_col.format('f', 'Filesystem')),
('show_hide_gpu', msg_col.format('G', 'GPU')),
('show_hide_ip', msg_col.format('I', 'IP')),
('show_hide_tcp_connection', msg_col.format('K', 'TCP')),
('show_hide_alert', msg_col.format('l', 'Alert logs')),
('show_hide_network', msg_col.format('n', 'Network')),
('show_hide_current_time', msg_col.format('N', 'Time')),
('show_hide_irq', msg_col.format('Q', 'IRQ')),
('show_hide_raid_plugin', msg_col.format('R', 'RAID')),
('show_hide_sensors', msg_col.format('s', 'Sensors')),
('show_hide_wifi_module', msg_col.format('W', 'Wifi')),
('show_hide_processes', msg_col.format('z', 'Processes')),
('show_hide_left_sidebar', msg_col.format('2', 'Left sidebar')),
# Second column
#
('show_hide_quick_look', msg_col.format('3', 'Quick Look')),
('show_hide_cpu_mem_swap', msg_col.format('4', 'CPU, MEM, and SWAP')),
('show_hide_all', msg_col.format('5', 'ALL')),
('header_toggle', msg_header.format('TOGGLE DATA TYPE:')),
('toggle_bits_bytes', msg_col.format('b', 'Network I/O, bits/bytes')),
('toggle_count_rate', msg_col.format('B', 'Disk I/O, count/rate')),
('toggle_used_free', msg_col.format('F', 'Filesystem space, used/free')),
('toggle_bar_sparkline', msg_col.format('S', 'Quick Look, bar/sparkline')),
('toggle_separate_combined', msg_col.format('T', 'Network I/O, separate/combined')),
('toggle_live_cumulative', msg_col.format('U', 'Network I/O, live/cumulative')),
('toggle_linux_percentage', msg_col.format('0', 'Load, Linux/percentage')),
('toggle_cpu_individual_combined', msg_col.format('1', 'CPU, individual/combined')),
('toggle_gpu_individual_combined', msg_col.format('6', 'GPU, individual/combined')),
('toggle_short_full', msg_col.format('/', 'Process names, short/full')),
('header_miscellaneous', msg_header.format('MISCELLANEOUS:')),
('misc_erase_process_filter', msg_col.format('E', 'Erase process filter')),
('misc_generate_history_graphs', msg_col.format('g', 'Generate history graphs')),
('misc_help', msg_col.format('h', 'HELP')),
('misc_accumulate_processes_by_program', msg_col.format('j', 'Display threads or programs')),
('misc_increase_nice_process', msg_col.format('+', 'Increase nice process')),
('misc_decrease_nice_process', msg_col.format('-', 'Decrease nice process (need admin rights)')),
('misc_kill_process', msg_col.format('k', 'Kill process')),
('misc_reset_processes_summary_min_max', msg_col.format('M', 'Reset processes summary min/max')),
('misc_quit', msg_col.format('q', 'QUIT (or Esc or Ctrl-C)')),
('misc_reset_history', msg_col.format('r', 'Reset history')),
('misc_delete_warning_alerts', msg_col.format('w', 'Delete warning alerts')),
('misc_delete_warning_and_critical_alerts', msg_col.format('x', 'Delete warning & critical alerts')),
('misc_theme_white', msg_col.format('9', 'Optimize colors for white background')),
('misc_edit_process_filter_pattern', ' ENTER: Edit process filter pattern'),
]
)
def get_view_data(self, args=None):
"""Return the view."""
return self.view_data
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the curse interface."""
# Init the return message
ret = []
# Build the header message
ret.append(self.curse_add_line(self.view_data['version'], 'TITLE'))
ret.append(self.curse_add_line(self.view_data['psutil_version']))
ret.append(self.curse_new_line())
# Build the configuration file path
if 'configuration_file' in self.view_data:
ret.append(self.curse_add_line(self.view_data['configuration_file']))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
# key-shortcuts
#
# Collect all values after the 1st key-msg
# in a list of curse-lines.
#
shortcuts = []
collecting = False
for k, v in iteritems(self.view_data):
if collecting:
pass
elif k == 'header_sort':
collecting = True
else:
continue
shortcuts.append(self.curse_add_line(v))
# Divide shortcuts into 2 columns
# and if number of schortcuts is even,
# make the 1st column taller (len+1).
#
nlines = (len(shortcuts) + 1) // 2
ret.extend(
msg
for triplet in zip(
iter(shortcuts[:nlines]),
chain(shortcuts[nlines:], iter(lambda: self.curse_add_line(''), None)),
iter(self.curse_new_line, None),
)
for msg in triplet
)
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('For an exhaustive list of key bindings:'))
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('https://glances.readthedocs.io/en/latest/cmds.html#interactive-commands'))
# Return the message with decoration
return ret

View File

@ -0,0 +1,300 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""IP plugin."""
import threading
from ujson import loads
from glances.globals import urlopen, queue, urlopen_auth
from glances.logger import logger
from glances.timer import Timer
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
# Import plugin specific dependency
try:
import netifaces
except ImportError as e:
import_error_tag = True
logger.warning("Missing Python Lib ({}), IP plugin is disabled".format(e))
else:
import_error_tag = False
# List of online services to retrieve public IP address
# List of tuple (url, json, key)
# - url: URL of the Web site
# - json: service return a JSON (True) or string (False)
# - key: key of the IP address in the JSON structure
urls = [
('https://httpbin.org/ip', True, 'origin'),
('https://api.ipify.org/?format=json', True, 'ip'),
('https://ipv4.jsonip.com', True, 'ip'),
]
class PluginModel(GlancesPluginModel):
"""Glances IP Plugin.
stats is a dict
"""
_default_public_refresh_interval = 300
_default_public_ip_disabled = ["False"]
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# For public IP address
self.public_address = ""
self.public_address_refresh_interval = self.get_conf_value(
"public_refresh_interval", default=self._default_public_refresh_interval
)
public_ip_disabled = self.get_conf_value("public_ip_disabled", default=self._default_public_ip_disabled)
self.public_ip_disabled = True if public_ip_disabled == ["True"] else False
# For the Censys options (see issue #2105)
self.public_info = ""
self.censys_url = self.get_conf_value("censys_url", default=[None])[0]
self.censys_username = self.get_conf_value("censys_username", default=[None])[0]
self.censys_password = self.get_conf_value("censys_password", default=[None])[0]
self.censys_fields = self.get_conf_value("censys_fields", default=[None])
self.public_info_disabled = (
self.censys_url is None
or self.censys_username is None
or self.censys_password is None
or self.censys_fields is None
)
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update IP stats using the input method.
:return: the stats dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local' and not import_error_tag:
# Update stats using the netifaces lib
# Start with the default IP gateway
try:
default_gw = netifaces.gateways()['default'][netifaces.AF_INET]
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab default gateway IP address ({})".format(e))
return {}
else:
stats['gateway'] = default_gw[0]
# Then the private IP address
# If multiple IP addresses are available, only the one with the default gateway is returned
try:
address = netifaces.ifaddresses(default_gw[1])[netifaces.AF_INET][0]['addr']
mask = netifaces.ifaddresses(default_gw[1])[netifaces.AF_INET][0]['netmask']
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab private IP address ({})".format(e))
return {}
else:
stats['address'] = address
stats['mask'] = mask
stats['mask_cidr'] = self.ip_to_cidr(stats['mask'])
# Finally with the public IP address
time_since_update = getTimeSinceLastUpdate('public-ip')
try:
if not self.public_ip_disabled and (
self.stats.get('address') != address or time_since_update > self.public_address_refresh_interval
):
self.public_address = PublicIpAddress().get()
if not self.public_info_disabled:
self.public_info = PublicIpInfo(
self.public_address, self.censys_url, self.censys_username, self.censys_password
).get()
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab public IP information ({})".format(e))
else:
stats['public_address'] = self.public_address
# Too much information provided in the public_info
# Limit it to public_info_for_human
# stats['public_info'] = self.public_info
stats['public_info_human'] = self.public_info_for_human(self.public_info)
elif self.input_method == 'snmp':
# Not implemented yet
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled() or import_error_tag:
return ret
# Build the string message
msg = ' - '
ret.append(self.curse_add_line(msg, optional=True))
# Start with the private IP information
msg = 'IP '
ret.append(self.curse_add_line(msg, 'TITLE', optional=True))
if 'address' in self.stats:
msg = '{}'.format(self.stats['address'])
ret.append(self.curse_add_line(msg, optional=True))
if 'mask_cidr' in self.stats:
# VPN with no internet access (issue #842)
msg = '/{}'.format(self.stats['mask_cidr'])
ret.append(self.curse_add_line(msg, optional=True))
# Then with the public IP information
try:
msg_pub = '{}'.format(self.stats['public_address'])
except (UnicodeEncodeError, KeyError):
# Add KeyError exception (see https://github.com/nicolargo/glances/issues/1469)
pass
else:
if self.stats['public_address']:
msg = ' Pub '
ret.append(self.curse_add_line(msg, 'TITLE', optional=True))
ret.append(self.curse_add_line(msg_pub, optional=True))
if self.stats['public_info_human']:
ret.append(self.curse_add_line(' {}'.format(self.stats['public_info_human']), optional=True))
return ret
def public_info_for_human(self, public_info):
"""Return the data to pack to the client."""
if not public_info:
return ''
field_result = []
for f in self.censys_fields:
field = f.split(':')
if len(field) == 1 and field[0] in public_info:
field_result.append('{}'.format(public_info[field[0]]))
elif len(field) == 2 and field[0] in public_info and field[1] in public_info[field[0]]:
field_result.append('{}'.format(public_info[field[0]][field[1]]))
return '/'.join(field_result)
@staticmethod
def ip_to_cidr(ip):
"""Convert IP address to CIDR.
Example: '255.255.255.0' will return 24
"""
# Thanks to @Atticfire
# See https://github.com/nicolargo/glances/issues/1417#issuecomment-469894399
if ip is None:
# Correct issue #1528
return 0
return sum(bin(int(x)).count('1') for x in ip.split('.'))
class PublicIpAddress(object):
"""Get public IP address from online services."""
def __init__(self, timeout=2):
"""Init the class."""
self.timeout = timeout
def get(self):
"""Get the first public IP address returned by one of the online services."""
q = queue.Queue()
for u, j, k in urls:
t = threading.Thread(target=self._get_ip_public, args=(q, u, j, k))
t.daemon = True
t.start()
timer = Timer(self.timeout)
ip = None
while not timer.finished() and ip is None:
if q.qsize() > 0:
ip = q.get()
if ip is None:
return None
return ', '.join(set([x.strip() for x in ip.split(',')]))
def _get_ip_public(self, queue_target, url, json=False, key=None):
"""Request the url service and put the result in the queue_target."""
try:
response = urlopen(url, timeout=self.timeout).read().decode('utf-8')
except Exception as e:
logger.debug("IP plugin - Cannot open URL {} ({})".format(url, e))
queue_target.put(None)
else:
# Request depend on service
try:
if not json:
queue_target.put(response)
else:
queue_target.put(loads(response)[key])
except ValueError:
queue_target.put(None)
class PublicIpInfo(object):
"""Get public IP information from Censys online service."""
def __init__(self, ip, url, username, password, timeout=2):
"""Init the class."""
self.ip = ip
self.url = url
self.username = username
self.password = password
self.timeout = timeout
def get(self):
"""Return the public IP information returned by one of the online service."""
q = queue.Queue()
t = threading.Thread(target=self._get_ip_public_info, args=(q, self.ip, self.url, self.username, self.password))
t.daemon = True
t.start()
timer = Timer(self.timeout)
info = None
while not timer.finished() and info is None:
if q.qsize() > 0:
info = q.get()
if info is None:
return None
return info
def _get_ip_public_info(self, queue_target, ip, url, username, password):
"""Request the url service and put the result in the queue_target."""
request_url = "{}/v2/hosts/{}".format(url, ip)
try:
response = urlopen_auth(request_url, username, password).read()
except Exception as e:
logger.debug("IP plugin - Cannot open URL {} ({})".format(request_url, e))
queue_target.put(None)
else:
try:
queue_target.put(loads(response)['result'])
except (ValueError, KeyError) as e:
logger.debug("IP plugin - Cannot get result field from {} ({})".format(request_url, e))
queue_target.put(None)

View File

@ -1,300 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""IP plugin."""
import threading
from ujson import loads
from glances.globals import urlopen, queue, urlopen_auth
from glances.logger import logger
from glances.timer import Timer
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
# Import plugin specific dependency
try:
import netifaces
except ImportError as e:
import_error_tag = True
logger.warning("Missing Python Lib ({}), IP plugin is disabled".format(e))
else:
import_error_tag = False
# List of online services to retrieve public IP address
# List of tuple (url, json, key)
# - url: URL of the Web site
# - json: service return a JSON (True) or string (False)
# - key: key of the IP address in the JSON structure
urls = [
('https://httpbin.org/ip', True, 'origin'),
('https://api.ipify.org/?format=json', True, 'ip'),
('https://ipv4.jsonip.com', True, 'ip'),
]
class PluginModel(GlancesPluginModel):
"""Glances IP Plugin.
stats is a dict
"""
_default_public_refresh_interval = 300
_default_public_ip_disabled = ["False"]
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# For public IP address
self.public_address = ""
self.public_address_refresh_interval = self.get_conf_value(
"public_refresh_interval", default=self._default_public_refresh_interval
)
public_ip_disabled = self.get_conf_value("public_ip_disabled", default=self._default_public_ip_disabled)
self.public_ip_disabled = True if public_ip_disabled == ["True"] else False
# For the Censys options (see issue #2105)
self.public_info = ""
self.censys_url = self.get_conf_value("censys_url", default=[None])[0]
self.censys_username = self.get_conf_value("censys_username", default=[None])[0]
self.censys_password = self.get_conf_value("censys_password", default=[None])[0]
self.censys_fields = self.get_conf_value("censys_fields", default=[None])
self.public_info_disabled = (
self.censys_url is None
or self.censys_username is None
or self.censys_password is None
or self.censys_fields is None
)
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update IP stats using the input method.
:return: the stats dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local' and not import_error_tag:
# Update stats using the netifaces lib
# Start with the default IP gateway
try:
default_gw = netifaces.gateways()['default'][netifaces.AF_INET]
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab default gateway IP address ({})".format(e))
return {}
else:
stats['gateway'] = default_gw[0]
# Then the private IP address
# If multiple IP addresses are available, only the one with the default gateway is returned
try:
address = netifaces.ifaddresses(default_gw[1])[netifaces.AF_INET][0]['addr']
mask = netifaces.ifaddresses(default_gw[1])[netifaces.AF_INET][0]['netmask']
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab private IP address ({})".format(e))
return {}
else:
stats['address'] = address
stats['mask'] = mask
stats['mask_cidr'] = self.ip_to_cidr(stats['mask'])
# Finally with the public IP address
time_since_update = getTimeSinceLastUpdate('public-ip')
try:
if not self.public_ip_disabled and (
self.stats.get('address') != address or time_since_update > self.public_address_refresh_interval
):
self.public_address = PublicIpAddress().get()
if not self.public_info_disabled:
self.public_info = PublicIpInfo(
self.public_address, self.censys_url, self.censys_username, self.censys_password
).get()
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab public IP information ({})".format(e))
else:
stats['public_address'] = self.public_address
# Too much information provided in the public_info
# Limit it to public_info_for_human
# stats['public_info'] = self.public_info
stats['public_info_human'] = self.public_info_for_human(self.public_info)
elif self.input_method == 'snmp':
# Not implemented yet
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled() or import_error_tag:
return ret
# Build the string message
msg = ' - '
ret.append(self.curse_add_line(msg, optional=True))
# Start with the private IP information
msg = 'IP '
ret.append(self.curse_add_line(msg, 'TITLE', optional=True))
if 'address' in self.stats:
msg = '{}'.format(self.stats['address'])
ret.append(self.curse_add_line(msg, optional=True))
if 'mask_cidr' in self.stats:
# VPN with no internet access (issue #842)
msg = '/{}'.format(self.stats['mask_cidr'])
ret.append(self.curse_add_line(msg, optional=True))
# Then with the public IP information
try:
msg_pub = '{}'.format(self.stats['public_address'])
except (UnicodeEncodeError, KeyError):
# Add KeyError exception (see https://github.com/nicolargo/glances/issues/1469)
pass
else:
if self.stats['public_address']:
msg = ' Pub '
ret.append(self.curse_add_line(msg, 'TITLE', optional=True))
ret.append(self.curse_add_line(msg_pub, optional=True))
if self.stats['public_info_human']:
ret.append(self.curse_add_line(' {}'.format(self.stats['public_info_human']), optional=True))
return ret
def public_info_for_human(self, public_info):
"""Return the data to pack to the client."""
if not public_info:
return ''
field_result = []
for f in self.censys_fields:
field = f.split(':')
if len(field) == 1 and field[0] in public_info:
field_result.append('{}'.format(public_info[field[0]]))
elif len(field) == 2 and field[0] in public_info and field[1] in public_info[field[0]]:
field_result.append('{}'.format(public_info[field[0]][field[1]]))
return '/'.join(field_result)
@staticmethod
def ip_to_cidr(ip):
"""Convert IP address to CIDR.
Example: '255.255.255.0' will return 24
"""
# Thanks to @Atticfire
# See https://github.com/nicolargo/glances/issues/1417#issuecomment-469894399
if ip is None:
# Correct issue #1528
return 0
return sum(bin(int(x)).count('1') for x in ip.split('.'))
class PublicIpAddress(object):
"""Get public IP address from online services."""
def __init__(self, timeout=2):
"""Init the class."""
self.timeout = timeout
def get(self):
"""Get the first public IP address returned by one of the online services."""
q = queue.Queue()
for u, j, k in urls:
t = threading.Thread(target=self._get_ip_public, args=(q, u, j, k))
t.daemon = True
t.start()
timer = Timer(self.timeout)
ip = None
while not timer.finished() and ip is None:
if q.qsize() > 0:
ip = q.get()
if ip is None:
return None
return ', '.join(set([x.strip() for x in ip.split(',')]))
def _get_ip_public(self, queue_target, url, json=False, key=None):
"""Request the url service and put the result in the queue_target."""
try:
response = urlopen(url, timeout=self.timeout).read().decode('utf-8')
except Exception as e:
logger.debug("IP plugin - Cannot open URL {} ({})".format(url, e))
queue_target.put(None)
else:
# Request depend on service
try:
if not json:
queue_target.put(response)
else:
queue_target.put(loads(response)[key])
except ValueError:
queue_target.put(None)
class PublicIpInfo(object):
"""Get public IP information from Censys online service."""
def __init__(self, ip, url, username, password, timeout=2):
"""Init the class."""
self.ip = ip
self.url = url
self.username = username
self.password = password
self.timeout = timeout
def get(self):
"""Return the public IP information returned by one of the online service."""
q = queue.Queue()
t = threading.Thread(target=self._get_ip_public_info, args=(q, self.ip, self.url, self.username, self.password))
t.daemon = True
t.start()
timer = Timer(self.timeout)
info = None
while not timer.finished() and info is None:
if q.qsize() > 0:
info = q.get()
if info is None:
return None
return info
def _get_ip_public_info(self, queue_target, ip, url, username, password):
"""Request the url service and put the result in the queue_target."""
request_url = "{}/v2/hosts/{}".format(url, ip)
try:
response = urlopen_auth(request_url, username, password).read()
except Exception as e:
logger.debug("IP plugin - Cannot open URL {} ({})".format(request_url, e))
queue_target.put(None)
else:
try:
queue_target.put(loads(response)['result'])
except (ValueError, KeyError) as e:
logger.debug("IP plugin - Cannot get result field from {} ({})".format(request_url, e))
queue_target.put(None)

View File

@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2018 Angelo Poerio <angelo.poerio@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""IRQ plugin."""
import os
import operator
from glances.globals import LINUX
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances IRQ plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Init the stats
self.irq = GlancesIRQ()
def get_key(self):
"""Return the key of the list."""
return self.irq.get_key()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the IRQ stats."""
# Init new stats
stats = self.get_init_value()
# IRQ plugin only available on GNU/Linux
if not LINUX:
return self.stats
if self.input_method == 'local':
# Grab the stats
stats = self.irq.get()
elif self.input_method == 'snmp':
# not available
pass
# Get the TOP 5 (by rate/s)
stats = sorted(stats, key=operator.itemgetter('irq_rate'), reverse=True)[:5]
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only available on GNU/Linux
# Only process if stats exist and display plugin enable...
if not LINUX or not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Build the string message
# Header
msg = '{:{width}}'.format('IRQ', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
msg = '{:>9}'.format('Rate/s')
ret.append(self.curse_add_line(msg))
for i in self.stats:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(i['irq_line'][:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
msg = '{:>9}'.format(str(i['irq_rate']))
ret.append(self.curse_add_line(msg))
return ret
class GlancesIRQ(object):
"""This class manages the IRQ file."""
IRQ_FILE = '/proc/interrupts'
def __init__(self):
"""Init the class.
The stat are stored in a internal list of dict
"""
self.lasts = {}
self.reset()
def reset(self):
"""Reset the stats."""
self.stats = []
self.cpu_number = 0
def get(self):
"""Return the current IRQ stats."""
return self.__update()
def get_key(self):
"""Return the key of the dict."""
return 'irq_line'
def __header(self, line):
"""Build the header (contain the number of CPU).
CPU0 CPU1 CPU2 CPU3
0: 21 0 0 0 IO-APIC 2-edge timer
"""
self.cpu_number = len(line.split())
return self.cpu_number
def __humanname(self, line):
"""Return the IRQ name, alias or number (choose the best for human).
IRQ line samples:
1: 44487 341 44 72 IO-APIC 1-edge i8042
LOC: 33549868 22394684 32474570 21855077 Local timer interrupts
"""
splitted_line = line.split()
irq_line = splitted_line[0].replace(':', '')
if irq_line.isdigit():
# If the first column is a digit, use the alias (last column)
irq_line += '_{}'.format(splitted_line[-1])
return irq_line
def __sum(self, line):
"""Return the IRQ sum number.
IRQ line samples:
1: 44487 341 44 72 IO-APIC 1-edge i8042
LOC: 33549868 22394684 32474570 21855077 Local timer interrupts
FIQ: usb_fiq
"""
splitted_line = line.split()
try:
ret = sum(map(int, splitted_line[1 : (self.cpu_number + 1)]))
except ValueError:
# Correct issue #1007 on some conf (Raspberry Pi with Raspbian)
ret = 0
return ret
def __update(self):
"""Load the IRQ file and update the internal dict."""
self.reset()
if not os.path.exists(self.IRQ_FILE):
# Correct issue #947: IRQ file do not exist on OpenVZ container
return self.stats
try:
with open(self.IRQ_FILE) as irq_proc:
time_since_update = getTimeSinceLastUpdate('irq')
# Read the header
self.__header(irq_proc.readline())
# Read the rest of the lines (one line per IRQ)
for line in irq_proc.readlines():
irq_line = self.__humanname(line)
current_irqs = self.__sum(line)
irq_rate = int(
current_irqs - self.lasts.get(irq_line) if self.lasts.get(irq_line) else 0 // time_since_update
)
irq_current = {
'irq_line': irq_line,
'irq_rate': irq_rate,
'key': self.get_key(),
'time_since_update': time_since_update,
}
self.stats.append(irq_current)
self.lasts[irq_line] = current_irqs
except (OSError, IOError):
pass
return self.stats

View File

@ -1,198 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2018 Angelo Poerio <angelo.poerio@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""IRQ plugin."""
import os
import operator
from glances.globals import LINUX
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances IRQ plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Init the stats
self.irq = GlancesIRQ()
def get_key(self):
"""Return the key of the list."""
return self.irq.get_key()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the IRQ stats."""
# Init new stats
stats = self.get_init_value()
# IRQ plugin only available on GNU/Linux
if not LINUX:
return self.stats
if self.input_method == 'local':
# Grab the stats
stats = self.irq.get()
elif self.input_method == 'snmp':
# not available
pass
# Get the TOP 5 (by rate/s)
stats = sorted(stats, key=operator.itemgetter('irq_rate'), reverse=True)[:5]
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only available on GNU/Linux
# Only process if stats exist and display plugin enable...
if not LINUX or not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Build the string message
# Header
msg = '{:{width}}'.format('IRQ', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
msg = '{:>9}'.format('Rate/s')
ret.append(self.curse_add_line(msg))
for i in self.stats:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(i['irq_line'][:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
msg = '{:>9}'.format(str(i['irq_rate']))
ret.append(self.curse_add_line(msg))
return ret
class GlancesIRQ(object):
"""This class manages the IRQ file."""
IRQ_FILE = '/proc/interrupts'
def __init__(self):
"""Init the class.
The stat are stored in a internal list of dict
"""
self.lasts = {}
self.reset()
def reset(self):
"""Reset the stats."""
self.stats = []
self.cpu_number = 0
def get(self):
"""Return the current IRQ stats."""
return self.__update()
def get_key(self):
"""Return the key of the dict."""
return 'irq_line'
def __header(self, line):
"""Build the header (contain the number of CPU).
CPU0 CPU1 CPU2 CPU3
0: 21 0 0 0 IO-APIC 2-edge timer
"""
self.cpu_number = len(line.split())
return self.cpu_number
def __humanname(self, line):
"""Return the IRQ name, alias or number (choose the best for human).
IRQ line samples:
1: 44487 341 44 72 IO-APIC 1-edge i8042
LOC: 33549868 22394684 32474570 21855077 Local timer interrupts
"""
splitted_line = line.split()
irq_line = splitted_line[0].replace(':', '')
if irq_line.isdigit():
# If the first column is a digit, use the alias (last column)
irq_line += '_{}'.format(splitted_line[-1])
return irq_line
def __sum(self, line):
"""Return the IRQ sum number.
IRQ line samples:
1: 44487 341 44 72 IO-APIC 1-edge i8042
LOC: 33549868 22394684 32474570 21855077 Local timer interrupts
FIQ: usb_fiq
"""
splitted_line = line.split()
try:
ret = sum(map(int, splitted_line[1 : (self.cpu_number + 1)]))
except ValueError:
# Correct issue #1007 on some conf (Raspberry Pi with Raspbian)
ret = 0
return ret
def __update(self):
"""Load the IRQ file and update the internal dict."""
self.reset()
if not os.path.exists(self.IRQ_FILE):
# Correct issue #947: IRQ file do not exist on OpenVZ container
return self.stats
try:
with open(self.IRQ_FILE) as irq_proc:
time_since_update = getTimeSinceLastUpdate('irq')
# Read the header
self.__header(irq_proc.readline())
# Read the rest of the lines (one line per IRQ)
for line in irq_proc.readlines():
irq_line = self.__humanname(line)
current_irqs = self.__sum(line)
irq_rate = int(
current_irqs - self.lasts.get(irq_line) if self.lasts.get(irq_line) else 0 // time_since_update
)
irq_current = {
'irq_line': irq_line,
'irq_rate': irq_rate,
'key': self.get_key(),
'time_since_update': time_since_update,
}
self.stats.append(irq_current)
self.lasts[irq_line] = current_irqs
except (OSError, IOError):
pass
return self.stats

View File

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Load plugin."""
import os
import psutil
from glances.globals import iteritems
from glances.plugins.core import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
# Fields description
fields_description = {
'min1': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 1 minute.',
'unit': 'float',
},
'min5': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 5 minutes.',
'unit': 'float',
},
'min15': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 15 minutes.',
'unit': 'float',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
}
# SNMP OID
# 1 minute Load: .1.3.6.1.4.1.2021.10.1.3.1
# 5 minute Load: .1.3.6.1.4.1.2021.10.1.3.2
# 15 minute Load: .1.3.6.1.4.1.2021.10.1.3.3
snmp_oid = {
'min1': '1.3.6.1.4.1.2021.10.1.3.1',
'min5': '1.3.6.1.4.1.2021.10.1.3.2',
'min15': '1.3.6.1.4.1.2021.10.1.3.3',
}
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [
{'name': 'min1', 'description': '1 minute load'},
{'name': 'min5', 'description': '5 minutes load'},
{'name': 'min15', 'description': '15 minutes load'},
]
class PluginModel(GlancesPluginModel):
"""Glances load plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
# Call CorePluginModel in order to display the core number
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception as e:
logger.warning('Error: Can not retrieve the CPU core number (set it to 1) ({})'.format(e))
self.nb_log_core = 1
def _getloadavg(self):
"""Get load average. On both Linux and Windows thanks to PsUtil"""
try:
return psutil.getloadavg()
except (AttributeError, OSError):
pass
try:
return os.getloadavg()
except (AttributeError, OSError):
return None
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update load stats."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Get the load using the os standard lib
load = self._getloadavg()
if load is None:
stats = self.get_init_value()
else:
stats = {'min1': load[0], 'min5': load[1], 'min15': load[2], 'cpucore': self.nb_log_core}
elif self.input_method == 'snmp':
# Update stats using SNMP
stats = self.get_stats_snmp(snmp_oid=snmp_oid)
if stats['min1'] == '':
stats = self.get_init_value()
return stats
# Python 3 return a dict like:
# {'min1': "b'0.08'", 'min5': "b'0.12'", 'min15': "b'0.15'"}
for k, v in iteritems(stats):
stats[k] = float(v)
stats['cpucore'] = self.nb_log_core
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
try:
# Alert and log
self.views['min15']['decoration'] = self.get_alert_log(
self.stats['min15'], maximum=100 * self.stats['cpucore']
)
# Alert only
self.views['min5']['decoration'] = self.get_alert(self.stats['min5'], maximum=100 * self.stats['cpucore'])
except KeyError:
# try/except mandatory for Windows compatibility (no load stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist, not empty (issue #871) and plugin not disabled
if not self.stats or (self.stats == {}) or self.is_disabled():
return ret
# Build the string message
# Header
msg = '{:4}'.format('LOAD')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:1}'.format(self.trend_msg(self.get_trend('min1')))
ret.append(self.curse_add_line(msg))
# Core number
if 'cpucore' in self.stats and self.stats['cpucore'] > 0:
msg = '{:3}core'.format(int(self.stats['cpucore']))
ret.append(self.curse_add_line(msg))
# Loop over 1min, 5min and 15min load
for load_time in ['1', '5', '15']:
ret.append(self.curse_new_line())
msg = '{:7}'.format('{} min'.format(load_time))
ret.append(self.curse_add_line(msg))
if args.disable_irix and self.nb_log_core != 0:
# Enable Irix mode for load (see issue #1554)
load_stat = self.stats['min{}'.format(load_time)] / self.nb_log_core * 100
msg = '{:>5.1f}%'.format(load_stat)
else:
# Default mode for load
load_stat = self.stats['min{}'.format(load_time)]
msg = '{:>6.2f}'.format(load_stat)
ret.append(self.curse_add_line(msg, self.get_views(key='min{}'.format(load_time), option='decoration')))
return ret

View File

@ -1,183 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Load plugin."""
import os
import psutil
from glances.globals import iteritems
from glances.plugins.core.model import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
# Fields description
fields_description = {
'min1': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 1 minute.',
'unit': 'float',
},
'min5': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 5 minutes.',
'unit': 'float',
},
'min15': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 15 minutes.',
'unit': 'float',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
}
# SNMP OID
# 1 minute Load: .1.3.6.1.4.1.2021.10.1.3.1
# 5 minute Load: .1.3.6.1.4.1.2021.10.1.3.2
# 15 minute Load: .1.3.6.1.4.1.2021.10.1.3.3
snmp_oid = {
'min1': '1.3.6.1.4.1.2021.10.1.3.1',
'min5': '1.3.6.1.4.1.2021.10.1.3.2',
'min15': '1.3.6.1.4.1.2021.10.1.3.3',
}
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [
{'name': 'min1', 'description': '1 minute load'},
{'name': 'min5', 'description': '5 minutes load'},
{'name': 'min15', 'description': '15 minutes load'},
]
class PluginModel(GlancesPluginModel):
"""Glances load plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
# Call CorePluginModel in order to display the core number
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception as e:
logger.warning('Error: Can not retrieve the CPU core number (set it to 1) ({})'.format(e))
self.nb_log_core = 1
def _getloadavg(self):
"""Get load average. On both Linux and Windows thanks to PsUtil"""
try:
return psutil.getloadavg()
except (AttributeError, OSError):
pass
try:
return os.getloadavg()
except (AttributeError, OSError):
return None
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update load stats."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Get the load using the os standard lib
load = self._getloadavg()
if load is None:
stats = self.get_init_value()
else:
stats = {'min1': load[0], 'min5': load[1], 'min15': load[2], 'cpucore': self.nb_log_core}
elif self.input_method == 'snmp':
# Update stats using SNMP
stats = self.get_stats_snmp(snmp_oid=snmp_oid)
if stats['min1'] == '':
stats = self.get_init_value()
return stats
# Python 3 return a dict like:
# {'min1': "b'0.08'", 'min5': "b'0.12'", 'min15': "b'0.15'"}
for k, v in iteritems(stats):
stats[k] = float(v)
stats['cpucore'] = self.nb_log_core
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
try:
# Alert and log
self.views['min15']['decoration'] = self.get_alert_log(
self.stats['min15'], maximum=100 * self.stats['cpucore']
)
# Alert only
self.views['min5']['decoration'] = self.get_alert(self.stats['min5'], maximum=100 * self.stats['cpucore'])
except KeyError:
# try/except mandatory for Windows compatibility (no load stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist, not empty (issue #871) and plugin not disabled
if not self.stats or (self.stats == {}) or self.is_disabled():
return ret
# Build the string message
# Header
msg = '{:4}'.format('LOAD')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:1}'.format(self.trend_msg(self.get_trend('min1')))
ret.append(self.curse_add_line(msg))
# Core number
if 'cpucore' in self.stats and self.stats['cpucore'] > 0:
msg = '{:3}core'.format(int(self.stats['cpucore']))
ret.append(self.curse_add_line(msg))
# Loop over 1min, 5min and 15min load
for load_time in ['1', '5', '15']:
ret.append(self.curse_new_line())
msg = '{:7}'.format('{} min'.format(load_time))
ret.append(self.curse_add_line(msg))
if args.disable_irix and self.nb_log_core != 0:
# Enable Irix mode for load (see issue #1554)
load_stat = self.stats['min{}'.format(load_time)] / self.nb_log_core * 100
msg = '{:>5.1f}%'.format(load_stat)
else:
# Default mode for load
load_stat = self.stats['min{}'.format(load_time)]
msg = '{:>6.2f}'.format(load_stat)
ret.append(self.curse_add_line(msg, self.get_views(key='min{}'.format(load_time), option='decoration')))
return ret

View File

@ -0,0 +1,285 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Virtual memory plugin."""
from glances.globals import iterkeys
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'total': {'description': 'Total physical memory available.', 'unit': 'bytes', 'min_symbol': 'K'},
'available': {
'description': 'The actual amount of available memory that can be given instantly \
to processes that request more memory in bytes; this is calculated by summing \
different memory values depending on the platform (e.g. free + buffers + cached on Linux) \
and it is supposed to be used to monitor actual memory usage in a cross platform fashion.',
'unit': 'bytes',
'min_symbol': 'K',
},
'percent': {
'description': 'The percentage usage calculated as (total - available) / total * 100.',
'unit': 'percent',
},
'used': {
'description': 'Memory used, calculated differently depending on the platform and \
designed for informational purposes only.',
'unit': 'bytes',
'min_symbol': 'K',
},
'free': {
'description': 'Memory not being used at all (zeroed) that is readily available; \
note that this doesn\'t reflect the actual memory available (use \'available\' instead).',
'unit': 'bytes',
'min_symbol': 'K',
},
'active': {
'description': '*(UNIX)*: memory currently in use or very recently used, and so it is in RAM.',
'unit': 'bytes',
'min_symbol': 'K',
},
'inactive': {
'description': '*(UNIX)*: memory that is marked as not used.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'inacti',
},
'buffers': {
'description': '*(Linux, BSD)*: cache for things like file system metadata.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'buffer',
},
'cached': {'description': '*(Linux, BSD)*: cache for various things.', 'unit': 'bytes', 'min_symbol': 'K'},
'wired': {
'description': '*(BSD, macOS)*: memory that is marked to always stay in RAM. It is never moved to disk.',
'unit': 'bytes',
'min_symbol': 'K',
},
'shared': {
'description': '*(BSD)*: memory that may be simultaneously accessed by multiple processes.',
'unit': 'bytes',
'min_symbol': 'K',
},
}
# SNMP OID
# Total RAM in machine: .1.3.6.1.4.1.2021.4.5.0
# Total RAM used: .1.3.6.1.4.1.2021.4.6.0
# Total RAM Free: .1.3.6.1.4.1.2021.4.11.0
# Total RAM Shared: .1.3.6.1.4.1.2021.4.13.0
# Total RAM Buffered: .1.3.6.1.4.1.2021.4.14.0
# Total Cached Memory: .1.3.6.1.4.1.2021.4.15.0
# Note: For Windows, stats are in the FS table
snmp_oid = {
'default': {
'total': '1.3.6.1.4.1.2021.4.5.0',
'free': '1.3.6.1.4.1.2021.4.11.0',
'shared': '1.3.6.1.4.1.2021.4.13.0',
'buffers': '1.3.6.1.4.1.2021.4.14.0',
'cached': '1.3.6.1.4.1.2021.4.15.0',
},
'windows': {
'mnt_point': '1.3.6.1.2.1.25.2.3.1.3',
'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4',
'size': '1.3.6.1.2.1.25.2.3.1.5',
'used': '1.3.6.1.2.1.25.2.3.1.6',
},
'esxi': {
'mnt_point': '1.3.6.1.2.1.25.2.3.1.3',
'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4',
'size': '1.3.6.1.2.1.25.2.3.1.5',
'used': '1.3.6.1.2.1.25.2.3.1.6',
},
}
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'RAM memory usage', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances' memory plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
items_history_list=items_history_list,
fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update RAM memory stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab MEM using the psutil virtual_memory method
vm_stats = psutil.virtual_memory()
# Get all the memory stats (copy/paste of the psutil documentation)
# total: total physical memory available.
# available: the actual amount of available memory that can be given instantly
# to processes that request more memory in bytes; this is calculated by summing
# different memory values depending on the platform (e.g. free + buffers + cached on Linux)
# and it is supposed to be used to monitor actual memory usage in a cross platform fashion.
# percent: the percentage usage calculated as (total - available) / total * 100.
# used: memory used, calculated differently depending on the platform and designed for informational
# purposes only.
# free: memory not being used at all (zeroed) that is readily available; note that this doesn't
# reflect the actual memory available (use available instead).
# Platform-specific fields:
# active: (UNIX): memory currently in use or very recently used, and so it is in RAM.
# inactive: (UNIX): memory that is marked as not used.
# buffers: (Linux, BSD): cache for things like file system metadata.
# cached: (Linux, BSD): cache for various things.
# wired: (BSD, macOS): memory that is marked to always stay in RAM. It is never moved to disk.
# shared: (BSD): memory that may be simultaneously accessed by multiple processes.
self.reset()
for mem in [
'total',
'available',
'percent',
'used',
'free',
'active',
'inactive',
'buffers',
'cached',
'wired',
'shared',
]:
if hasattr(vm_stats, mem):
stats[mem] = getattr(vm_stats, mem)
# Use the 'free'/htop calculation
# free=available+buffer+cached
stats['free'] = stats['available']
if hasattr(stats, 'buffers'):
stats['free'] += stats['buffers']
if hasattr(stats, 'cached'):
stats['free'] += stats['cached']
# used=total-free
stats['used'] = stats['total'] - stats['free']
elif self.input_method == 'snmp':
# Update stats using SNMP
if self.short_system_name in ('windows', 'esxi'):
# Mem stats for Windows|Vmware Esxi are stored in the FS table
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
else:
for fs in fs_stat:
# The Physical Memory (Windows) or Real Memory (VMware)
# gives statistics on RAM usage and availability.
if fs in ('Physical Memory', 'Real Memory'):
stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
stats['percent'] = float(stats['used'] * 100 / stats['total'])
stats['free'] = stats['total'] - stats['used']
break
else:
# Default behavior for others OS
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['total'] == '':
self.reset()
return self.stats
for key in iterkeys(stats):
if stats[key] != '':
stats[key] = float(stats[key]) * 1024
# Use the 'free'/htop calculation
stats['free'] = stats['free'] - stats['total'] + (stats['buffers'] + stats['cached'])
# used=total-free
stats['used'] = stats['total'] - stats['free']
# percent: the percentage usage calculated as (total - available) / total * 100.
stats['percent'] = float((stats['total'] - stats['free']) / stats['total'] * 100)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
self.views['percent']['decoration'] = self.get_alert_log(self.stats['used'], maximum=self.stats['total'])
# Optional
for key in ['active', 'inactive', 'buffers', 'cached']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# First line
# total% + active
msg = '{}'.format('MEM')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:2}'.format(self.trend_msg(self.get_trend('percent')))
ret.append(self.curse_add_line(msg))
# Percent memory usage
msg = '{:>7.1%}'.format(self.stats['percent'] / 100)
ret.append(self.curse_add_line(msg, self.get_views(key='percent', option='decoration')))
# Active memory usage
ret.extend(self.curse_add_stat('active', width=16, header=' '))
# Second line
# total + inactive
ret.append(self.curse_new_line())
# Total memory usage
ret.extend(self.curse_add_stat('total', width=15))
# Inactive memory usage
ret.extend(self.curse_add_stat('inactive', width=16, header=' '))
# Third line
# used + buffers
ret.append(self.curse_new_line())
# Used memory usage
ret.extend(self.curse_add_stat('used', width=15))
# Buffers memory usage
ret.extend(self.curse_add_stat('buffers', width=16, header=' '))
# Fourth line
# free + cached
ret.append(self.curse_new_line())
# Free memory usage
ret.extend(self.curse_add_stat('free', width=15))
# Cached memory usage
ret.extend(self.curse_add_stat('cached', width=16, header=' '))
return ret

View File

@ -1,285 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Virtual memory plugin."""
from glances.globals import iterkeys
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'total': {'description': 'Total physical memory available.', 'unit': 'bytes', 'min_symbol': 'K'},
'available': {
'description': 'The actual amount of available memory that can be given instantly \
to processes that request more memory in bytes; this is calculated by summing \
different memory values depending on the platform (e.g. free + buffers + cached on Linux) \
and it is supposed to be used to monitor actual memory usage in a cross platform fashion.',
'unit': 'bytes',
'min_symbol': 'K',
},
'percent': {
'description': 'The percentage usage calculated as (total - available) / total * 100.',
'unit': 'percent',
},
'used': {
'description': 'Memory used, calculated differently depending on the platform and \
designed for informational purposes only.',
'unit': 'bytes',
'min_symbol': 'K',
},
'free': {
'description': 'Memory not being used at all (zeroed) that is readily available; \
note that this doesn\'t reflect the actual memory available (use \'available\' instead).',
'unit': 'bytes',
'min_symbol': 'K',
},
'active': {
'description': '*(UNIX)*: memory currently in use or very recently used, and so it is in RAM.',
'unit': 'bytes',
'min_symbol': 'K',
},
'inactive': {
'description': '*(UNIX)*: memory that is marked as not used.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'inacti',
},
'buffers': {
'description': '*(Linux, BSD)*: cache for things like file system metadata.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'buffer',
},
'cached': {'description': '*(Linux, BSD)*: cache for various things.', 'unit': 'bytes', 'min_symbol': 'K'},
'wired': {
'description': '*(BSD, macOS)*: memory that is marked to always stay in RAM. It is never moved to disk.',
'unit': 'bytes',
'min_symbol': 'K',
},
'shared': {
'description': '*(BSD)*: memory that may be simultaneously accessed by multiple processes.',
'unit': 'bytes',
'min_symbol': 'K',
},
}
# SNMP OID
# Total RAM in machine: .1.3.6.1.4.1.2021.4.5.0
# Total RAM used: .1.3.6.1.4.1.2021.4.6.0
# Total RAM Free: .1.3.6.1.4.1.2021.4.11.0
# Total RAM Shared: .1.3.6.1.4.1.2021.4.13.0
# Total RAM Buffered: .1.3.6.1.4.1.2021.4.14.0
# Total Cached Memory: .1.3.6.1.4.1.2021.4.15.0
# Note: For Windows, stats are in the FS table
snmp_oid = {
'default': {
'total': '1.3.6.1.4.1.2021.4.5.0',
'free': '1.3.6.1.4.1.2021.4.11.0',
'shared': '1.3.6.1.4.1.2021.4.13.0',
'buffers': '1.3.6.1.4.1.2021.4.14.0',
'cached': '1.3.6.1.4.1.2021.4.15.0',
},
'windows': {
'mnt_point': '1.3.6.1.2.1.25.2.3.1.3',
'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4',
'size': '1.3.6.1.2.1.25.2.3.1.5',
'used': '1.3.6.1.2.1.25.2.3.1.6',
},
'esxi': {
'mnt_point': '1.3.6.1.2.1.25.2.3.1.3',
'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4',
'size': '1.3.6.1.2.1.25.2.3.1.5',
'used': '1.3.6.1.2.1.25.2.3.1.6',
},
}
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'RAM memory usage', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances' memory plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
items_history_list=items_history_list,
fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update RAM memory stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab MEM using the psutil virtual_memory method
vm_stats = psutil.virtual_memory()
# Get all the memory stats (copy/paste of the psutil documentation)
# total: total physical memory available.
# available: the actual amount of available memory that can be given instantly
# to processes that request more memory in bytes; this is calculated by summing
# different memory values depending on the platform (e.g. free + buffers + cached on Linux)
# and it is supposed to be used to monitor actual memory usage in a cross platform fashion.
# percent: the percentage usage calculated as (total - available) / total * 100.
# used: memory used, calculated differently depending on the platform and designed for informational
# purposes only.
# free: memory not being used at all (zeroed) that is readily available; note that this doesn't
# reflect the actual memory available (use available instead).
# Platform-specific fields:
# active: (UNIX): memory currently in use or very recently used, and so it is in RAM.
# inactive: (UNIX): memory that is marked as not used.
# buffers: (Linux, BSD): cache for things like file system metadata.
# cached: (Linux, BSD): cache for various things.
# wired: (BSD, macOS): memory that is marked to always stay in RAM. It is never moved to disk.
# shared: (BSD): memory that may be simultaneously accessed by multiple processes.
self.reset()
for mem in [
'total',
'available',
'percent',
'used',
'free',
'active',
'inactive',
'buffers',
'cached',
'wired',
'shared',
]:
if hasattr(vm_stats, mem):
stats[mem] = getattr(vm_stats, mem)
# Use the 'free'/htop calculation
# free=available+buffer+cached
stats['free'] = stats['available']
if hasattr(stats, 'buffers'):
stats['free'] += stats['buffers']
if hasattr(stats, 'cached'):
stats['free'] += stats['cached']
# used=total-free
stats['used'] = stats['total'] - stats['free']
elif self.input_method == 'snmp':
# Update stats using SNMP
if self.short_system_name in ('windows', 'esxi'):
# Mem stats for Windows|Vmware Esxi are stored in the FS table
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
else:
for fs in fs_stat:
# The Physical Memory (Windows) or Real Memory (VMware)
# gives statistics on RAM usage and availability.
if fs in ('Physical Memory', 'Real Memory'):
stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
stats['percent'] = float(stats['used'] * 100 / stats['total'])
stats['free'] = stats['total'] - stats['used']
break
else:
# Default behavior for others OS
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['total'] == '':
self.reset()
return self.stats
for key in iterkeys(stats):
if stats[key] != '':
stats[key] = float(stats[key]) * 1024
# Use the 'free'/htop calculation
stats['free'] = stats['free'] - stats['total'] + (stats['buffers'] + stats['cached'])
# used=total-free
stats['used'] = stats['total'] - stats['free']
# percent: the percentage usage calculated as (total - available) / total * 100.
stats['percent'] = float((stats['total'] - stats['free']) / stats['total'] * 100)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
self.views['percent']['decoration'] = self.get_alert_log(self.stats['used'], maximum=self.stats['total'])
# Optional
for key in ['active', 'inactive', 'buffers', 'cached']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# First line
# total% + active
msg = '{}'.format('MEM')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:2}'.format(self.trend_msg(self.get_trend('percent')))
ret.append(self.curse_add_line(msg))
# Percent memory usage
msg = '{:>7.1%}'.format(self.stats['percent'] / 100)
ret.append(self.curse_add_line(msg, self.get_views(key='percent', option='decoration')))
# Active memory usage
ret.extend(self.curse_add_stat('active', width=16, header=' '))
# Second line
# total + inactive
ret.append(self.curse_new_line())
# Total memory usage
ret.extend(self.curse_add_stat('total', width=15))
# Inactive memory usage
ret.extend(self.curse_add_stat('inactive', width=16, header=' '))
# Third line
# used + buffers
ret.append(self.curse_new_line())
# Used memory usage
ret.extend(self.curse_add_stat('used', width=15))
# Buffers memory usage
ret.extend(self.curse_add_stat('buffers', width=16, header=' '))
# Fourth line
# free + cached
ret.append(self.curse_new_line())
# Free memory usage
ret.extend(self.curse_add_stat('free', width=15))
# Cached memory usage
ret.extend(self.curse_add_stat('cached', width=16, header=' '))
return ret

View File

@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Swap memory plugin."""
from glances.globals import iterkeys
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'total': {'description': 'Total swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'used': {'description': 'Used swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'free': {'description': 'Free swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'percent': {'description': 'Used swap memory in percentage.', 'unit': 'percent'},
'sin': {
'description': 'The number of bytes the system has swapped in from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
},
'sout': {
'description': 'The number of bytes the system has swapped out from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# Total Swap Size: .1.3.6.1.4.1.2021.4.3.0
# Available Swap Space: .1.3.6.1.4.1.2021.4.4.0
snmp_oid = {
'default': {'total': '1.3.6.1.4.1.2021.4.3.0', 'free': '1.3.6.1.4.1.2021.4.4.0'},
'windows': {
'mnt_point': '1.3.6.1.2.1.25.2.3.1.3',
'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4',
'size': '1.3.6.1.2.1.25.2.3.1.5',
'used': '1.3.6.1.2.1.25.2.3.1.6',
},
}
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'Swap memory usage', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances swap memory plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update swap memory stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab SWAP using the psutil swap_memory method
try:
sm_stats = psutil.swap_memory()
except RuntimeError:
# Crash on startup on Illumos when no swap is configured #1767
pass
else:
# Get all the swap stats (copy/paste of the psutil documentation)
# total: total swap memory in bytes
# used: used swap memory in bytes
# free: free swap memory in bytes
# percent: the percentage usage
# sin: the number of bytes the system has swapped in from disk (cumulative)
# sout: the number of bytes the system has swapped out from disk (cumulative)
for swap in ['total', 'used', 'free', 'percent', 'sin', 'sout']:
if hasattr(sm_stats, swap):
stats[swap] = getattr(sm_stats, swap)
# By storing time data we enable sin/s and sout/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
stats['time_since_update'] = getTimeSinceLastUpdate('memswap')
elif self.input_method == 'snmp':
# Update stats using SNMP
if self.short_system_name == 'windows':
# Mem stats for Windows OS are stored in the FS table
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
else:
for fs in fs_stat:
# The virtual memory concept is used by the operating
# system to extend (virtually) the physical memory and
# thus to run more programs by swapping unused memory
# zone (page) to a disk file.
if fs == 'Virtual Memory':
stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
stats['percent'] = float(stats['used'] * 100 / stats['total'])
stats['free'] = stats['total'] - stats['used']
break
else:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['total'] == '':
self.reset()
return stats
for key in iterkeys(stats):
if stats[key] != '':
stats[key] = float(stats[key]) * 1024
# used=total-free
stats['used'] = stats['total'] - stats['free']
# percent: the percentage usage calculated as (total -
# available) / total * 100.
stats['percent'] = float((stats['total'] - stats['free']) / stats['total'] * 100)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
if 'used' in self.stats and 'total' in self.stats and 'percent' in self.stats:
self.views['percent']['decoration'] = self.get_alert_log(self.stats['used'], maximum=self.stats['total'])
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# First line
# total%
msg = '{:4}'.format('SWAP')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:2}'.format(self.trend_msg(self.get_trend('percent')))
ret.append(self.curse_add_line(msg))
# Percent memory usage
msg = '{:>6.1%}'.format(self.stats['percent'] / 100)
ret.append(self.curse_add_line(msg, self.get_views(key='percent', option='decoration')))
# Second line
# total
ret.append(self.curse_new_line())
# Total memory usage
ret.extend(self.curse_add_stat('total', width=15))
# Third line
# used
ret.append(self.curse_new_line())
# Used memory usage
ret.extend(self.curse_add_stat('used', width=15))
# Fourth line
# free
ret.append(self.curse_new_line())
# Free memory usage
ret.extend(self.curse_add_stat('free', width=15))
return ret

View File

@ -1,191 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Swap memory plugin."""
from glances.globals import iterkeys
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'total': {'description': 'Total swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'used': {'description': 'Used swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'free': {'description': 'Free swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'percent': {'description': 'Used swap memory in percentage.', 'unit': 'percent'},
'sin': {
'description': 'The number of bytes the system has swapped in from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
},
'sout': {
'description': 'The number of bytes the system has swapped out from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# Total Swap Size: .1.3.6.1.4.1.2021.4.3.0
# Available Swap Space: .1.3.6.1.4.1.2021.4.4.0
snmp_oid = {
'default': {'total': '1.3.6.1.4.1.2021.4.3.0', 'free': '1.3.6.1.4.1.2021.4.4.0'},
'windows': {
'mnt_point': '1.3.6.1.2.1.25.2.3.1.3',
'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4',
'size': '1.3.6.1.2.1.25.2.3.1.5',
'used': '1.3.6.1.2.1.25.2.3.1.6',
},
}
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'Swap memory usage', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances swap memory plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update swap memory stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab SWAP using the psutil swap_memory method
try:
sm_stats = psutil.swap_memory()
except RuntimeError:
# Crash on startup on Illumos when no swap is configured #1767
pass
else:
# Get all the swap stats (copy/paste of the psutil documentation)
# total: total swap memory in bytes
# used: used swap memory in bytes
# free: free swap memory in bytes
# percent: the percentage usage
# sin: the number of bytes the system has swapped in from disk (cumulative)
# sout: the number of bytes the system has swapped out from disk (cumulative)
for swap in ['total', 'used', 'free', 'percent', 'sin', 'sout']:
if hasattr(sm_stats, swap):
stats[swap] = getattr(sm_stats, swap)
# By storing time data we enable sin/s and sout/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
stats['time_since_update'] = getTimeSinceLastUpdate('memswap')
elif self.input_method == 'snmp':
# Update stats using SNMP
if self.short_system_name == 'windows':
# Mem stats for Windows OS are stored in the FS table
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
else:
for fs in fs_stat:
# The virtual memory concept is used by the operating
# system to extend (virtually) the physical memory and
# thus to run more programs by swapping unused memory
# zone (page) to a disk file.
if fs == 'Virtual Memory':
stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
stats['percent'] = float(stats['used'] * 100 / stats['total'])
stats['free'] = stats['total'] - stats['used']
break
else:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['total'] == '':
self.reset()
return stats
for key in iterkeys(stats):
if stats[key] != '':
stats[key] = float(stats[key]) * 1024
# used=total-free
stats['used'] = stats['total'] - stats['free']
# percent: the percentage usage calculated as (total -
# available) / total * 100.
stats['percent'] = float((stats['total'] - stats['free']) / stats['total'] * 100)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
if 'used' in self.stats and 'total' in self.stats and 'percent' in self.stats:
self.views['percent']['decoration'] = self.get_alert_log(self.stats['used'], maximum=self.stats['total'])
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# First line
# total%
msg = '{:4}'.format('SWAP')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:2}'.format(self.trend_msg(self.get_trend('percent')))
ret.append(self.curse_add_line(msg))
# Percent memory usage
msg = '{:>6.1%}'.format(self.stats['percent'] / 100)
ret.append(self.curse_add_line(msg, self.get_views(key='percent', option='decoration')))
# Second line
# total
ret.append(self.curse_new_line())
# Total memory usage
ret.extend(self.curse_add_stat('total', width=15))
# Third line
# used
ret.append(self.curse_new_line())
# Used memory usage
ret.extend(self.curse_add_stat('used', width=15))
# Fourth line
# free
ret.append(self.curse_new_line())
# Free memory usage
ret.extend(self.curse_add_stat('free', width=15))
return ret

View File

@ -0,0 +1,389 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Network plugin."""
from __future__ import unicode_literals
import base64
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
import psutil
# {'interface_name': 'mpqemubr0-dummy',
# 'alias': None,
# 'time_since_update': 2.081636428833008,
# 'cumulative_rx': 0,
# 'rx': 0, 'cumulative_tx': 0, 'tx': 0, 'cumulative_cx': 0, 'cx': 0,
# 'is_up': False,
# 'speed': 0,
# 'key': 'interface_name'}
# Fields description
fields_description = {
'interface_name': {'description': 'Interface name.', 'unit': 'string'},
'alias': {'description': 'Interface alias name (optional).', 'unit': 'string'},
'rx': {'description': 'The received/input rate (in bit per second).', 'unit': 'bps'},
'tx': {'description': 'The sent/output rate (in bit per second).', 'unit': 'bps'},
'cx': {'description': 'The cumulative received+sent rate (in bit per second).', 'unit': 'bps'},
'cumulative_rx': {
'description': 'The number of bytes received through the interface (cumulative).',
'unit': 'bytes',
},
'cumulative_tx': {'description': 'The number of bytes sent through the interface (cumulative).', 'unit': 'bytes'},
'cumulative_cx': {
'description': 'The cumulative number of bytes reveived and sent through the interface (cumulative).',
'unit': 'bytes',
},
'speed': {
'description': 'Maximum interface speed (in bit per second). Can return 0 on some operating-system.',
'unit': 'bps',
},
'is_up': {'description': 'Is the interface up ?', 'unit': 'bool'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# http://www.net-snmp.org/docs/mibs/interfaces.html
# Dict key = interface_name
snmp_oid = {
'default': {
'interface_name': '1.3.6.1.2.1.2.2.1.2',
'cumulative_rx': '1.3.6.1.2.1.2.2.1.10',
'cumulative_tx': '1.3.6.1.2.1.2.2.1.16',
}
}
# Define the history items list
items_history_list = [
{'name': 'rx', 'description': 'Download rate per second', 'y_unit': 'bit/s'},
{'name': 'tx', 'description': 'Upload rate per second', 'y_unit': 'bit/s'},
]
class PluginModel(GlancesPluginModel):
"""Glances network plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
items_history_list=items_history_list,
fields_description=fields_description,
stats_init_value=[],
)
# We want to display the stat in the curse interface
self.display_curse = True
# Hide stats if it has never been != 0
if config is not None:
self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
else:
self.hide_zero = False
self.hide_zero_fields = ['rx', 'tx']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def get_key(self):
"""Return the key of the list."""
return 'interface_name'
# @GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update network stats using the input method.
:return: list of stats dict (one dict per interface)
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab network interface stat using the psutil net_io_counter method
try:
net_io_counters = psutil.net_io_counters(pernic=True)
except UnicodeDecodeError as e:
logger.debug('Can not get network interface counters ({})'.format(e))
return self.stats
# Grab interface's status (issue #765)
# Grab interface's speed (issue #718)
net_status = {}
try:
net_status = psutil.net_if_stats()
except OSError as e:
# see psutil #797/glances #1106
logger.debug('Can not get network interface status ({})'.format(e))
# Previous network interface stats are stored in the network_old variable
if not hasattr(self, 'network_old'):
# First call, we init the network_old var
try:
self.network_old = net_io_counters
except (IOError, UnboundLocalError):
pass
return self.stats
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
time_since_update = getTimeSinceLastUpdate('net')
# Loop over interfaces
network_new = net_io_counters
for net in network_new:
# Do not take hidden interface into account
# or KeyError: 'eth0' when interface is not connected #1348
if not self.is_display(net) or net not in net_status:
continue
try:
cumulative_rx = network_new[net].bytes_recv
cumulative_tx = network_new[net].bytes_sent
cumulative_cx = cumulative_rx + cumulative_tx
rx = cumulative_rx - self.network_old[net].bytes_recv
tx = cumulative_tx - self.network_old[net].bytes_sent
cx = rx + tx
netstat = {
'interface_name': net,
'alias': self.has_alias(net),
'time_since_update': time_since_update,
'cumulative_rx': cumulative_rx,
'rx': rx,
'cumulative_tx': cumulative_tx,
'tx': tx,
'cumulative_cx': cumulative_cx,
'cx': cx,
# Interface status
'is_up': net_status[net].isup,
# Interface speed in Mbps, convert it to bps
# Can be always 0 on some OSes
'speed': net_status[net].speed * 1048576,
# Set the key for the dict
'key': self.get_key(),
}
except KeyError:
continue
else:
# Append the interface stats to the list
stats.append(netstat)
# Save stats to compute next bitrate
self.network_old = network_new
elif self.input_method == 'snmp':
# Update stats using SNMP
# SNMP bulk command to get all network interface in one shot
try:
net_io_counters = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
net_io_counters = self.get_stats_snmp(snmp_oid=snmp_oid['default'], bulk=True)
# Previous network interface stats are stored in the network_old variable
if not hasattr(self, 'network_old'):
# First call, we init the network_old var
try:
self.network_old = net_io_counters
except (IOError, UnboundLocalError):
pass
else:
# See description in the 'local' block
time_since_update = getTimeSinceLastUpdate('net')
# Loop over interfaces
network_new = net_io_counters
for net in network_new:
# Do not take hidden interface into account
if not self.is_display(net):
continue
try:
# Windows: a tips is needed to convert HEX to TXT
# http://blogs.technet.com/b/networking/archive/2009/12/18/how-to-query-the-list-of-network-interfaces-using-snmp-via-the-ifdescr-counter.aspx
if self.short_system_name == 'windows':
try:
interface_name = str(base64.b16decode(net[2:-2].upper()))
except TypeError:
interface_name = net
else:
interface_name = net
cumulative_rx = float(network_new[net]['cumulative_rx'])
cumulative_tx = float(network_new[net]['cumulative_tx'])
cumulative_cx = cumulative_rx + cumulative_tx
rx = cumulative_rx - float(self.network_old[net]['cumulative_rx'])
tx = cumulative_tx - float(self.network_old[net]['cumulative_tx'])
cx = rx + tx
netstat = {
'interface_name': interface_name,
'alias': self.has_alias(interface_name),
'time_since_update': time_since_update,
'cumulative_rx': cumulative_rx,
'rx': rx,
'cumulative_tx': cumulative_tx,
'tx': tx,
'cumulative_cx': cumulative_cx,
'cx': cx,
}
except KeyError:
continue
else:
netstat['key'] = self.get_key()
stats.append(netstat)
# Save stats to compute next bitrate
self.network_old = network_new
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Check if the stats should be hidden
self.update_views_hidden()
# Add specifics information
# Alert
for i in self.get_raw():
if i['time_since_update'] == 0:
# Skip alert if no timespan to measure
continue
if_real_name = i['interface_name'].split(':')[0]
# Convert rate in bps (to be able to compare to interface speed)
bps_rx = int(i['rx'] // i['time_since_update'] * 8)
bps_tx = int(i['tx'] // i['time_since_update'] * 8)
# Decorate the bitrate with the configuration file thresholds
alert_rx = self.get_alert(bps_rx, header=if_real_name + '_rx')
alert_tx = self.get_alert(bps_tx, header=if_real_name + '_tx')
# If nothing is define in the configuration file...
# ... then use the interface speed (not available on all systems)
if alert_rx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
alert_rx = self.get_alert(current=bps_rx, maximum=i['speed'], header='rx')
if alert_tx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
alert_tx = self.get_alert(current=bps_tx, maximum=i['speed'], header='tx')
# then decorates
self.views[i[self.get_key()]]['rx']['decoration'] = alert_rx
self.views[i[self.get_key()]]['tx']['decoration'] = alert_tx
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Header
msg = '{:{width}}'.format('NETWORK', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.network_cumul:
# Cumulative stats
if args.network_sum:
# Sum stats
msg = '{:>14}'.format('Rx+Tx')
ret.append(self.curse_add_line(msg))
else:
# Rx/Tx stats
msg = '{:>7}'.format('Rx')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Tx')
ret.append(self.curse_add_line(msg))
else:
# Bitrate stats
if args.network_sum:
# Sum stats
msg = '{:>14}'.format('Rx+Tx/s')
ret.append(self.curse_add_line(msg))
else:
msg = '{:>7}'.format('Rx/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Tx/s')
ret.append(self.curse_add_line(msg))
# Interface list (sorted by name)
for i in self.sorted_stats():
# Do not display interface in down state (issue #765)
if ('is_up' in i) and (i['is_up'] is False):
continue
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields]):
continue
# Format stats
# Is there an alias for the interface name ?
if i['alias'] is None:
if_name = i['interface_name'].split(':')[0]
else:
if_name = i['alias']
if len(if_name) > name_max_width:
# Cut interface name if it is too long
if_name = '_' + if_name[-name_max_width + 1 :]
if args.byte:
# Bytes per second (for dummy)
to_bit = 1
unit = ''
else:
# Bits per second (for real network administrator | Default)
to_bit = 8
unit = 'b'
if args.network_cumul:
rx = self.auto_unit(int(i['cumulative_rx'] * to_bit)) + unit
tx = self.auto_unit(int(i['cumulative_tx'] * to_bit)) + unit
sx = self.auto_unit(int(i['cumulative_rx'] * to_bit) + int(i['cumulative_tx'] * to_bit)) + unit
else:
rx = self.auto_unit(int(i['rx'] // i['time_since_update'] * to_bit)) + unit
tx = self.auto_unit(int(i['tx'] // i['time_since_update'] * to_bit)) + unit
sx = (
self.auto_unit(
int(i['rx'] // i['time_since_update'] * to_bit)
+ int(i['tx'] // i['time_since_update'] * to_bit)
)
+ unit
)
# New line
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(if_name, width=name_max_width)
ret.append(self.curse_add_line(msg))
if args.network_sum:
msg = '{:>14}'.format(sx)
ret.append(self.curse_add_line(msg))
else:
msg = '{:>7}'.format(rx)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='rx', option='decoration'))
)
msg = '{:>7}'.format(tx)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='tx', option='decoration'))
)
return ret

View File

@ -1,389 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Network plugin."""
from __future__ import unicode_literals
import base64
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
import psutil
# {'interface_name': 'mpqemubr0-dummy',
# 'alias': None,
# 'time_since_update': 2.081636428833008,
# 'cumulative_rx': 0,
# 'rx': 0, 'cumulative_tx': 0, 'tx': 0, 'cumulative_cx': 0, 'cx': 0,
# 'is_up': False,
# 'speed': 0,
# 'key': 'interface_name'}
# Fields description
fields_description = {
'interface_name': {'description': 'Interface name.', 'unit': 'string'},
'alias': {'description': 'Interface alias name (optional).', 'unit': 'string'},
'rx': {'description': 'The received/input rate (in bit per second).', 'unit': 'bps'},
'tx': {'description': 'The sent/output rate (in bit per second).', 'unit': 'bps'},
'cx': {'description': 'The cumulative received+sent rate (in bit per second).', 'unit': 'bps'},
'cumulative_rx': {
'description': 'The number of bytes received through the interface (cumulative).',
'unit': 'bytes',
},
'cumulative_tx': {'description': 'The number of bytes sent through the interface (cumulative).', 'unit': 'bytes'},
'cumulative_cx': {
'description': 'The cumulative number of bytes reveived and sent through the interface (cumulative).',
'unit': 'bytes',
},
'speed': {
'description': 'Maximum interface speed (in bit per second). Can return 0 on some operating-system.',
'unit': 'bps',
},
'is_up': {'description': 'Is the interface up ?', 'unit': 'bool'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# http://www.net-snmp.org/docs/mibs/interfaces.html
# Dict key = interface_name
snmp_oid = {
'default': {
'interface_name': '1.3.6.1.2.1.2.2.1.2',
'cumulative_rx': '1.3.6.1.2.1.2.2.1.10',
'cumulative_tx': '1.3.6.1.2.1.2.2.1.16',
}
}
# Define the history items list
items_history_list = [
{'name': 'rx', 'description': 'Download rate per second', 'y_unit': 'bit/s'},
{'name': 'tx', 'description': 'Upload rate per second', 'y_unit': 'bit/s'},
]
class PluginModel(GlancesPluginModel):
"""Glances network plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
items_history_list=items_history_list,
fields_description=fields_description,
stats_init_value=[],
)
# We want to display the stat in the curse interface
self.display_curse = True
# Hide stats if it has never been != 0
if config is not None:
self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
else:
self.hide_zero = False
self.hide_zero_fields = ['rx', 'tx']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def get_key(self):
"""Return the key of the list."""
return 'interface_name'
# @GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update network stats using the input method.
:return: list of stats dict (one dict per interface)
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab network interface stat using the psutil net_io_counter method
try:
net_io_counters = psutil.net_io_counters(pernic=True)
except UnicodeDecodeError as e:
logger.debug('Can not get network interface counters ({})'.format(e))
return self.stats
# Grab interface's status (issue #765)
# Grab interface's speed (issue #718)
net_status = {}
try:
net_status = psutil.net_if_stats()
except OSError as e:
# see psutil #797/glances #1106
logger.debug('Can not get network interface status ({})'.format(e))
# Previous network interface stats are stored in the network_old variable
if not hasattr(self, 'network_old'):
# First call, we init the network_old var
try:
self.network_old = net_io_counters
except (IOError, UnboundLocalError):
pass
return self.stats
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
time_since_update = getTimeSinceLastUpdate('net')
# Loop over interfaces
network_new = net_io_counters
for net in network_new:
# Do not take hidden interface into account
# or KeyError: 'eth0' when interface is not connected #1348
if not self.is_display(net) or net not in net_status:
continue
try:
cumulative_rx = network_new[net].bytes_recv
cumulative_tx = network_new[net].bytes_sent
cumulative_cx = cumulative_rx + cumulative_tx
rx = cumulative_rx - self.network_old[net].bytes_recv
tx = cumulative_tx - self.network_old[net].bytes_sent
cx = rx + tx
netstat = {
'interface_name': net,
'alias': self.has_alias(net),
'time_since_update': time_since_update,
'cumulative_rx': cumulative_rx,
'rx': rx,
'cumulative_tx': cumulative_tx,
'tx': tx,
'cumulative_cx': cumulative_cx,
'cx': cx,
# Interface status
'is_up': net_status[net].isup,
# Interface speed in Mbps, convert it to bps
# Can be always 0 on some OSes
'speed': net_status[net].speed * 1048576,
# Set the key for the dict
'key': self.get_key(),
}
except KeyError:
continue
else:
# Append the interface stats to the list
stats.append(netstat)
# Save stats to compute next bitrate
self.network_old = network_new
elif self.input_method == 'snmp':
# Update stats using SNMP
# SNMP bulk command to get all network interface in one shot
try:
net_io_counters = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
net_io_counters = self.get_stats_snmp(snmp_oid=snmp_oid['default'], bulk=True)
# Previous network interface stats are stored in the network_old variable
if not hasattr(self, 'network_old'):
# First call, we init the network_old var
try:
self.network_old = net_io_counters
except (IOError, UnboundLocalError):
pass
else:
# See description in the 'local' block
time_since_update = getTimeSinceLastUpdate('net')
# Loop over interfaces
network_new = net_io_counters
for net in network_new:
# Do not take hidden interface into account
if not self.is_display(net):
continue
try:
# Windows: a tips is needed to convert HEX to TXT
# http://blogs.technet.com/b/networking/archive/2009/12/18/how-to-query-the-list-of-network-interfaces-using-snmp-via-the-ifdescr-counter.aspx
if self.short_system_name == 'windows':
try:
interface_name = str(base64.b16decode(net[2:-2].upper()))
except TypeError:
interface_name = net
else:
interface_name = net
cumulative_rx = float(network_new[net]['cumulative_rx'])
cumulative_tx = float(network_new[net]['cumulative_tx'])
cumulative_cx = cumulative_rx + cumulative_tx
rx = cumulative_rx - float(self.network_old[net]['cumulative_rx'])
tx = cumulative_tx - float(self.network_old[net]['cumulative_tx'])
cx = rx + tx
netstat = {
'interface_name': interface_name,
'alias': self.has_alias(interface_name),
'time_since_update': time_since_update,
'cumulative_rx': cumulative_rx,
'rx': rx,
'cumulative_tx': cumulative_tx,
'tx': tx,
'cumulative_cx': cumulative_cx,
'cx': cx,
}
except KeyError:
continue
else:
netstat['key'] = self.get_key()
stats.append(netstat)
# Save stats to compute next bitrate
self.network_old = network_new
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Check if the stats should be hidden
self.update_views_hidden()
# Add specifics information
# Alert
for i in self.get_raw():
if i['time_since_update'] == 0:
# Skip alert if no timespan to measure
continue
if_real_name = i['interface_name'].split(':')[0]
# Convert rate in bps (to be able to compare to interface speed)
bps_rx = int(i['rx'] // i['time_since_update'] * 8)
bps_tx = int(i['tx'] // i['time_since_update'] * 8)
# Decorate the bitrate with the configuration file thresholds
alert_rx = self.get_alert(bps_rx, header=if_real_name + '_rx')
alert_tx = self.get_alert(bps_tx, header=if_real_name + '_tx')
# If nothing is define in the configuration file...
# ... then use the interface speed (not available on all systems)
if alert_rx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
alert_rx = self.get_alert(current=bps_rx, maximum=i['speed'], header='rx')
if alert_tx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
alert_tx = self.get_alert(current=bps_tx, maximum=i['speed'], header='tx')
# then decorates
self.views[i[self.get_key()]]['rx']['decoration'] = alert_rx
self.views[i[self.get_key()]]['tx']['decoration'] = alert_tx
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Header
msg = '{:{width}}'.format('NETWORK', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.network_cumul:
# Cumulative stats
if args.network_sum:
# Sum stats
msg = '{:>14}'.format('Rx+Tx')
ret.append(self.curse_add_line(msg))
else:
# Rx/Tx stats
msg = '{:>7}'.format('Rx')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Tx')
ret.append(self.curse_add_line(msg))
else:
# Bitrate stats
if args.network_sum:
# Sum stats
msg = '{:>14}'.format('Rx+Tx/s')
ret.append(self.curse_add_line(msg))
else:
msg = '{:>7}'.format('Rx/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Tx/s')
ret.append(self.curse_add_line(msg))
# Interface list (sorted by name)
for i in self.sorted_stats():
# Do not display interface in down state (issue #765)
if ('is_up' in i) and (i['is_up'] is False):
continue
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields]):
continue
# Format stats
# Is there an alias for the interface name ?
if i['alias'] is None:
if_name = i['interface_name'].split(':')[0]
else:
if_name = i['alias']
if len(if_name) > name_max_width:
# Cut interface name if it is too long
if_name = '_' + if_name[-name_max_width + 1 :]
if args.byte:
# Bytes per second (for dummy)
to_bit = 1
unit = ''
else:
# Bits per second (for real network administrator | Default)
to_bit = 8
unit = 'b'
if args.network_cumul:
rx = self.auto_unit(int(i['cumulative_rx'] * to_bit)) + unit
tx = self.auto_unit(int(i['cumulative_tx'] * to_bit)) + unit
sx = self.auto_unit(int(i['cumulative_rx'] * to_bit) + int(i['cumulative_tx'] * to_bit)) + unit
else:
rx = self.auto_unit(int(i['rx'] // i['time_since_update'] * to_bit)) + unit
tx = self.auto_unit(int(i['tx'] // i['time_since_update'] * to_bit)) + unit
sx = (
self.auto_unit(
int(i['rx'] // i['time_since_update'] * to_bit)
+ int(i['tx'] // i['time_since_update'] * to_bit)
)
+ unit
)
# New line
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(if_name, width=name_max_width)
ret.append(self.curse_add_line(msg))
if args.network_sum:
msg = '{:>14}'.format(sx)
ret.append(self.curse_add_line(msg))
else:
msg = '{:>7}'.format(rx)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='rx', option='decoration'))
)
msg = '{:>7}'.format(tx)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='tx', option='decoration'))
)
return ret

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Now (current date) plugin."""
from time import tzname, strftime
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Plugin to get the current date/time.
stats is (string)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'bottom'
if args.strftime_format:
self.strftime = args.strftime_format
elif config is not None:
if 'global' in config.as_dict():
self.strftime = config.as_dict()['global']['strftime_format']
def reset(self):
"""Reset/init the stats."""
self.stats = ''
def update(self):
"""Update current date/time."""
# Had to convert it to string because datetime is not JSON serializable
# Add the time zone (issue #1249 / #1337 / #1598)
if self.strftime:
self.stats = strftime(self.strftime)
else:
if len(tzname[1]) > 6:
self.stats = strftime('%Y-%m-%d %H:%M:%S %z')
else:
self.stats = strftime('%Y-%m-%d %H:%M:%S %Z')
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
if not self.stats or self.is_disabled():
return ret
# Build the string message
# 23 is the padding for the process list
msg = '{:23}'.format(self.stats)
ret.append(self.curse_add_line(msg))
return ret

View File

@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Now (current date) plugin."""
from time import tzname, strftime
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Plugin to get the current date/time.
stats is (string)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'bottom'
if args.strftime_format:
self.strftime = args.strftime_format
elif config is not None:
if 'global' in config.as_dict():
self.strftime = config.as_dict()['global']['strftime_format']
def reset(self):
"""Reset/init the stats."""
self.stats = ''
def update(self):
"""Update current date/time."""
# Had to convert it to string because datetime is not JSON serializable
# Add the time zone (issue #1249 / #1337 / #1598)
if self.strftime:
self.stats = strftime(self.strftime)
else:
if len(tzname[1]) > 6:
self.stats = strftime('%Y-%m-%d %H:%M:%S %z')
else:
self.stats = strftime('%Y-%m-%d %H:%M:%S %Z')
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
if not self.stats or self.is_disabled():
return ret
# Build the string message
# 23 is the padding for the process list
msg = '{:23}'.format(self.stats)
ret.append(self.curse_add_line(msg))
return ret

View File

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Per-CPU plugin."""
from glances.cpu_percent import cpu_percent
from glances.plugins.plugin.model import GlancesPluginModel
# Define the history items list
items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
{'name': 'system', 'description': 'System CPU usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances per-CPU plugin.
'stats' is a list of dictionaries that contain the utilization percentages
for each CPU.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
def get_key(self):
"""Return the key of the list."""
return 'cpu_number'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update per-CPU stats using the input method."""
# Init new stats
stats = self.get_init_value()
# Grab per-CPU stats using psutil's cpu_percent(percpu=True) and
# cpu_times_percent(percpu=True) methods
if self.input_method == 'local':
stats = cpu_percent.get(percpu=True)
else:
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist...
if not self.stats or not self.args.percpu or self.is_disabled():
return ret
# Build the string message
if self.is_disabled('quicklook'):
msg = '{:7}'.format('PER CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
# Per CPU stats displayed per line
for stat in ['user', 'system', 'idle', 'iowait', 'steal']:
if stat not in self.stats[0]:
continue
msg = '{:>7}'.format(stat)
ret.append(self.curse_add_line(msg))
# Per CPU stats displayed per column
for cpu in self.stats:
ret.append(self.curse_new_line())
if self.is_disabled('quicklook'):
try:
msg = '{:6.1f}%'.format(cpu['total'])
except TypeError:
# TypeError: string indices must be integers (issue #1027)
msg = '{:>6}%'.format('?')
ret.append(self.curse_add_line(msg))
for stat in ['user', 'system', 'idle', 'iowait', 'steal']:
if stat not in self.stats[0]:
continue
try:
msg = '{:6.1f}%'.format(cpu[stat])
except TypeError:
msg = '{:>6}%'.format('?')
ret.append(self.curse_add_line(msg, self.get_alert(cpu[stat], header=stat)))
return ret

View File

@ -1,102 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Per-CPU plugin."""
from glances.cpu_percent import cpu_percent
from glances.plugins.plugin.model import GlancesPluginModel
# Define the history items list
items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
{'name': 'system', 'description': 'System CPU usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances per-CPU plugin.
'stats' is a list of dictionaries that contain the utilization percentages
for each CPU.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
def get_key(self):
"""Return the key of the list."""
return 'cpu_number'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update per-CPU stats using the input method."""
# Init new stats
stats = self.get_init_value()
# Grab per-CPU stats using psutil's cpu_percent(percpu=True) and
# cpu_times_percent(percpu=True) methods
if self.input_method == 'local':
stats = cpu_percent.get(percpu=True)
else:
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist...
if not self.stats or not self.args.percpu or self.is_disabled():
return ret
# Build the string message
if self.is_disabled('quicklook'):
msg = '{:7}'.format('PER CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
# Per CPU stats displayed per line
for stat in ['user', 'system', 'idle', 'iowait', 'steal']:
if stat not in self.stats[0]:
continue
msg = '{:>7}'.format(stat)
ret.append(self.curse_add_line(msg))
# Per CPU stats displayed per column
for cpu in self.stats:
ret.append(self.curse_new_line())
if self.is_disabled('quicklook'):
try:
msg = '{:6.1f}%'.format(cpu['total'])
except TypeError:
# TypeError: string indices must be integers (issue #1027)
msg = '{:>6}%'.format('?')
ret.append(self.curse_add_line(msg))
for stat in ['user', 'system', 'idle', 'iowait', 'steal']:
if stat not in self.stats[0]:
continue
try:
msg = '{:6.1f}%'.format(cpu[stat])
except TypeError:
msg = '{:>6}%'.format('?')
ret.append(self.curse_add_line(msg, self.get_alert(cpu[stat], header=stat)))
return ret

View File

@ -0,0 +1,352 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Ports scanner plugin."""
import os
import subprocess
import threading
import socket
import time
import numbers
from glances.globals import WINDOWS, MACOS, BSD, bool_type
from glances.ports_list import GlancesPortsList
from glances.web_list import GlancesWebList
from glances.timer import Counter
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
try:
import requests
requests_tag = True
except ImportError as e:
requests_tag = False
logger.warning("Missing Python Lib ({}), Ports plugin is limited to port scanning".format(e))
class PluginModel(GlancesPluginModel):
"""Glances ports scanner plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init stats
self.stats = (
GlancesPortsList(config=config, args=args).get_ports_list()
+ GlancesWebList(config=config, args=args).get_web_list()
)
# Global Thread running all the scans
self._thread = None
def exit(self):
"""Overwrite the exit method to close threads."""
if self._thread is not None:
self._thread.stop()
# Call the father class
super(PluginModel, self).exit()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the ports list."""
if self.input_method == 'local':
# Only refresh:
# * if there is not other scanning thread
# * every refresh seconds (define in the configuration file)
if self._thread is None:
thread_is_running = False
else:
thread_is_running = self._thread.is_alive()
if not thread_is_running:
# Run ports scanner
self._thread = ThreadScanner(self.stats)
self._thread.start()
else:
# Not available in SNMP mode
pass
return self.stats
def get_key(self):
"""Return the key of the list."""
return 'indice'
def get_ports_alert(self, port, header="", log=False):
"""Return the alert status relative to the port scan return value."""
ret = 'OK'
if port['status'] is None:
ret = 'CAREFUL'
elif port['status'] == 0:
ret = 'CRITICAL'
elif (
isinstance(port['status'], (float, int))
and port['rtt_warning'] is not None
and port['status'] > port['rtt_warning']
):
ret = 'WARNING'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, port[self.get_key()])
return ret
def get_web_alert(self, web, header="", log=False):
"""Return the alert status relative to the web/url scan return value."""
ret = 'OK'
if web['status'] is None:
ret = 'CAREFUL'
elif web['status'] not in [200, 301, 302]:
ret = 'CRITICAL'
elif web['rtt_warning'] is not None and web['elapsed'] > web['rtt_warning']:
ret = 'WARNING'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, web[self.get_key()])
return ret
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
# Only process if stats exist and display plugin enable...
ret = []
if not self.stats or args.disable_ports:
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Build the string message
for p in self.stats:
if 'host' in p:
if p['host'] is None:
status = 'None'
elif p['status'] is None:
status = 'Scanning'
elif isinstance(p['status'], bool_type) and p['status'] is True:
status = 'Open'
elif p['status'] == 0:
status = 'Timeout'
else:
# Convert second to ms
status = '{0:.0f}ms'.format(p['status'] * 1000.0)
msg = '{:{width}}'.format(p['description'][0:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
msg = '{:>9}'.format(status)
ret.append(self.curse_add_line(msg, self.get_ports_alert(p, header=p['indice'] + '_rtt')))
ret.append(self.curse_new_line())
elif 'url' in p:
msg = '{:{width}}'.format(p['description'][0:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
if isinstance(p['status'], numbers.Number):
status = 'Code {}'.format(p['status'])
elif p['status'] is None:
status = 'Scanning'
else:
status = p['status']
msg = '{:>9}'.format(status)
ret.append(self.curse_add_line(msg, self.get_web_alert(p, header=p['indice'] + '_rtt')))
ret.append(self.curse_new_line())
# Delete the last empty line
try:
ret.pop()
except IndexError:
pass
return ret
class ThreadScanner(threading.Thread):
"""
Specific thread for the port/web scanner.
stats is a list of dict
"""
def __init__(self, stats):
"""Init the class."""
logger.debug("ports plugin - Create thread for scan list {}".format(stats))
super(ThreadScanner, self).__init__()
# Event needed to stop properly the thread
self._stopper = threading.Event()
# The class return the stats as a list of dict
self._stats = stats
# Is part of Ports plugin
self.plugin_name = "ports"
def run(self):
"""Grab the stats.
Infinite loop, should be stopped by calling the stop() method.
"""
for p in self._stats:
# End of the thread has been asked
if self.stopped():
break
# Scan a port (ICMP or TCP)
if 'port' in p:
self._port_scan(p)
# Had to wait between two scans
# If not, result are not ok
time.sleep(1)
# Scan an URL
elif 'url' in p and requests_tag:
self._web_scan(p)
@property
def stats(self):
"""Stats getter."""
return self._stats
@stats.setter
def stats(self, value):
"""Stats setter."""
self._stats = value
def stop(self, timeout=None):
"""Stop the thread."""
logger.debug("ports plugin - Close thread for scan list {}".format(self._stats))
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()
def _web_scan(self, web):
"""Scan the Web/URL (dict) and update the status key."""
try:
req = requests.head(
web['url'],
allow_redirects=True,
verify=web['ssl_verify'],
proxies=web['proxies'],
timeout=web['timeout'],
)
except Exception as e:
logger.debug(e)
web['status'] = 'Error'
web['elapsed'] = 0
else:
web['status'] = req.status_code
web['elapsed'] = req.elapsed.total_seconds()
return web
def _port_scan(self, port):
"""Scan the port structure (dict) and update the status key."""
if int(port['port']) == 0:
return self._port_scan_icmp(port)
else:
return self._port_scan_tcp(port)
def _resolv_name(self, hostname):
"""Convert hostname to IP address."""
ip = hostname
try:
ip = socket.gethostbyname(hostname)
except Exception as e:
logger.debug("{}: Cannot convert {} to IP address ({})".format(self.plugin_name, hostname, e))
return ip
def _port_scan_icmp(self, port):
"""Scan the (ICMP) port structure (dict) and update the status key."""
ret = None
# Create the ping command
# Use the system ping command because it already have the sticky bit set
# Python can not create ICMP packet with non root right
if WINDOWS:
timeout_opt = '-w'
count_opt = '-n'
elif MACOS or BSD:
timeout_opt = '-t'
count_opt = '-c'
else:
# Linux and co...
timeout_opt = '-W'
count_opt = '-c'
# Build the command line
# Note: Only string are allowed
cmd = [
'ping',
count_opt,
'1',
timeout_opt,
str(self._resolv_name(port['timeout'])),
self._resolv_name(port['host']),
]
fnull = open(os.devnull, 'w')
try:
counter = Counter()
ret = subprocess.check_call(cmd, stdout=fnull, stderr=fnull, close_fds=True)
if ret == 0:
port['status'] = counter.get()
else:
port['status'] = False
except subprocess.CalledProcessError:
# Correct issue #1084: No Offline status for timed-out ports
port['status'] = False
except Exception as e:
logger.debug("{}: Error while pinging host {} ({})".format(self.plugin_name, port['host'], e))
fnull.close()
return ret
def _port_scan_tcp(self, port):
"""Scan the (TCP) port structure (dict) and update the status key."""
ret = None
# Create and configure the scanning socket
try:
socket.setdefaulttimeout(port['timeout'])
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except Exception as e:
logger.debug("{}: Error while creating scanning socket ({})".format(self.plugin_name, e))
# Scan port
ip = self._resolv_name(port['host'])
counter = Counter()
try:
ret = _socket.connect_ex((ip, int(port['port'])))
except Exception as e:
logger.debug("{}: Error while scanning port {} ({})".format(self.plugin_name, port, e))
else:
if ret == 0:
port['status'] = counter.get()
else:
port['status'] = False
finally:
_socket.close()
return ret

View File

@ -1,352 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Ports scanner plugin."""
import os
import subprocess
import threading
import socket
import time
import numbers
from glances.globals import WINDOWS, MACOS, BSD, bool_type
from glances.ports_list import GlancesPortsList
from glances.web_list import GlancesWebList
from glances.timer import Counter
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
try:
import requests
requests_tag = True
except ImportError as e:
requests_tag = False
logger.warning("Missing Python Lib ({}), Ports plugin is limited to port scanning".format(e))
class PluginModel(GlancesPluginModel):
"""Glances ports scanner plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init stats
self.stats = (
GlancesPortsList(config=config, args=args).get_ports_list()
+ GlancesWebList(config=config, args=args).get_web_list()
)
# Global Thread running all the scans
self._thread = None
def exit(self):
"""Overwrite the exit method to close threads."""
if self._thread is not None:
self._thread.stop()
# Call the father class
super(PluginModel, self).exit()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the ports list."""
if self.input_method == 'local':
# Only refresh:
# * if there is not other scanning thread
# * every refresh seconds (define in the configuration file)
if self._thread is None:
thread_is_running = False
else:
thread_is_running = self._thread.is_alive()
if not thread_is_running:
# Run ports scanner
self._thread = ThreadScanner(self.stats)
self._thread.start()
else:
# Not available in SNMP mode
pass
return self.stats
def get_key(self):
"""Return the key of the list."""
return 'indice'
def get_ports_alert(self, port, header="", log=False):
"""Return the alert status relative to the port scan return value."""
ret = 'OK'
if port['status'] is None:
ret = 'CAREFUL'
elif port['status'] == 0:
ret = 'CRITICAL'
elif (
isinstance(port['status'], (float, int))
and port['rtt_warning'] is not None
and port['status'] > port['rtt_warning']
):
ret = 'WARNING'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, port[self.get_key()])
return ret
def get_web_alert(self, web, header="", log=False):
"""Return the alert status relative to the web/url scan return value."""
ret = 'OK'
if web['status'] is None:
ret = 'CAREFUL'
elif web['status'] not in [200, 301, 302]:
ret = 'CRITICAL'
elif web['rtt_warning'] is not None and web['elapsed'] > web['rtt_warning']:
ret = 'WARNING'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, web[self.get_key()])
return ret
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
# Only process if stats exist and display plugin enable...
ret = []
if not self.stats or args.disable_ports:
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Build the string message
for p in self.stats:
if 'host' in p:
if p['host'] is None:
status = 'None'
elif p['status'] is None:
status = 'Scanning'
elif isinstance(p['status'], bool_type) and p['status'] is True:
status = 'Open'
elif p['status'] == 0:
status = 'Timeout'
else:
# Convert second to ms
status = '{0:.0f}ms'.format(p['status'] * 1000.0)
msg = '{:{width}}'.format(p['description'][0:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
msg = '{:>9}'.format(status)
ret.append(self.curse_add_line(msg, self.get_ports_alert(p, header=p['indice'] + '_rtt')))
ret.append(self.curse_new_line())
elif 'url' in p:
msg = '{:{width}}'.format(p['description'][0:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
if isinstance(p['status'], numbers.Number):
status = 'Code {}'.format(p['status'])
elif p['status'] is None:
status = 'Scanning'
else:
status = p['status']
msg = '{:>9}'.format(status)
ret.append(self.curse_add_line(msg, self.get_web_alert(p, header=p['indice'] + '_rtt')))
ret.append(self.curse_new_line())
# Delete the last empty line
try:
ret.pop()
except IndexError:
pass
return ret
class ThreadScanner(threading.Thread):
"""
Specific thread for the port/web scanner.
stats is a list of dict
"""
def __init__(self, stats):
"""Init the class."""
logger.debug("ports plugin - Create thread for scan list {}".format(stats))
super(ThreadScanner, self).__init__()
# Event needed to stop properly the thread
self._stopper = threading.Event()
# The class return the stats as a list of dict
self._stats = stats
# Is part of Ports plugin
self.plugin_name = "ports"
def run(self):
"""Grab the stats.
Infinite loop, should be stopped by calling the stop() method.
"""
for p in self._stats:
# End of the thread has been asked
if self.stopped():
break
# Scan a port (ICMP or TCP)
if 'port' in p:
self._port_scan(p)
# Had to wait between two scans
# If not, result are not ok
time.sleep(1)
# Scan an URL
elif 'url' in p and requests_tag:
self._web_scan(p)
@property
def stats(self):
"""Stats getter."""
return self._stats
@stats.setter
def stats(self, value):
"""Stats setter."""
self._stats = value
def stop(self, timeout=None):
"""Stop the thread."""
logger.debug("ports plugin - Close thread for scan list {}".format(self._stats))
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()
def _web_scan(self, web):
"""Scan the Web/URL (dict) and update the status key."""
try:
req = requests.head(
web['url'],
allow_redirects=True,
verify=web['ssl_verify'],
proxies=web['proxies'],
timeout=web['timeout'],
)
except Exception as e:
logger.debug(e)
web['status'] = 'Error'
web['elapsed'] = 0
else:
web['status'] = req.status_code
web['elapsed'] = req.elapsed.total_seconds()
return web
def _port_scan(self, port):
"""Scan the port structure (dict) and update the status key."""
if int(port['port']) == 0:
return self._port_scan_icmp(port)
else:
return self._port_scan_tcp(port)
def _resolv_name(self, hostname):
"""Convert hostname to IP address."""
ip = hostname
try:
ip = socket.gethostbyname(hostname)
except Exception as e:
logger.debug("{}: Cannot convert {} to IP address ({})".format(self.plugin_name, hostname, e))
return ip
def _port_scan_icmp(self, port):
"""Scan the (ICMP) port structure (dict) and update the status key."""
ret = None
# Create the ping command
# Use the system ping command because it already have the sticky bit set
# Python can not create ICMP packet with non root right
if WINDOWS:
timeout_opt = '-w'
count_opt = '-n'
elif MACOS or BSD:
timeout_opt = '-t'
count_opt = '-c'
else:
# Linux and co...
timeout_opt = '-W'
count_opt = '-c'
# Build the command line
# Note: Only string are allowed
cmd = [
'ping',
count_opt,
'1',
timeout_opt,
str(self._resolv_name(port['timeout'])),
self._resolv_name(port['host']),
]
fnull = open(os.devnull, 'w')
try:
counter = Counter()
ret = subprocess.check_call(cmd, stdout=fnull, stderr=fnull, close_fds=True)
if ret == 0:
port['status'] = counter.get()
else:
port['status'] = False
except subprocess.CalledProcessError:
# Correct issue #1084: No Offline status for timed-out ports
port['status'] = False
except Exception as e:
logger.debug("{}: Error while pinging host {} ({})".format(self.plugin_name, port['host'], e))
fnull.close()
return ret
def _port_scan_tcp(self, port):
"""Scan the (TCP) port structure (dict) and update the status key."""
ret = None
# Create and configure the scanning socket
try:
socket.setdefaulttimeout(port['timeout'])
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except Exception as e:
logger.debug("{}: Error while creating scanning socket ({})".format(self.plugin_name, e))
# Scan port
ip = self._resolv_name(port['host'])
counter = Counter()
try:
ret = _socket.connect_ex((ip, int(port['port'])))
except Exception as e:
logger.debug("{}: Error while scanning port {} ({})".format(self.plugin_name, port, e))
else:
if ret == 0:
port['status'] = counter.get()
else:
port['status'] = False
finally:
_socket.close()
return ret

View File

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Process count plugin."""
from glances.processes import glances_processes, sort_for_human
from glances.plugins.plugin.model import GlancesPluginModel
# Define the history items list
items_history_list = [
{'name': 'total', 'description': 'Total number of processes', 'y_unit': ''},
{'name': 'running', 'description': 'Total number of running processes', 'y_unit': ''},
{'name': 'sleeping', 'description': 'Total number of sleeping processes', 'y_unit': ''},
{'name': 'thread', 'description': 'Total number of threads', 'y_unit': ''},
]
class PluginModel(GlancesPluginModel):
"""Glances process count plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
# We want to display the stat in the curse interface
self.display_curse = True
# Note: 'glances_processes' is already init in the glances_processes.py script
def enable_extended(self):
"""Enable extended stats."""
glances_processes.enable_extended()
def disable_extended(self):
"""Disable extended stats."""
glances_processes.disable_extended()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update processes stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Here, update is call for processcount AND processlist
glances_processes.update()
# Return the processes count
stats = glances_processes.get_count()
elif self.input_method == 'snmp':
# Update stats using SNMP
# Not available
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if args.disable_process:
msg = "PROCESSES DISABLED (press 'z' to display)"
ret.append(self.curse_add_line(msg))
return ret
if not self.stats:
return ret
# Display the filter (if it exists)
if glances_processes.process_filter is not None:
msg = 'Processes filter:'
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {} '.format(glances_processes.process_filter)
if glances_processes.process_filter_key is not None:
msg += 'on column {} '.format(glances_processes.process_filter_key)
ret.append(self.curse_add_line(msg, "FILTER"))
msg = '(\'ENTER\' to edit, \'E\' to reset)'
ret.append(self.curse_add_line(msg))
ret.append(self.curse_new_line())
# Build the string message
# Header
msg = 'TASKS'
ret.append(self.curse_add_line(msg, "TITLE"))
# Compute processes
other = self.stats['total']
msg = '{:>4}'.format(self.stats['total'])
ret.append(self.curse_add_line(msg))
if 'thread' in self.stats:
msg = ' ({} thr),'.format(self.stats['thread'])
ret.append(self.curse_add_line(msg))
if 'running' in self.stats:
other -= self.stats['running']
msg = ' {} run,'.format(self.stats['running'])
ret.append(self.curse_add_line(msg))
if 'sleeping' in self.stats:
other -= self.stats['sleeping']
msg = ' {} slp,'.format(self.stats['sleeping'])
ret.append(self.curse_add_line(msg))
msg = ' {} oth '.format(other)
ret.append(self.curse_add_line(msg))
# Display sort information
msg = 'Programs' if self.args.programs else 'Threads'
try:
sort_human = sort_for_human[glances_processes.sort_key]
except KeyError:
sort_human = glances_processes.sort_key
if glances_processes.auto_sort:
msg += ' sorted automatically'
ret.append(self.curse_add_line(msg))
msg = ' by {}'.format(sort_human)
else:
msg += ' sorted by {}'.format(sort_human)
ret.append(self.curse_add_line(msg))
# Return the message with decoration
return ret

View File

@ -1,138 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Process count plugin."""
from glances.processes import glances_processes, sort_for_human
from glances.plugins.plugin.model import GlancesPluginModel
# Define the history items list
items_history_list = [
{'name': 'total', 'description': 'Total number of processes', 'y_unit': ''},
{'name': 'running', 'description': 'Total number of running processes', 'y_unit': ''},
{'name': 'sleeping', 'description': 'Total number of sleeping processes', 'y_unit': ''},
{'name': 'thread', 'description': 'Total number of threads', 'y_unit': ''},
]
class PluginModel(GlancesPluginModel):
"""Glances process count plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
# We want to display the stat in the curse interface
self.display_curse = True
# Note: 'glances_processes' is already init in the glances_processes.py script
def enable_extended(self):
"""Enable extended stats."""
glances_processes.enable_extended()
def disable_extended(self):
"""Disable extended stats."""
glances_processes.disable_extended()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update processes stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Here, update is call for processcount AND processlist
glances_processes.update()
# Return the processes count
stats = glances_processes.get_count()
elif self.input_method == 'snmp':
# Update stats using SNMP
# Not available
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if args.disable_process:
msg = "PROCESSES DISABLED (press 'z' to display)"
ret.append(self.curse_add_line(msg))
return ret
if not self.stats:
return ret
# Display the filter (if it exists)
if glances_processes.process_filter is not None:
msg = 'Processes filter:'
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {} '.format(glances_processes.process_filter)
if glances_processes.process_filter_key is not None:
msg += 'on column {} '.format(glances_processes.process_filter_key)
ret.append(self.curse_add_line(msg, "FILTER"))
msg = '(\'ENTER\' to edit, \'E\' to reset)'
ret.append(self.curse_add_line(msg))
ret.append(self.curse_new_line())
# Build the string message
# Header
msg = 'TASKS'
ret.append(self.curse_add_line(msg, "TITLE"))
# Compute processes
other = self.stats['total']
msg = '{:>4}'.format(self.stats['total'])
ret.append(self.curse_add_line(msg))
if 'thread' in self.stats:
msg = ' ({} thr),'.format(self.stats['thread'])
ret.append(self.curse_add_line(msg))
if 'running' in self.stats:
other -= self.stats['running']
msg = ' {} run,'.format(self.stats['running'])
ret.append(self.curse_add_line(msg))
if 'sleeping' in self.stats:
other -= self.stats['sleeping']
msg = ' {} slp,'.format(self.stats['sleeping'])
ret.append(self.curse_add_line(msg))
msg = ' {} oth '.format(other)
ret.append(self.curse_add_line(msg))
# Display sort information
msg = 'Programs' if self.args.programs else 'Threads'
try:
sort_human = sort_for_human[glances_processes.sort_key]
except KeyError:
sort_human = glances_processes.sort_key
if glances_processes.auto_sort:
msg += ' sorted automatically'
ret.append(self.curse_add_line(msg))
msg = ' by {}'.format(sort_human)
else:
msg += ' sorted by {}'.format(sort_human)
ret.append(self.curse_add_line(msg))
# Return the message with decoration
return ret

View File

@ -0,0 +1,833 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Process list plugin."""
import os
import copy
from glances.logger import logger
from glances.globals import WINDOWS, key_exist_value_not_none_not_v
from glances.processes import glances_processes, sort_stats
from glances.outputs.glances_unicode import unicode_message
from glances.plugins.core import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
def seconds_to_hms(input_seconds):
"""Convert seconds to human-readable time."""
minutes, seconds = divmod(input_seconds, 60)
hours, minutes = divmod(minutes, 60)
hours = int(hours)
minutes = int(minutes)
seconds = str(int(seconds)).zfill(2)
return hours, minutes, seconds
def split_cmdline(bare_process_name, cmdline):
"""Return path, cmd and arguments for a process cmdline based on bare_process_name.
If first argument of cmdline starts with the bare_process_name then
cmdline will just be considered cmd and path will be empty (see https://github.com/nicolargo/glances/issues/1795)
:param bare_process_name: Name of the process from psutil
:param cmdline: cmdline from psutil
:return: Tuple with three strings, which are path, cmd and arguments of the process
"""
if cmdline[0].startswith(bare_process_name):
path, cmd = "", cmdline[0]
else:
path, cmd = os.path.split(cmdline[0])
arguments = ' '.join(cmdline[1:])
return path, cmd, arguments
class PluginModel(GlancesPluginModel):
"""Glances' processes plugin.
stats is a list
"""
# Define the header layout of the processes list columns
layout_header = {
'cpu': '{:<6} ',
'mem': '{:<5} ',
'virt': '{:<5} ',
'res': '{:<5} ',
'pid': '{:>{width}} ',
'user': '{:<10} ',
'time': '{:>8} ',
'thread': '{:<3} ',
'nice': '{:>3} ',
'status': '{:>1} ',
'ior': '{:>4} ',
'iow': '{:<4} ',
'command': '{} {}',
}
# Define the stat layout of the processes list columns
layout_stat = {
'cpu': '{:<6.1f}',
'cpu_no_digit': '{:<6.0f}',
'mem': '{:<5.1f} ',
'virt': '{:<5} ',
'res': '{:<5} ',
'pid': '{:>{width}} ',
'user': '{:<10} ',
'time': '{:>8} ',
'thread': '{:<3} ',
'nice': '{:>3} ',
'status': '{:>1} ',
'ior': '{:>4} ',
'iow': '{:<4} ',
'command': '{}',
'name': '[{}]',
}
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Trying to display proc time
self.tag_proc_time = True
# Call CorePluginModel to get the core number (needed when not in IRIX mode / Solaris mode)
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception:
self.nb_log_core = 0
# Get the max values (dict)
self.max_values = copy.deepcopy(glances_processes.max_values())
# Get the maximum PID number
# Use to optimize space (see https://github.com/nicolargo/glances/issues/959)
self.pid_max = glances_processes.pid_max
# Set the default sort key if it is defined in the configuration file
if config is not None:
if 'processlist' in config.as_dict() and 'sort_key' in config.as_dict()['processlist']:
logger.debug(
'Configuration overwrites processes sort key by {}'.format(
config.as_dict()['processlist']['sort_key']
)
)
glances_processes.set_sort_key(config.as_dict()['processlist']['sort_key'], False)
# The default sort key could also be overwrite by command line (see #1903)
if args.sort_processes_key is not None:
glances_processes.set_sort_key(args.sort_processes_key, False)
# Note: 'glances_processes' is already init in the processes.py script
def get_key(self):
"""Return the key of the list."""
return 'pid'
def update(self):
"""Update processes stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Note: Update is done in the processcount plugin
# Just return the processes list
if self.args.programs:
stats = glances_processes.getlist(as_programs=True)
else:
stats = glances_processes.getlist()
elif self.input_method == 'snmp':
# No SNMP grab for processes
pass
# Update the stats
self.stats = stats
# Get the max values (dict)
# Use Deep copy to avoid change between update and display
self.max_values = copy.deepcopy(glances_processes.max_values())
return self.stats
def get_nice_alert(self, value):
"""Return the alert relative to the Nice configuration list"""
value = str(value)
try:
if value in self.get_limit('nice_critical'):
return 'CRITICAL'
except KeyError:
pass
try:
if value in self.get_limit('nice_warning'):
return 'WARNING'
except KeyError:
pass
try:
if value in self.get_limit('nice_careful'):
return 'CAREFUL'
except KeyError:
pass
return 'DEFAULT'
def _get_process_curses_cpu(self, p, selected, args):
"""Return process CPU curses"""
if key_exist_value_not_none_not_v('cpu_percent', p, ''):
cpu_layout = self.layout_stat['cpu'] if p['cpu_percent'] < 100 else self.layout_stat['cpu_no_digit']
if args.disable_irix and self.nb_log_core != 0:
msg = cpu_layout.format(p['cpu_percent'] / float(self.nb_log_core))
else:
msg = cpu_layout.format(p['cpu_percent'])
alert = self.get_alert(
p['cpu_percent'],
highlight_zero=False,
is_max=(p['cpu_percent'] == self.max_values['cpu_percent']),
header="cpu",
)
ret = self.curse_add_line(msg, alert)
else:
msg = self.layout_header['cpu'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_mem(self, p, selected, args):
"""Return process MEM curses"""
if key_exist_value_not_none_not_v('memory_percent', p, ''):
msg = self.layout_stat['mem'].format(p['memory_percent'])
alert = self.get_alert(
p['memory_percent'],
highlight_zero=False,
is_max=(p['memory_percent'] == self.max_values['memory_percent']),
header="mem",
)
ret = self.curse_add_line(msg, alert)
else:
msg = self.layout_header['mem'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_vms(self, p, selected, args):
"""Return process VMS curses"""
if key_exist_value_not_none_not_v('memory_info', p, '', 1):
msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info'][1], low_precision=False))
ret = self.curse_add_line(msg, optional=True)
else:
msg = self.layout_header['virt'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_rss(self, p, selected, args):
"""Return process RSS curses"""
if key_exist_value_not_none_not_v('memory_info', p, '', 0):
msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info'][0], low_precision=False))
ret = self.curse_add_line(msg, optional=True)
else:
msg = self.layout_header['res'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_username(self, p, selected, args):
"""Return process username curses"""
if 'username' in p:
# docker internal users are displayed as ints only, therefore str()
# Correct issue #886 on Windows OS
msg = self.layout_stat['user'].format(str(p['username'])[:9])
ret = self.curse_add_line(msg)
else:
msg = self.layout_header['user'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_time(self, p, selected, args):
"""Return process time curses"""
try:
# Sum user and system time
user_system_time = p['cpu_times'][0] + p['cpu_times'][1]
except (OverflowError, TypeError):
# Catch OverflowError on some Amazon EC2 server
# See https://github.com/nicolargo/glances/issues/87
# Also catch TypeError on macOS
# See: https://github.com/nicolargo/glances/issues/622
# logger.debug("Cannot get TIME+ ({})".format(e))
msg = self.layout_header['time'].format('?')
ret = self.curse_add_line(msg, optional=True)
else:
hours, minutes, seconds = seconds_to_hms(user_system_time)
if hours > 99:
msg = '{:<7}h'.format(hours)
elif 0 < hours < 100:
msg = '{}h{}:{}'.format(hours, minutes, seconds)
else:
msg = '{}:{}'.format(minutes, seconds)
msg = self.layout_stat['time'].format(msg)
if hours > 0:
ret = self.curse_add_line(msg, decoration='CPU_TIME', optional=True)
else:
ret = self.curse_add_line(msg, optional=True)
return ret
def _get_process_curses_thread(self, p, selected, args):
"""Return process thread curses"""
if 'num_threads' in p:
num_threads = p['num_threads']
if num_threads is None:
num_threads = '?'
msg = self.layout_stat['thread'].format(num_threads)
ret = self.curse_add_line(msg)
else:
msg = self.layout_header['thread'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_nice(self, p, selected, args):
"""Return process nice curses"""
if 'nice' in p:
nice = p['nice']
if nice is None:
nice = '?'
msg = self.layout_stat['nice'].format(nice)
ret = self.curse_add_line(msg, decoration=self.get_nice_alert(nice))
else:
msg = self.layout_header['nice'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_status(self, p, selected, args):
"""Return process status curses"""
if 'status' in p:
status = p['status']
msg = self.layout_stat['status'].format(status)
if status == 'R':
ret = self.curse_add_line(msg, decoration='STATUS')
else:
ret = self.curse_add_line(msg)
else:
msg = self.layout_header['status'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_io(self, p, selected, args, rorw='ior'):
"""Return process IO Read or Write curses"""
if 'io_counters' in p and p['io_counters'][4] == 1 and p['time_since_update'] != 0:
# Display rate if stats is available and io_tag ([4]) == 1
# IO
io = int(
(p['io_counters'][0 if rorw == 'ior' else 1] - p['io_counters'][2 if rorw == 'ior' else 3])
/ p['time_since_update']
)
if io == 0:
msg = self.layout_stat[rorw].format("0")
else:
msg = self.layout_stat[rorw].format(self.auto_unit(io, low_precision=True))
ret = self.curse_add_line(msg, optional=True, additional=True)
else:
msg = self.layout_header[rorw].format("?")
ret = self.curse_add_line(msg, optional=True, additional=True)
return ret
def _get_process_curses_io_read(self, p, selected, args):
"""Return process IO Read curses"""
return self._get_process_curses_io(p, selected, args, rorw='ior')
def _get_process_curses_io_write(self, p, selected, args):
"""Return process IO Write curses"""
return self._get_process_curses_io(p, selected, args, rorw='iow')
def get_process_curses_data(self, p, selected, args):
"""Get curses data to display for a process.
- p is the process to display
- selected is a tag=True if p is the selected process
"""
ret = [self.curse_new_line()]
# When a process is selected:
# * display a special character at the beginning of the line
# * underline the command name
ret.append(
self.curse_add_line(
unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED'
)
)
# CPU
ret.append(self._get_process_curses_cpu(p, selected, args))
# MEM
ret.append(self._get_process_curses_mem(p, selected, args))
ret.append(self._get_process_curses_vms(p, selected, args))
ret.append(self._get_process_curses_rss(p, selected, args))
# PID
if not self.args.programs:
# Display processes, so the PID should be displayed
msg = self.layout_stat['pid'].format(p['pid'], width=self.__max_pid_size())
else:
# Display programs, so the PID should not be displayed
# Instead displays the number of children
msg = self.layout_stat['pid'].format(
len(p['childrens']) if 'childrens' in p else '_', width=self.__max_pid_size()
)
ret.append(self.curse_add_line(msg))
# USER
ret.append(self._get_process_curses_username(p, selected, args))
# TIME+
ret.append(self._get_process_curses_time(p, selected, args))
# THREAD
ret.append(self._get_process_curses_thread(p, selected, args))
# NICE
ret.append(self._get_process_curses_nice(p, selected, args))
# STATUS
ret.append(self._get_process_curses_status(p, selected, args))
# IO read/write
ret.append(self._get_process_curses_io_read(p, selected, args))
ret.append(self._get_process_curses_io_write(p, selected, args))
# Command line
# If no command line for the process is available, fallback to the bare process name instead
bare_process_name = p['name']
cmdline = p.get('cmdline', '?')
try:
process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS'
if cmdline:
path, cmd, arguments = split_cmdline(bare_process_name, cmdline)
# Manage end of line in arguments (see #1692)
arguments = arguments.replace('\r\n', ' ')
arguments = arguments.replace('\n', ' ')
arguments = arguments.replace('\t', ' ')
if os.path.isdir(path) and not args.process_short_name:
msg = self.layout_stat['command'].format(path) + os.sep
ret.append(self.curse_add_line(msg, splittable=True))
ret.append(self.curse_add_line(cmd, decoration=process_decoration, splittable=True))
else:
msg = self.layout_stat['command'].format(cmd)
ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
if arguments:
msg = ' ' + self.layout_stat['command'].format(arguments)
ret.append(self.curse_add_line(msg, splittable=True))
else:
msg = self.layout_stat['name'].format(bare_process_name)
ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
except (TypeError, UnicodeEncodeError) as e:
# Avoid crash after running fine for several hours #1335
logger.debug("Can not decode command line '{}' ({})".format(cmdline, e))
ret.append(self.curse_add_line('', splittable=True))
return ret
def is_selected_process(self, args):
return (
args.is_standalone
and self.args.enable_process_extended
and args.cursor_position is not None
and glances_processes.extended_process is not None
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or args.disable_process:
return ret
# Compute the sort key
process_sort_key = glances_processes.sort_key
processes_list_sorted = self.__sort_stats(process_sort_key)
# Display extended stats for selected process
#############################################
if self.is_selected_process(args):
self.__msg_curse_extended_process(ret, glances_processes.extended_process)
# Display others processes list
###############################
# Header
self.__msg_curse_header(ret, process_sort_key, args)
# Process list
# Loop over processes (sorted by the sort key previously compute)
# This is a Glances bottleneck (see flame graph),
# get_process_curses_data should be optimzed
for position, process in enumerate(processes_list_sorted):
ret.extend(self.get_process_curses_data(process, position == args.cursor_position, args))
# A filter is set Display the stats summaries
if glances_processes.process_filter is not None:
if args.reset_minmax_tag:
args.reset_minmax_tag = not args.reset_minmax_tag
self.__mmm_reset()
self.__msg_curse_sum(ret, args=args)
self.__msg_curse_sum(ret, mmm='min', args=args)
self.__msg_curse_sum(ret, mmm='max', args=args)
# Return the message with decoration
return ret
def __msg_curse_extended_process(self, ret, p):
"""Get extended curses data for the selected process (see issue #2225)
The result depends of the process type (process or thread).
Input p is a dict with the following keys:
{'status': 'S',
'memory_info': pmem(rss=466890752, vms=3365347328, shared=68153344,
text=659456, lib=0, data=774647808, dirty=0),
'pid': 4980,
'io_counters': [165385216, 0, 165385216, 0, 1],
'num_threads': 20,
'nice': 0,
'memory_percent': 5.958135664449709,
'cpu_percent': 0.0,
'gids': pgids(real=1000, effective=1000, saved=1000),
'cpu_times': pcputimes(user=696.38, system=119.98, children_user=0.0, children_system=0.0, iowait=0.0),
'name': 'WebExtensions',
'key': 'pid',
'time_since_update': 2.1997854709625244,
'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'],
'username': 'nicolargo',
'cpu_min': 0.0,
'cpu_max': 7.0,
'cpu_mean': 3.2}
"""
if self.args.programs:
self.__msg_curse_extended_process_program(ret, p)
else:
self.__msg_curse_extended_process_thread(ret, p)
def __msg_curse_extended_process_program(self, ret, p):
# Title
msg = "Pinned program {} ('e' to unpin)".format(p['name'])
ret.append(self.curse_add_line(msg, "TITLE"))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
def __msg_curse_extended_process_thread(self, ret, p):
# Title
ret.append(self.curse_add_line("Pinned thread ", "TITLE"))
ret.append(self.curse_add_line(p['name'], "UNDERLINE"))
ret.append(self.curse_add_line(" ('e' to unpin)"))
# First line is CPU affinity
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' CPU Min/Max/Mean: '))
msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['cpu_min'], p['cpu_max'], p['cpu_mean'])
ret.append(self.curse_add_line(msg, decoration='INFO'))
if 'cpu_affinity' in p and p['cpu_affinity'] is not None:
ret.append(self.curse_add_line(' Affinity: '))
ret.append(self.curse_add_line(str(len(p['cpu_affinity'])), decoration='INFO'))
ret.append(self.curse_add_line(' cores', decoration='INFO'))
if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'):
msg = ' IO nice: '
k = 'Class is '
v = p['ionice'].ioclass
# Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
# Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
if WINDOWS:
if v == 0:
msg += k + 'Very Low'
elif v == 1:
msg += k + 'Low'
elif v == 2:
msg += 'No specific I/O priority'
else:
msg += k + str(v)
else:
if v == 0:
msg += 'No specific I/O priority'
elif v == 1:
msg += k + 'Real Time'
elif v == 2:
msg += k + 'Best Effort'
elif v == 3:
msg += k + 'IDLE'
else:
msg += k + str(v)
# value is a number which goes from 0 to 7.
# The higher the value, the lower the I/O priority of the process.
if hasattr(p['ionice'], 'value') and p['ionice'].value != 0:
msg += ' (value %s/7)' % str(p['ionice'].value)
ret.append(self.curse_add_line(msg, splittable=True))
# Second line is memory info
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' MEM Min/Max/Mean: '))
msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['memory_min'], p['memory_max'], p['memory_mean'])
ret.append(self.curse_add_line(msg, decoration='INFO'))
if 'memory_info' in p and p['memory_info'] is not None:
ret.append(self.curse_add_line(' Memory info: '))
for k in p['memory_info']._asdict():
ret.append(
self.curse_add_line(
self.auto_unit(p['memory_info']._asdict()[k], low_precision=False),
decoration='INFO',
splittable=True,
)
)
ret.append(self.curse_add_line(' ' + k + ' ', splittable=True))
if 'memory_swap' in p and p['memory_swap'] is not None:
ret.append(
self.curse_add_line(
self.auto_unit(p['memory_swap'], low_precision=False), decoration='INFO', splittable=True
)
)
ret.append(self.curse_add_line(' swap ', splittable=True))
# Third line is for open files/network sessions
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' Open: '))
for stat_prefix in ['num_threads', 'num_fds', 'num_handles', 'tcp', 'udp']:
if stat_prefix in p and p[stat_prefix] is not None:
ret.append(self.curse_add_line(str(p[stat_prefix]), decoration='INFO'))
ret.append(self.curse_add_line(' {} '.format(stat_prefix.replace('num_', ''))))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
def __msg_curse_header(self, ret, process_sort_key, args=None):
"""Build the header and add it to the ret dict."""
sort_style = 'SORT'
if args.disable_irix and 0 < self.nb_log_core < 10:
msg = self.layout_header['cpu'].format('CPU%/' + str(self.nb_log_core))
elif args.disable_irix and self.nb_log_core != 0:
msg = self.layout_header['cpu'].format('CPU%/C')
else:
msg = self.layout_header['cpu'].format('CPU%')
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_percent' else 'DEFAULT'))
msg = self.layout_header['mem'].format('MEM%')
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'memory_percent' else 'DEFAULT'))
msg = self.layout_header['virt'].format('VIRT')
ret.append(self.curse_add_line(msg, optional=True))
msg = self.layout_header['res'].format('RES')
ret.append(self.curse_add_line(msg, optional=True))
if not self.args.programs:
msg = self.layout_header['pid'].format('PID', width=self.__max_pid_size())
else:
msg = self.layout_header['pid'].format('NPROCS', width=self.__max_pid_size())
ret.append(self.curse_add_line(msg))
msg = self.layout_header['user'].format('USER')
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT'))
msg = self.layout_header['time'].format('TIME+')
ret.append(
self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_times' else 'DEFAULT', optional=True)
)
msg = self.layout_header['thread'].format('THR')
ret.append(self.curse_add_line(msg))
msg = self.layout_header['nice'].format('NI')
ret.append(self.curse_add_line(msg))
msg = self.layout_header['status'].format('S')
ret.append(self.curse_add_line(msg))
msg = self.layout_header['ior'].format('R/s')
ret.append(
self.curse_add_line(
msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
)
)
msg = self.layout_header['iow'].format('W/s')
ret.append(
self.curse_add_line(
msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
)
)
if args.is_standalone and not args.disable_cursor:
if self.args.programs:
shortkey = "('k' to kill)"
else:
shortkey = "('e' to pin | 'k' to kill)"
else:
shortkey = ""
msg = self.layout_header['command'].format("Programs" if self.args.programs else "Command", shortkey)
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT'))
def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None):
"""
Build the sum message (only when filter is on) and add it to the ret dict.
:param ret: list of string where the message is added
:param sep_char: define the line separation char
:param mmm: display min, max, mean or current (if mmm=None)
:param args: Glances args
"""
ret.append(self.curse_new_line())
if mmm is None:
ret.append(self.curse_add_line(sep_char * 69))
ret.append(self.curse_new_line())
# CPU percent sum
msg = self.layout_stat['cpu'].format(self.__sum_stats('cpu_percent', mmm=mmm))
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm)))
# MEM percent sum
msg = self.layout_stat['mem'].format(self.__sum_stats('memory_percent', mmm=mmm))
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm)))
# VIRT and RES memory sum
if (
'memory_info' in self.stats[0]
and self.stats[0]['memory_info'] is not None
and self.stats[0]['memory_info'] != ''
):
# VMS
msg = self.layout_stat['virt'].format(
self.auto_unit(self.__sum_stats('memory_info', indice=1, mmm=mmm), low_precision=False)
)
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True))
# RSS
msg = self.layout_stat['res'].format(
self.auto_unit(self.__sum_stats('memory_info', indice=0, mmm=mmm), low_precision=False)
)
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True))
else:
msg = self.layout_header['virt'].format('')
ret.append(self.curse_add_line(msg))
msg = self.layout_header['res'].format('')
ret.append(self.curse_add_line(msg))
# PID
msg = self.layout_header['pid'].format('', width=self.__max_pid_size())
ret.append(self.curse_add_line(msg))
# USER
msg = self.layout_header['user'].format('')
ret.append(self.curse_add_line(msg))
# TIME+
msg = self.layout_header['time'].format('')
ret.append(self.curse_add_line(msg, optional=True))
# THREAD
msg = self.layout_header['thread'].format('')
ret.append(self.curse_add_line(msg))
# NICE
msg = self.layout_header['nice'].format('')
ret.append(self.curse_add_line(msg))
# STATUS
msg = self.layout_header['status'].format('')
ret.append(self.curse_add_line(msg))
# IO read/write
if 'io_counters' in self.stats[0] and mmm is None:
# IO read
io_rs = int(
(self.__sum_stats('io_counters', 0) - self.__sum_stats('io_counters', indice=2, mmm=mmm))
/ self.stats[0]['time_since_update']
)
if io_rs == 0:
msg = self.layout_stat['ior'].format('0')
else:
msg = self.layout_stat['ior'].format(self.auto_unit(io_rs, low_precision=True))
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True, additional=True))
# IO write
io_ws = int(
(self.__sum_stats('io_counters', 1) - self.__sum_stats('io_counters', indice=3, mmm=mmm))
/ self.stats[0]['time_since_update']
)
if io_ws == 0:
msg = self.layout_stat['iow'].format('0')
else:
msg = self.layout_stat['iow'].format(self.auto_unit(io_ws, low_precision=True))
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True, additional=True))
else:
msg = self.layout_header['ior'].format('')
ret.append(self.curse_add_line(msg, optional=True, additional=True))
msg = self.layout_header['iow'].format('')
ret.append(self.curse_add_line(msg, optional=True, additional=True))
if mmm is None:
msg = ' < {}'.format('current')
ret.append(self.curse_add_line(msg, optional=True))
else:
msg = ' < {}'.format(mmm)
ret.append(self.curse_add_line(msg, optional=True))
msg = ' (\'M\' to reset)'
ret.append(self.curse_add_line(msg, optional=True))
def __mmm_deco(self, mmm):
"""Return the decoration string for the current mmm status."""
if mmm is not None:
return 'DEFAULT'
else:
return 'FILTER'
def __mmm_reset(self):
"""Reset the MMM stats."""
self.mmm_min = {}
self.mmm_max = {}
def __sum_stats(self, key, indice=None, mmm=None):
"""Return the sum of the stats value for the given key.
:param indice: If indice is set, get the p[key][indice]
:param mmm: display min, max, mean or current (if mmm=None)
"""
# Compute stats summary
ret = 0
for p in self.stats:
if key not in p:
# Correct issue #1188
continue
if p[key] is None:
# Correct https://github.com/nicolargo/glances/issues/1105#issuecomment-363553788
continue
if indice is None:
ret += p[key]
else:
ret += p[key][indice]
# Manage Min/Max/Mean
mmm_key = self.__mmm_key(key, indice)
if mmm == 'min':
try:
if self.mmm_min[mmm_key] > ret:
self.mmm_min[mmm_key] = ret
except AttributeError:
self.mmm_min = {}
return 0
except KeyError:
self.mmm_min[mmm_key] = ret
ret = self.mmm_min[mmm_key]
elif mmm == 'max':
try:
if self.mmm_max[mmm_key] < ret:
self.mmm_max[mmm_key] = ret
except AttributeError:
self.mmm_max = {}
return 0
except KeyError:
self.mmm_max[mmm_key] = ret
ret = self.mmm_max[mmm_key]
return ret
def __mmm_key(self, key, indice):
ret = key
if indice is not None:
ret += str(indice)
return ret
def __sort_stats(self, sorted_by=None):
"""Return the stats (dict) sorted by (sorted_by)."""
return sort_stats(self.stats, sorted_by, reverse=glances_processes.sort_reverse)
def __max_pid_size(self):
"""Return the maximum PID size in number of char."""
if self.pid_max is not None:
return len(str(self.pid_max))
else:
# By default return 5 (corresponding to 99999 PID number)
return 5

View File

@ -1,833 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Process list plugin."""
import os
import copy
from glances.logger import logger
from glances.globals import WINDOWS, key_exist_value_not_none_not_v
from glances.processes import glances_processes, sort_stats
from glances.outputs.glances_unicode import unicode_message
from glances.plugins.core.model import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
def seconds_to_hms(input_seconds):
"""Convert seconds to human-readable time."""
minutes, seconds = divmod(input_seconds, 60)
hours, minutes = divmod(minutes, 60)
hours = int(hours)
minutes = int(minutes)
seconds = str(int(seconds)).zfill(2)
return hours, minutes, seconds
def split_cmdline(bare_process_name, cmdline):
"""Return path, cmd and arguments for a process cmdline based on bare_process_name.
If first argument of cmdline starts with the bare_process_name then
cmdline will just be considered cmd and path will be empty (see https://github.com/nicolargo/glances/issues/1795)
:param bare_process_name: Name of the process from psutil
:param cmdline: cmdline from psutil
:return: Tuple with three strings, which are path, cmd and arguments of the process
"""
if cmdline[0].startswith(bare_process_name):
path, cmd = "", cmdline[0]
else:
path, cmd = os.path.split(cmdline[0])
arguments = ' '.join(cmdline[1:])
return path, cmd, arguments
class PluginModel(GlancesPluginModel):
"""Glances' processes plugin.
stats is a list
"""
# Define the header layout of the processes list columns
layout_header = {
'cpu': '{:<6} ',
'mem': '{:<5} ',
'virt': '{:<5} ',
'res': '{:<5} ',
'pid': '{:>{width}} ',
'user': '{:<10} ',
'time': '{:>8} ',
'thread': '{:<3} ',
'nice': '{:>3} ',
'status': '{:>1} ',
'ior': '{:>4} ',
'iow': '{:<4} ',
'command': '{} {}',
}
# Define the stat layout of the processes list columns
layout_stat = {
'cpu': '{:<6.1f}',
'cpu_no_digit': '{:<6.0f}',
'mem': '{:<5.1f} ',
'virt': '{:<5} ',
'res': '{:<5} ',
'pid': '{:>{width}} ',
'user': '{:<10} ',
'time': '{:>8} ',
'thread': '{:<3} ',
'nice': '{:>3} ',
'status': '{:>1} ',
'ior': '{:>4} ',
'iow': '{:<4} ',
'command': '{}',
'name': '[{}]',
}
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Trying to display proc time
self.tag_proc_time = True
# Call CorePluginModel to get the core number (needed when not in IRIX mode / Solaris mode)
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception:
self.nb_log_core = 0
# Get the max values (dict)
self.max_values = copy.deepcopy(glances_processes.max_values())
# Get the maximum PID number
# Use to optimize space (see https://github.com/nicolargo/glances/issues/959)
self.pid_max = glances_processes.pid_max
# Set the default sort key if it is defined in the configuration file
if config is not None:
if 'processlist' in config.as_dict() and 'sort_key' in config.as_dict()['processlist']:
logger.debug(
'Configuration overwrites processes sort key by {}'.format(
config.as_dict()['processlist']['sort_key']
)
)
glances_processes.set_sort_key(config.as_dict()['processlist']['sort_key'], False)
# The default sort key could also be overwrite by command line (see #1903)
if args.sort_processes_key is not None:
glances_processes.set_sort_key(args.sort_processes_key, False)
# Note: 'glances_processes' is already init in the processes.py script
def get_key(self):
"""Return the key of the list."""
return 'pid'
def update(self):
"""Update processes stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Note: Update is done in the processcount plugin
# Just return the processes list
if self.args.programs:
stats = glances_processes.getlist(as_programs=True)
else:
stats = glances_processes.getlist()
elif self.input_method == 'snmp':
# No SNMP grab for processes
pass
# Update the stats
self.stats = stats
# Get the max values (dict)
# Use Deep copy to avoid change between update and display
self.max_values = copy.deepcopy(glances_processes.max_values())
return self.stats
def get_nice_alert(self, value):
"""Return the alert relative to the Nice configuration list"""
value = str(value)
try:
if value in self.get_limit('nice_critical'):
return 'CRITICAL'
except KeyError:
pass
try:
if value in self.get_limit('nice_warning'):
return 'WARNING'
except KeyError:
pass
try:
if value in self.get_limit('nice_careful'):
return 'CAREFUL'
except KeyError:
pass
return 'DEFAULT'
def _get_process_curses_cpu(self, p, selected, args):
"""Return process CPU curses"""
if key_exist_value_not_none_not_v('cpu_percent', p, ''):
cpu_layout = self.layout_stat['cpu'] if p['cpu_percent'] < 100 else self.layout_stat['cpu_no_digit']
if args.disable_irix and self.nb_log_core != 0:
msg = cpu_layout.format(p['cpu_percent'] / float(self.nb_log_core))
else:
msg = cpu_layout.format(p['cpu_percent'])
alert = self.get_alert(
p['cpu_percent'],
highlight_zero=False,
is_max=(p['cpu_percent'] == self.max_values['cpu_percent']),
header="cpu",
)
ret = self.curse_add_line(msg, alert)
else:
msg = self.layout_header['cpu'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_mem(self, p, selected, args):
"""Return process MEM curses"""
if key_exist_value_not_none_not_v('memory_percent', p, ''):
msg = self.layout_stat['mem'].format(p['memory_percent'])
alert = self.get_alert(
p['memory_percent'],
highlight_zero=False,
is_max=(p['memory_percent'] == self.max_values['memory_percent']),
header="mem",
)
ret = self.curse_add_line(msg, alert)
else:
msg = self.layout_header['mem'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_vms(self, p, selected, args):
"""Return process VMS curses"""
if key_exist_value_not_none_not_v('memory_info', p, '', 1):
msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info'][1], low_precision=False))
ret = self.curse_add_line(msg, optional=True)
else:
msg = self.layout_header['virt'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_rss(self, p, selected, args):
"""Return process RSS curses"""
if key_exist_value_not_none_not_v('memory_info', p, '', 0):
msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info'][0], low_precision=False))
ret = self.curse_add_line(msg, optional=True)
else:
msg = self.layout_header['res'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_username(self, p, selected, args):
"""Return process username curses"""
if 'username' in p:
# docker internal users are displayed as ints only, therefore str()
# Correct issue #886 on Windows OS
msg = self.layout_stat['user'].format(str(p['username'])[:9])
ret = self.curse_add_line(msg)
else:
msg = self.layout_header['user'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_time(self, p, selected, args):
"""Return process time curses"""
try:
# Sum user and system time
user_system_time = p['cpu_times'][0] + p['cpu_times'][1]
except (OverflowError, TypeError):
# Catch OverflowError on some Amazon EC2 server
# See https://github.com/nicolargo/glances/issues/87
# Also catch TypeError on macOS
# See: https://github.com/nicolargo/glances/issues/622
# logger.debug("Cannot get TIME+ ({})".format(e))
msg = self.layout_header['time'].format('?')
ret = self.curse_add_line(msg, optional=True)
else:
hours, minutes, seconds = seconds_to_hms(user_system_time)
if hours > 99:
msg = '{:<7}h'.format(hours)
elif 0 < hours < 100:
msg = '{}h{}:{}'.format(hours, minutes, seconds)
else:
msg = '{}:{}'.format(minutes, seconds)
msg = self.layout_stat['time'].format(msg)
if hours > 0:
ret = self.curse_add_line(msg, decoration='CPU_TIME', optional=True)
else:
ret = self.curse_add_line(msg, optional=True)
return ret
def _get_process_curses_thread(self, p, selected, args):
"""Return process thread curses"""
if 'num_threads' in p:
num_threads = p['num_threads']
if num_threads is None:
num_threads = '?'
msg = self.layout_stat['thread'].format(num_threads)
ret = self.curse_add_line(msg)
else:
msg = self.layout_header['thread'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_nice(self, p, selected, args):
"""Return process nice curses"""
if 'nice' in p:
nice = p['nice']
if nice is None:
nice = '?'
msg = self.layout_stat['nice'].format(nice)
ret = self.curse_add_line(msg, decoration=self.get_nice_alert(nice))
else:
msg = self.layout_header['nice'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_status(self, p, selected, args):
"""Return process status curses"""
if 'status' in p:
status = p['status']
msg = self.layout_stat['status'].format(status)
if status == 'R':
ret = self.curse_add_line(msg, decoration='STATUS')
else:
ret = self.curse_add_line(msg)
else:
msg = self.layout_header['status'].format('?')
ret = self.curse_add_line(msg)
return ret
def _get_process_curses_io(self, p, selected, args, rorw='ior'):
"""Return process IO Read or Write curses"""
if 'io_counters' in p and p['io_counters'][4] == 1 and p['time_since_update'] != 0:
# Display rate if stats is available and io_tag ([4]) == 1
# IO
io = int(
(p['io_counters'][0 if rorw == 'ior' else 1] - p['io_counters'][2 if rorw == 'ior' else 3])
/ p['time_since_update']
)
if io == 0:
msg = self.layout_stat[rorw].format("0")
else:
msg = self.layout_stat[rorw].format(self.auto_unit(io, low_precision=True))
ret = self.curse_add_line(msg, optional=True, additional=True)
else:
msg = self.layout_header[rorw].format("?")
ret = self.curse_add_line(msg, optional=True, additional=True)
return ret
def _get_process_curses_io_read(self, p, selected, args):
"""Return process IO Read curses"""
return self._get_process_curses_io(p, selected, args, rorw='ior')
def _get_process_curses_io_write(self, p, selected, args):
"""Return process IO Write curses"""
return self._get_process_curses_io(p, selected, args, rorw='iow')
def get_process_curses_data(self, p, selected, args):
"""Get curses data to display for a process.
- p is the process to display
- selected is a tag=True if p is the selected process
"""
ret = [self.curse_new_line()]
# When a process is selected:
# * display a special character at the beginning of the line
# * underline the command name
ret.append(
self.curse_add_line(
unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED'
)
)
# CPU
ret.append(self._get_process_curses_cpu(p, selected, args))
# MEM
ret.append(self._get_process_curses_mem(p, selected, args))
ret.append(self._get_process_curses_vms(p, selected, args))
ret.append(self._get_process_curses_rss(p, selected, args))
# PID
if not self.args.programs:
# Display processes, so the PID should be displayed
msg = self.layout_stat['pid'].format(p['pid'], width=self.__max_pid_size())
else:
# Display programs, so the PID should not be displayed
# Instead displays the number of children
msg = self.layout_stat['pid'].format(
len(p['childrens']) if 'childrens' in p else '_', width=self.__max_pid_size()
)
ret.append(self.curse_add_line(msg))
# USER
ret.append(self._get_process_curses_username(p, selected, args))
# TIME+
ret.append(self._get_process_curses_time(p, selected, args))
# THREAD
ret.append(self._get_process_curses_thread(p, selected, args))
# NICE
ret.append(self._get_process_curses_nice(p, selected, args))
# STATUS
ret.append(self._get_process_curses_status(p, selected, args))
# IO read/write
ret.append(self._get_process_curses_io_read(p, selected, args))
ret.append(self._get_process_curses_io_write(p, selected, args))
# Command line
# If no command line for the process is available, fallback to the bare process name instead
bare_process_name = p['name']
cmdline = p.get('cmdline', '?')
try:
process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS'
if cmdline:
path, cmd, arguments = split_cmdline(bare_process_name, cmdline)
# Manage end of line in arguments (see #1692)
arguments = arguments.replace('\r\n', ' ')
arguments = arguments.replace('\n', ' ')
arguments = arguments.replace('\t', ' ')
if os.path.isdir(path) and not args.process_short_name:
msg = self.layout_stat['command'].format(path) + os.sep
ret.append(self.curse_add_line(msg, splittable=True))
ret.append(self.curse_add_line(cmd, decoration=process_decoration, splittable=True))
else:
msg = self.layout_stat['command'].format(cmd)
ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
if arguments:
msg = ' ' + self.layout_stat['command'].format(arguments)
ret.append(self.curse_add_line(msg, splittable=True))
else:
msg = self.layout_stat['name'].format(bare_process_name)
ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
except (TypeError, UnicodeEncodeError) as e:
# Avoid crash after running fine for several hours #1335
logger.debug("Can not decode command line '{}' ({})".format(cmdline, e))
ret.append(self.curse_add_line('', splittable=True))
return ret
def is_selected_process(self, args):
return (
args.is_standalone
and self.args.enable_process_extended
and args.cursor_position is not None
and glances_processes.extended_process is not None
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or args.disable_process:
return ret
# Compute the sort key
process_sort_key = glances_processes.sort_key
processes_list_sorted = self.__sort_stats(process_sort_key)
# Display extended stats for selected process
#############################################
if self.is_selected_process(args):
self.__msg_curse_extended_process(ret, glances_processes.extended_process)
# Display others processes list
###############################
# Header
self.__msg_curse_header(ret, process_sort_key, args)
# Process list
# Loop over processes (sorted by the sort key previously compute)
# This is a Glances bottleneck (see flame graph),
# get_process_curses_data should be optimzed
for position, process in enumerate(processes_list_sorted):
ret.extend(self.get_process_curses_data(process, position == args.cursor_position, args))
# A filter is set Display the stats summaries
if glances_processes.process_filter is not None:
if args.reset_minmax_tag:
args.reset_minmax_tag = not args.reset_minmax_tag
self.__mmm_reset()
self.__msg_curse_sum(ret, args=args)
self.__msg_curse_sum(ret, mmm='min', args=args)
self.__msg_curse_sum(ret, mmm='max', args=args)
# Return the message with decoration
return ret
def __msg_curse_extended_process(self, ret, p):
"""Get extended curses data for the selected process (see issue #2225)
The result depends of the process type (process or thread).
Input p is a dict with the following keys:
{'status': 'S',
'memory_info': pmem(rss=466890752, vms=3365347328, shared=68153344,
text=659456, lib=0, data=774647808, dirty=0),
'pid': 4980,
'io_counters': [165385216, 0, 165385216, 0, 1],
'num_threads': 20,
'nice': 0,
'memory_percent': 5.958135664449709,
'cpu_percent': 0.0,
'gids': pgids(real=1000, effective=1000, saved=1000),
'cpu_times': pcputimes(user=696.38, system=119.98, children_user=0.0, children_system=0.0, iowait=0.0),
'name': 'WebExtensions',
'key': 'pid',
'time_since_update': 2.1997854709625244,
'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'],
'username': 'nicolargo',
'cpu_min': 0.0,
'cpu_max': 7.0,
'cpu_mean': 3.2}
"""
if self.args.programs:
self.__msg_curse_extended_process_program(ret, p)
else:
self.__msg_curse_extended_process_thread(ret, p)
def __msg_curse_extended_process_program(self, ret, p):
# Title
msg = "Pinned program {} ('e' to unpin)".format(p['name'])
ret.append(self.curse_add_line(msg, "TITLE"))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
def __msg_curse_extended_process_thread(self, ret, p):
# Title
ret.append(self.curse_add_line("Pinned thread ", "TITLE"))
ret.append(self.curse_add_line(p['name'], "UNDERLINE"))
ret.append(self.curse_add_line(" ('e' to unpin)"))
# First line is CPU affinity
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' CPU Min/Max/Mean: '))
msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['cpu_min'], p['cpu_max'], p['cpu_mean'])
ret.append(self.curse_add_line(msg, decoration='INFO'))
if 'cpu_affinity' in p and p['cpu_affinity'] is not None:
ret.append(self.curse_add_line(' Affinity: '))
ret.append(self.curse_add_line(str(len(p['cpu_affinity'])), decoration='INFO'))
ret.append(self.curse_add_line(' cores', decoration='INFO'))
if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'):
msg = ' IO nice: '
k = 'Class is '
v = p['ionice'].ioclass
# Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
# Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
if WINDOWS:
if v == 0:
msg += k + 'Very Low'
elif v == 1:
msg += k + 'Low'
elif v == 2:
msg += 'No specific I/O priority'
else:
msg += k + str(v)
else:
if v == 0:
msg += 'No specific I/O priority'
elif v == 1:
msg += k + 'Real Time'
elif v == 2:
msg += k + 'Best Effort'
elif v == 3:
msg += k + 'IDLE'
else:
msg += k + str(v)
# value is a number which goes from 0 to 7.
# The higher the value, the lower the I/O priority of the process.
if hasattr(p['ionice'], 'value') and p['ionice'].value != 0:
msg += ' (value %s/7)' % str(p['ionice'].value)
ret.append(self.curse_add_line(msg, splittable=True))
# Second line is memory info
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' MEM Min/Max/Mean: '))
msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['memory_min'], p['memory_max'], p['memory_mean'])
ret.append(self.curse_add_line(msg, decoration='INFO'))
if 'memory_info' in p and p['memory_info'] is not None:
ret.append(self.curse_add_line(' Memory info: '))
for k in p['memory_info']._asdict():
ret.append(
self.curse_add_line(
self.auto_unit(p['memory_info']._asdict()[k], low_precision=False),
decoration='INFO',
splittable=True,
)
)
ret.append(self.curse_add_line(' ' + k + ' ', splittable=True))
if 'memory_swap' in p and p['memory_swap'] is not None:
ret.append(
self.curse_add_line(
self.auto_unit(p['memory_swap'], low_precision=False), decoration='INFO', splittable=True
)
)
ret.append(self.curse_add_line(' swap ', splittable=True))
# Third line is for open files/network sessions
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' Open: '))
for stat_prefix in ['num_threads', 'num_fds', 'num_handles', 'tcp', 'udp']:
if stat_prefix in p and p[stat_prefix] is not None:
ret.append(self.curse_add_line(str(p[stat_prefix]), decoration='INFO'))
ret.append(self.curse_add_line(' {} '.format(stat_prefix.replace('num_', ''))))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
def __msg_curse_header(self, ret, process_sort_key, args=None):
"""Build the header and add it to the ret dict."""
sort_style = 'SORT'
if args.disable_irix and 0 < self.nb_log_core < 10:
msg = self.layout_header['cpu'].format('CPU%/' + str(self.nb_log_core))
elif args.disable_irix and self.nb_log_core != 0:
msg = self.layout_header['cpu'].format('CPU%/C')
else:
msg = self.layout_header['cpu'].format('CPU%')
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_percent' else 'DEFAULT'))
msg = self.layout_header['mem'].format('MEM%')
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'memory_percent' else 'DEFAULT'))
msg = self.layout_header['virt'].format('VIRT')
ret.append(self.curse_add_line(msg, optional=True))
msg = self.layout_header['res'].format('RES')
ret.append(self.curse_add_line(msg, optional=True))
if not self.args.programs:
msg = self.layout_header['pid'].format('PID', width=self.__max_pid_size())
else:
msg = self.layout_header['pid'].format('NPROCS', width=self.__max_pid_size())
ret.append(self.curse_add_line(msg))
msg = self.layout_header['user'].format('USER')
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT'))
msg = self.layout_header['time'].format('TIME+')
ret.append(
self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_times' else 'DEFAULT', optional=True)
)
msg = self.layout_header['thread'].format('THR')
ret.append(self.curse_add_line(msg))
msg = self.layout_header['nice'].format('NI')
ret.append(self.curse_add_line(msg))
msg = self.layout_header['status'].format('S')
ret.append(self.curse_add_line(msg))
msg = self.layout_header['ior'].format('R/s')
ret.append(
self.curse_add_line(
msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
)
)
msg = self.layout_header['iow'].format('W/s')
ret.append(
self.curse_add_line(
msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
)
)
if args.is_standalone and not args.disable_cursor:
if self.args.programs:
shortkey = "('k' to kill)"
else:
shortkey = "('e' to pin | 'k' to kill)"
else:
shortkey = ""
msg = self.layout_header['command'].format("Programs" if self.args.programs else "Command", shortkey)
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT'))
def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None):
"""
Build the sum message (only when filter is on) and add it to the ret dict.
:param ret: list of string where the message is added
:param sep_char: define the line separation char
:param mmm: display min, max, mean or current (if mmm=None)
:param args: Glances args
"""
ret.append(self.curse_new_line())
if mmm is None:
ret.append(self.curse_add_line(sep_char * 69))
ret.append(self.curse_new_line())
# CPU percent sum
msg = self.layout_stat['cpu'].format(self.__sum_stats('cpu_percent', mmm=mmm))
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm)))
# MEM percent sum
msg = self.layout_stat['mem'].format(self.__sum_stats('memory_percent', mmm=mmm))
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm)))
# VIRT and RES memory sum
if (
'memory_info' in self.stats[0]
and self.stats[0]['memory_info'] is not None
and self.stats[0]['memory_info'] != ''
):
# VMS
msg = self.layout_stat['virt'].format(
self.auto_unit(self.__sum_stats('memory_info', indice=1, mmm=mmm), low_precision=False)
)
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True))
# RSS
msg = self.layout_stat['res'].format(
self.auto_unit(self.__sum_stats('memory_info', indice=0, mmm=mmm), low_precision=False)
)
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True))
else:
msg = self.layout_header['virt'].format('')
ret.append(self.curse_add_line(msg))
msg = self.layout_header['res'].format('')
ret.append(self.curse_add_line(msg))
# PID
msg = self.layout_header['pid'].format('', width=self.__max_pid_size())
ret.append(self.curse_add_line(msg))
# USER
msg = self.layout_header['user'].format('')
ret.append(self.curse_add_line(msg))
# TIME+
msg = self.layout_header['time'].format('')
ret.append(self.curse_add_line(msg, optional=True))
# THREAD
msg = self.layout_header['thread'].format('')
ret.append(self.curse_add_line(msg))
# NICE
msg = self.layout_header['nice'].format('')
ret.append(self.curse_add_line(msg))
# STATUS
msg = self.layout_header['status'].format('')
ret.append(self.curse_add_line(msg))
# IO read/write
if 'io_counters' in self.stats[0] and mmm is None:
# IO read
io_rs = int(
(self.__sum_stats('io_counters', 0) - self.__sum_stats('io_counters', indice=2, mmm=mmm))
/ self.stats[0]['time_since_update']
)
if io_rs == 0:
msg = self.layout_stat['ior'].format('0')
else:
msg = self.layout_stat['ior'].format(self.auto_unit(io_rs, low_precision=True))
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True, additional=True))
# IO write
io_ws = int(
(self.__sum_stats('io_counters', 1) - self.__sum_stats('io_counters', indice=3, mmm=mmm))
/ self.stats[0]['time_since_update']
)
if io_ws == 0:
msg = self.layout_stat['iow'].format('0')
else:
msg = self.layout_stat['iow'].format(self.auto_unit(io_ws, low_precision=True))
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True, additional=True))
else:
msg = self.layout_header['ior'].format('')
ret.append(self.curse_add_line(msg, optional=True, additional=True))
msg = self.layout_header['iow'].format('')
ret.append(self.curse_add_line(msg, optional=True, additional=True))
if mmm is None:
msg = ' < {}'.format('current')
ret.append(self.curse_add_line(msg, optional=True))
else:
msg = ' < {}'.format(mmm)
ret.append(self.curse_add_line(msg, optional=True))
msg = ' (\'M\' to reset)'
ret.append(self.curse_add_line(msg, optional=True))
def __mmm_deco(self, mmm):
"""Return the decoration string for the current mmm status."""
if mmm is not None:
return 'DEFAULT'
else:
return 'FILTER'
def __mmm_reset(self):
"""Reset the MMM stats."""
self.mmm_min = {}
self.mmm_max = {}
def __sum_stats(self, key, indice=None, mmm=None):
"""Return the sum of the stats value for the given key.
:param indice: If indice is set, get the p[key][indice]
:param mmm: display min, max, mean or current (if mmm=None)
"""
# Compute stats summary
ret = 0
for p in self.stats:
if key not in p:
# Correct issue #1188
continue
if p[key] is None:
# Correct https://github.com/nicolargo/glances/issues/1105#issuecomment-363553788
continue
if indice is None:
ret += p[key]
else:
ret += p[key][indice]
# Manage Min/Max/Mean
mmm_key = self.__mmm_key(key, indice)
if mmm == 'min':
try:
if self.mmm_min[mmm_key] > ret:
self.mmm_min[mmm_key] = ret
except AttributeError:
self.mmm_min = {}
return 0
except KeyError:
self.mmm_min[mmm_key] = ret
ret = self.mmm_min[mmm_key]
elif mmm == 'max':
try:
if self.mmm_max[mmm_key] < ret:
self.mmm_max[mmm_key] = ret
except AttributeError:
self.mmm_max = {}
return 0
except KeyError:
self.mmm_max[mmm_key] = ret
ret = self.mmm_max[mmm_key]
return ret
def __mmm_key(self, key, indice):
ret = key
if indice is not None:
ret += str(indice)
return ret
def __sort_stats(self, sorted_by=None):
"""Return the stats (dict) sorted by (sorted_by)."""
return sort_stats(self.stats, sorted_by, reverse=glances_processes.sort_reverse)
def __max_pid_size(self):
"""Return the maximum PID size in number of char."""
if self.pid_max is not None:
return len(str(self.pid_max))
else:
# By default return 5 (corresponding to 99999 PID number)
return 5

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""psutil plugin."""
from glances import psutil_version_info
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Get the psutil version for client/server purposes.
stats is a tuple
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
self.reset()
def reset(self):
"""Reset/init the stats."""
self.stats = None
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the stats."""
# Reset stats
self.reset()
# Return psutil version as a tuple
if self.input_method == 'local':
# psutil version only available in local
try:
self.stats = psutil_version_info
except NameError:
pass
else:
pass
return self.stats

View File

@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""psutil plugin."""
from glances import psutil_version_info
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Get the psutil version for client/server purposes.
stats is a tuple
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
self.reset()
def reset(self):
"""Reset/init the stats."""
self.stats = None
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the stats."""
# Reset stats
self.reset()
# Return psutil version as a tuple
if self.input_method == 'local':
# psutil version only available in local
try:
self.stats = psutil_version_info
except NameError:
pass
else:
pass
return self.stats

View File

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Quicklook plugin."""
from glances.cpu_percent import cpu_percent
from glances.outputs.glances_bars import Bar
from glances.outputs.glances_sparklines import Sparkline
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [
{'name': 'cpu', 'description': 'CPU percent usage', 'y_unit': '%'},
{'name': 'percpu', 'description': 'PERCPU percent usage', 'y_unit': '%'},
{'name': 'mem', 'description': 'MEM percent usage', 'y_unit': '%'},
{'name': 'swap', 'description': 'SWAP percent usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances quicklook plugin.
'stats' is a dictionary.
"""
def __init__(self, args=None, config=None):
"""Init the quicklook plugin."""
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update quicklook stats using the input method."""
# Init new stats
stats = self.get_init_value()
# Grab quicklook stats: CPU, MEM and SWAP
if self.input_method == 'local':
# Get the latest CPU percent value
stats['cpu'] = cpu_percent.get()
stats['percpu'] = cpu_percent.get(percpu=True)
# Use the psutil lib for the memory (virtual and swap)
stats['mem'] = psutil.virtual_memory().percent
try:
stats['swap'] = psutil.swap_memory().percent
except RuntimeError:
# Correct issue in Illumos OS (see #1767)
stats['swap'] = None
# Get additional information
cpu_info = cpu_percent.get_info()
stats['cpu_name'] = cpu_info['cpu_name']
stats['cpu_hz_current'] = (
self._mhz_to_hz(cpu_info['cpu_hz_current']) if cpu_info['cpu_hz_current'] is not None else None
)
stats['cpu_hz'] = self._mhz_to_hz(cpu_info['cpu_hz']) if cpu_info['cpu_hz'] is not None else None
elif self.input_method == 'snmp':
# Not available
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert only
for key in ['cpu', 'mem', 'swap']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
def msg_curse(self, args=None, max_width=10):
"""Return the list to display in the UI."""
# Init the return message
ret = []
# Only process if stats exist...
if not self.stats or self.is_disabled():
return ret
# Define the data: Bar (default behavior) or Sparkline
sparkline_tag = False
if self.args.sparkline and self.history_enable() and not self.args.client:
data = Sparkline(max_width)
sparkline_tag = data.available
if not sparkline_tag:
# Fallback to bar if Sparkline module is not installed
data = Bar(max_width, percentage_char=self.get_conf_value('percentage_char', default=['|'])[0])
# Build the string message
if 'cpu_name' in self.stats and 'cpu_hz_current' in self.stats and 'cpu_hz' in self.stats:
msg_name = self.stats['cpu_name']
if self.stats['cpu_hz_current'] and self.stats['cpu_hz']:
msg_freq = ' - {:.2f}/{:.2f}GHz'.format(
self._hz_to_ghz(self.stats['cpu_hz_current']), self._hz_to_ghz(self.stats['cpu_hz'])
)
else:
msg_freq = ''
if len(msg_name + msg_freq) - 6 <= max_width:
ret.append(self.curse_add_line(msg_name))
ret.append(self.curse_add_line(msg_freq))
ret.append(self.curse_new_line())
for key in ['cpu', 'mem', 'swap']:
if key == 'cpu' and args.percpu:
if sparkline_tag:
raw_cpu = self.get_raw_history(item='percpu', nb=data.size)
for cpu_index, cpu in enumerate(self.stats['percpu']):
if sparkline_tag:
# Sparkline display an history
data.percents = [i[1][cpu_index]['total'] for i in raw_cpu]
# A simple padding in order to align metrics to the right
data.percents += [None] * (data.size - len(data.percents))
else:
# Bar only the last value
data.percent = cpu['total']
if cpu[cpu['key']] < 10:
msg = '{:3}{} '.format(key.upper(), cpu['cpu_number'])
else:
msg = '{:4} '.format(cpu['cpu_number'])
ret.extend(self._msg_create_line(msg, data, key))
ret.append(self.curse_new_line())
else:
if sparkline_tag:
# Sparkline display an history
data.percents = [i[1] for i in self.get_raw_history(item=key, nb=data.size)]
# A simple padding in order to align metrics to the right
data.percents += [None] * (data.size - len(data.percents))
else:
# Bar only the last value
data.percent = self.stats[key]
msg = '{:4} '.format(key.upper())
ret.extend(self._msg_create_line(msg, data, key))
ret.append(self.curse_new_line())
# Remove the last new line
ret.pop()
# Return the message with decoration
return ret
def _msg_create_line(self, msg, data, key):
"""Create a new line to the Quick view."""
return [
self.curse_add_line(msg),
self.curse_add_line(data.pre_char, decoration='BOLD'),
self.curse_add_line(data.get(), self.get_views(key=key, option='decoration')),
self.curse_add_line(data.post_char, decoration='BOLD'),
self.curse_add_line(' '),
]
def _hz_to_ghz(self, hz):
"""Convert Hz to Ghz."""
return hz / 1000000000.0
def _mhz_to_hz(self, hz):
"""Convert Mhz to Hz."""
return hz * 1000000.0

View File

@ -1,176 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Quicklook plugin."""
from glances.cpu_percent import cpu_percent
from glances.outputs.glances_bars import Bar
from glances.outputs.glances_sparklines import Sparkline
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [
{'name': 'cpu', 'description': 'CPU percent usage', 'y_unit': '%'},
{'name': 'percpu', 'description': 'PERCPU percent usage', 'y_unit': '%'},
{'name': 'mem', 'description': 'MEM percent usage', 'y_unit': '%'},
{'name': 'swap', 'description': 'SWAP percent usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances quicklook plugin.
'stats' is a dictionary.
"""
def __init__(self, args=None, config=None):
"""Init the quicklook plugin."""
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update quicklook stats using the input method."""
# Init new stats
stats = self.get_init_value()
# Grab quicklook stats: CPU, MEM and SWAP
if self.input_method == 'local':
# Get the latest CPU percent value
stats['cpu'] = cpu_percent.get()
stats['percpu'] = cpu_percent.get(percpu=True)
# Use the psutil lib for the memory (virtual and swap)
stats['mem'] = psutil.virtual_memory().percent
try:
stats['swap'] = psutil.swap_memory().percent
except RuntimeError:
# Correct issue in Illumos OS (see #1767)
stats['swap'] = None
# Get additional information
cpu_info = cpu_percent.get_info()
stats['cpu_name'] = cpu_info['cpu_name']
stats['cpu_hz_current'] = (
self._mhz_to_hz(cpu_info['cpu_hz_current']) if cpu_info['cpu_hz_current'] is not None else None
)
stats['cpu_hz'] = self._mhz_to_hz(cpu_info['cpu_hz']) if cpu_info['cpu_hz'] is not None else None
elif self.input_method == 'snmp':
# Not available
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert only
for key in ['cpu', 'mem', 'swap']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
def msg_curse(self, args=None, max_width=10):
"""Return the list to display in the UI."""
# Init the return message
ret = []
# Only process if stats exist...
if not self.stats or self.is_disabled():
return ret
# Define the data: Bar (default behavior) or Sparkline
sparkline_tag = False
if self.args.sparkline and self.history_enable() and not self.args.client:
data = Sparkline(max_width)
sparkline_tag = data.available
if not sparkline_tag:
# Fallback to bar if Sparkline module is not installed
data = Bar(max_width, percentage_char=self.get_conf_value('percentage_char', default=['|'])[0])
# Build the string message
if 'cpu_name' in self.stats and 'cpu_hz_current' in self.stats and 'cpu_hz' in self.stats:
msg_name = self.stats['cpu_name']
if self.stats['cpu_hz_current'] and self.stats['cpu_hz']:
msg_freq = ' - {:.2f}/{:.2f}GHz'.format(
self._hz_to_ghz(self.stats['cpu_hz_current']), self._hz_to_ghz(self.stats['cpu_hz'])
)
else:
msg_freq = ''
if len(msg_name + msg_freq) - 6 <= max_width:
ret.append(self.curse_add_line(msg_name))
ret.append(self.curse_add_line(msg_freq))
ret.append(self.curse_new_line())
for key in ['cpu', 'mem', 'swap']:
if key == 'cpu' and args.percpu:
if sparkline_tag:
raw_cpu = self.get_raw_history(item='percpu', nb=data.size)
for cpu_index, cpu in enumerate(self.stats['percpu']):
if sparkline_tag:
# Sparkline display an history
data.percents = [i[1][cpu_index]['total'] for i in raw_cpu]
# A simple padding in order to align metrics to the right
data.percents += [None] * (data.size - len(data.percents))
else:
# Bar only the last value
data.percent = cpu['total']
if cpu[cpu['key']] < 10:
msg = '{:3}{} '.format(key.upper(), cpu['cpu_number'])
else:
msg = '{:4} '.format(cpu['cpu_number'])
ret.extend(self._msg_create_line(msg, data, key))
ret.append(self.curse_new_line())
else:
if sparkline_tag:
# Sparkline display an history
data.percents = [i[1] for i in self.get_raw_history(item=key, nb=data.size)]
# A simple padding in order to align metrics to the right
data.percents += [None] * (data.size - len(data.percents))
else:
# Bar only the last value
data.percent = self.stats[key]
msg = '{:4} '.format(key.upper())
ret.extend(self._msg_create_line(msg, data, key))
ret.append(self.curse_new_line())
# Remove the last new line
ret.pop()
# Return the message with decoration
return ret
def _msg_create_line(self, msg, data, key):
"""Create a new line to the Quick view."""
return [
self.curse_add_line(msg),
self.curse_add_line(data.pre_char, decoration='BOLD'),
self.curse_add_line(data.get(), self.get_views(key=key, option='decoration')),
self.curse_add_line(data.post_char, decoration='BOLD'),
self.curse_add_line(' '),
]
def _hz_to_ghz(self, hz):
"""Convert Hz to Ghz."""
return hz / 1000000000.0
def _mhz_to_hz(self, hz):
"""Convert Mhz to Hz."""
return hz * 1000000.0

View File

@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""RAID plugin."""
from glances.globals import iterkeys
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# Import plugin specific dependency
try:
from pymdstat import MdStat
except ImportError as e:
import_error_tag = True
logger.warning("Missing Python Lib ({}), Raid plugin is disabled".format(e))
else:
import_error_tag = False
class PluginModel(GlancesPluginModel):
"""Glances RAID plugin.
stats is a dict (see pymdstat documentation)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update RAID stats using the input method."""
# Init new stats
stats = self.get_init_value()
if import_error_tag:
return self.stats
if self.input_method == 'local':
# Update stats using the PyMDstat lib (https://github.com/nicolargo/pymdstat)
try:
# Just for test
# mds = MdStat(path='/home/nicolargo/dev/pymdstat/tests/mdstat.10')
mds = MdStat()
stats = mds.get_stats()['arrays']
except Exception as e:
logger.debug("Can not grab RAID stats (%s)" % e)
return self.stats
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard way for the moment...
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Header
msg = '{:{width}}'.format('RAID disks', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
msg = '{:>7}'.format('Used')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Avail')
ret.append(self.curse_add_line(msg))
# Data
arrays = sorted(iterkeys(self.stats))
for array in arrays:
# New line
ret.append(self.curse_new_line())
# Display the current status
if not isinstance(self.stats[array], dict):
continue
status = self.raid_alert(
self.stats[array]['status'],
self.stats[array]['used'],
self.stats[array]['available'],
self.stats[array]['type'],
)
# Data: RAID type name | disk used | disk available
array_type = self.stats[array]['type'].upper() if self.stats[array]['type'] is not None else 'UNKNOWN'
# Build the full name = array type + array name
full_name = '{} {}'.format(array_type, array)
msg = '{:{width}}'.format(full_name, width=name_max_width)
ret.append(self.curse_add_line(msg))
if self.stats[array]['type'] == 'raid0' and self.stats[array]['status'] == 'active':
msg = '{:>7}'.format(len(self.stats[array]['components']))
ret.append(self.curse_add_line(msg, status))
msg = '{:>7}'.format('-')
ret.append(self.curse_add_line(msg, status))
elif self.stats[array]['status'] == 'active':
msg = '{:>7}'.format(self.stats[array]['used'])
ret.append(self.curse_add_line(msg, status))
msg = '{:>7}'.format(self.stats[array]['available'])
ret.append(self.curse_add_line(msg, status))
elif self.stats[array]['status'] == 'inactive':
ret.append(self.curse_new_line())
msg = '└─ Status {}'.format(self.stats[array]['status'])
ret.append(self.curse_add_line(msg, status))
components = sorted(iterkeys(self.stats[array]['components']))
for i, component in enumerate(components):
if i == len(components) - 1:
tree_char = '└─'
else:
tree_char = '├─'
ret.append(self.curse_new_line())
msg = ' {} disk {}: '.format(tree_char, self.stats[array]['components'][component])
ret.append(self.curse_add_line(msg))
msg = '{}'.format(component)
ret.append(self.curse_add_line(msg))
if self.stats[array]['type'] != 'raid0' and (self.stats[array]['used'] < self.stats[array]['available']):
# Display current array configuration
ret.append(self.curse_new_line())
msg = '└─ Degraded mode'
ret.append(self.curse_add_line(msg, status))
if len(self.stats[array]['config']) < 17:
ret.append(self.curse_new_line())
msg = ' └─ {}'.format(self.stats[array]['config'].replace('_', 'A'))
ret.append(self.curse_add_line(msg))
return ret
def raid_alert(self, status, used, available, type):
"""RAID alert messages.
[available/used] means that ideally the array may have _available_
devices however, _used_ devices are in use.
Obviously when used >= available then things are good.
"""
if type == 'raid0':
return 'OK'
if status == 'inactive':
return 'CRITICAL'
if used is None or available is None:
return 'DEFAULT'
elif used < available:
return 'WARNING'
return 'OK'

View File

@ -1,161 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""RAID plugin."""
from glances.globals import iterkeys
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# Import plugin specific dependency
try:
from pymdstat import MdStat
except ImportError as e:
import_error_tag = True
logger.warning("Missing Python Lib ({}), Raid plugin is disabled".format(e))
else:
import_error_tag = False
class PluginModel(GlancesPluginModel):
"""Glances RAID plugin.
stats is a dict (see pymdstat documentation)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update RAID stats using the input method."""
# Init new stats
stats = self.get_init_value()
if import_error_tag:
return self.stats
if self.input_method == 'local':
# Update stats using the PyMDstat lib (https://github.com/nicolargo/pymdstat)
try:
# Just for test
# mds = MdStat(path='/home/nicolargo/dev/pymdstat/tests/mdstat.10')
mds = MdStat()
stats = mds.get_stats()['arrays']
except Exception as e:
logger.debug("Can not grab RAID stats (%s)" % e)
return self.stats
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard way for the moment...
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Header
msg = '{:{width}}'.format('RAID disks', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
msg = '{:>7}'.format('Used')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Avail')
ret.append(self.curse_add_line(msg))
# Data
arrays = sorted(iterkeys(self.stats))
for array in arrays:
# New line
ret.append(self.curse_new_line())
# Display the current status
if not isinstance(self.stats[array], dict):
continue
status = self.raid_alert(
self.stats[array]['status'],
self.stats[array]['used'],
self.stats[array]['available'],
self.stats[array]['type'],
)
# Data: RAID type name | disk used | disk available
array_type = self.stats[array]['type'].upper() if self.stats[array]['type'] is not None else 'UNKNOWN'
# Build the full name = array type + array name
full_name = '{} {}'.format(array_type, array)
msg = '{:{width}}'.format(full_name, width=name_max_width)
ret.append(self.curse_add_line(msg))
if self.stats[array]['type'] == 'raid0' and self.stats[array]['status'] == 'active':
msg = '{:>7}'.format(len(self.stats[array]['components']))
ret.append(self.curse_add_line(msg, status))
msg = '{:>7}'.format('-')
ret.append(self.curse_add_line(msg, status))
elif self.stats[array]['status'] == 'active':
msg = '{:>7}'.format(self.stats[array]['used'])
ret.append(self.curse_add_line(msg, status))
msg = '{:>7}'.format(self.stats[array]['available'])
ret.append(self.curse_add_line(msg, status))
elif self.stats[array]['status'] == 'inactive':
ret.append(self.curse_new_line())
msg = '└─ Status {}'.format(self.stats[array]['status'])
ret.append(self.curse_add_line(msg, status))
components = sorted(iterkeys(self.stats[array]['components']))
for i, component in enumerate(components):
if i == len(components) - 1:
tree_char = '└─'
else:
tree_char = '├─'
ret.append(self.curse_new_line())
msg = ' {} disk {}: '.format(tree_char, self.stats[array]['components'][component])
ret.append(self.curse_add_line(msg))
msg = '{}'.format(component)
ret.append(self.curse_add_line(msg))
if self.stats[array]['type'] != 'raid0' and (self.stats[array]['used'] < self.stats[array]['available']):
# Display current array configuration
ret.append(self.curse_new_line())
msg = '└─ Degraded mode'
ret.append(self.curse_add_line(msg, status))
if len(self.stats[array]['config']) < 17:
ret.append(self.curse_new_line())
msg = ' └─ {}'.format(self.stats[array]['config'].replace('_', 'A'))
ret.append(self.curse_add_line(msg))
return ret
def raid_alert(self, status, used, available, type):
"""RAID alert messages.
[available/used] means that ideally the array may have _available_
devices however, _used_ devices are in use.
Obviously when used >= available then things are good.
"""
if type == 'raid0':
return 'OK'
if status == 'inactive':
return 'CRITICAL'
if used is None or available is None:
return 'DEFAULT'
elif used < available:
return 'WARNING'
return 'OK'

View File

@ -0,0 +1,367 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Sensors plugin."""
import psutil
import warnings
from glances.logger import logger
from glances.globals import iteritems, to_fahrenheit
from glances.timer import Counter
from glances.plugins.sensors.sensor.glances_batpercent import PluginModel as BatPercentPluginModel
from glances.plugins.sensors.sensor.glances_hddtemp import PluginModel as HddTempPluginModel
from glances.outputs.glances_unicode import unicode_message
from glances.plugins.plugin.model import GlancesPluginModel
SENSOR_TEMP_TYPE = 'temperature_core'
SENSOR_TEMP_UNIT = 'C'
SENSOR_FAN_TYPE = 'fan_speed'
SENSOR_FAN_UNIT = 'R'
class PluginModel(GlancesPluginModel):
"""Glances sensors plugin.
The stats list includes both sensors and hard disks stats, if any.
The sensors are already grouped by chip type and then sorted by name.
The hard disks are already sorted by name.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
start_duration = Counter()
# Init the sensor class
start_duration.reset()
self.glances_grab_sensors = GlancesGrabSensors()
logger.debug("Generic sensor plugin init duration: {} seconds".format(start_duration.get()))
# Instance for the HDDTemp Plugin in order to display the hard disks
# temperatures
start_duration.reset()
self.hddtemp_plugin = HddTempPluginModel(args=args, config=config)
logger.debug("HDDTemp sensor plugin init duration: {} seconds".format(start_duration.get()))
# Instance for the BatPercent in order to display the batteries
# capacities
start_duration.reset()
self.batpercent_plugin = BatPercentPluginModel(args=args, config=config)
logger.debug("Battery sensor plugin init duration: {} seconds".format(start_duration.get()))
# We want to display the stat in the curse interface
self.display_curse = True
# Not necessary to refresh every refresh time
# By default set to refresh * 2
if self.get_refresh() == args.time:
self.set_refresh(self.get_refresh() * 2)
def get_key(self):
"""Return the key of the list."""
return 'label'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update sensors stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the dedicated lib
stats = []
# Get the temperature
try:
temperature = self.__set_type(self.glances_grab_sensors.get(SENSOR_TEMP_TYPE), SENSOR_TEMP_TYPE)
except Exception as e:
logger.error("Cannot grab sensors temperatures (%s)" % e)
else:
# Append temperature
stats.extend(temperature)
# Get the FAN speed
try:
fan_speed = self.__set_type(self.glances_grab_sensors.get(SENSOR_FAN_TYPE), SENSOR_FAN_TYPE)
except Exception as e:
logger.error("Cannot grab FAN speed (%s)" % e)
else:
# Append FAN speed
stats.extend(fan_speed)
# Update HDDtemp stats
try:
hddtemp = self.__set_type(self.hddtemp_plugin.update(), 'temperature_hdd')
except Exception as e:
logger.error("Cannot grab HDD temperature (%s)" % e)
else:
# Append HDD temperature
stats.extend(hddtemp)
# Update batteries stats
try:
bat_percent = self.__set_type(self.batpercent_plugin.update(), 'battery')
except Exception as e:
logger.error("Cannot grab battery percent (%s)" % e)
else:
# Append Batteries %
stats.extend(bat_percent)
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard:
# http://www.net-snmp.org/wiki/index.php/Net-SNMP_and_lm-sensors_on_Ubuntu_10.04
pass
# Global change on stats
self.stats = self.get_init_value()
for stat in stats:
# Do not take hide stat into account
if not self.is_display(stat["label"].lower()):
continue
# Set the alias for each stat
# alias = self.has_alias(stat["label"].lower())
# if alias:
# stat["label"] = alias
stat["label"] = self.__get_alias(stat)
# Update the stats
self.stats.append(stat)
return self.stats
def __get_alias(self, stats):
"""Return the alias of the sensor."""
# Get the alias for each stat
if self.has_alias(stats["label"].lower()):
return self.has_alias(stats["label"].lower())
elif self.has_alias("{}_{}".format(stats["label"], stats["type"]).lower()):
return self.has_alias("{}_{}".format(stats["label"], stats["type"]).lower())
else:
return stats["label"]
def __set_type(self, stats, sensor_type):
"""Set the plugin type.
4 types of stats is possible in the sensors plugin:
- Core temperature: SENSOR_TEMP_TYPE
- Fan speed: SENSOR_FAN_TYPE
- HDD temperature: 'temperature_hdd'
- Battery capacity: 'battery'
"""
for i in stats:
# Set the sensors type
i.update({'type': sensor_type})
# also add the key name
i.update({'key': self.get_key()})
return stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
if not i['value']:
continue
# Alert processing
if i['type'] == SENSOR_TEMP_TYPE:
if self.is_limit('critical', stat_name='sensors_temperature_' + i['label']):
# By default use the thresholds configured in the glances.conf file (see #2058)
alert = self.get_alert(current=i['value'], header='temperature_' + i['label'])
else:
# Else use the system thresholds
if i['critical'] is None:
alert = 'DEFAULT'
elif i['value'] >= i['critical']:
alert = 'CRITICAL'
elif i['warning'] is None:
alert = 'DEFAULT'
elif i['value'] >= i['warning']:
alert = 'WARNING'
else:
alert = 'OK'
elif i['type'] == 'battery':
alert = self.get_alert(current=100 - i['value'], header=i['type'])
else:
alert = self.get_alert(current=i['value'], header=i['type'])
# Set the alert in the view
self.views[i[self.get_key()]]['value']['decoration'] = alert
def battery_trend(self, stats):
"""Return the trend character for the battery"""
if 'status' not in stats:
return ''
if stats['status'].startswith('Charg'):
return unicode_message('ARROW_UP')
elif stats['status'].startswith('Discharg'):
return unicode_message('ARROW_DOWN')
elif stats['status'].startswith('Full'):
return unicode_message('CHECK')
return ''
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Header
msg = '{:{width}}'.format('SENSORS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
# Stats
for i in self.stats:
# Do not display anything if no battery are detected
if i['type'] == 'battery' and i['value'] == []:
continue
# New line
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(i["label"][:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
if i['value'] in (b'ERR', b'SLP', b'UNK', b'NOS'):
msg = '{:>14}'.format(i['value'])
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='value', option='decoration'))
)
else:
if args.fahrenheit and i['type'] != 'battery' and i['type'] != SENSOR_FAN_TYPE:
trend = ''
value = to_fahrenheit(i['value'])
unit = 'F'
else:
trend = self.battery_trend(i)
value = i['value']
unit = i['unit']
try:
msg = '{:.0f}{}{}'.format(value, unit, trend)
msg = '{:>14}'.format(msg)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='value', option='decoration')
)
)
except (TypeError, ValueError):
pass
return ret
class GlancesGrabSensors(object):
"""Get sensors stats."""
def __init__(self):
"""Init sensors stats."""
# Temperatures
self.init_temp = False
self.sensor_temps = {}
try:
# psutil>=5.1.0, Linux-only
self.sensor_temps = psutil.sensors_temperatures()
except AttributeError:
logger.debug("Cannot grab temperatures. Platform not supported.")
else:
self.init_temp = True
# Solve an issue #1203 concerning a RunTimeError warning message displayed
# in the curses interface.
warnings.filterwarnings("ignore")
# Fans
self.init_fan = False
self.sensor_fans = {}
try:
# psutil>=5.2.0, Linux-only
self.sensor_fans = psutil.sensors_fans()
except AttributeError:
logger.debug("Cannot grab fans speed. Platform not supported.")
else:
self.init_fan = True
# Init the stats
self.reset()
def reset(self):
"""Reset/init the stats."""
self.sensors_list = []
def __update__(self):
"""Update the stats."""
# Reset the list
self.reset()
if not self.init_temp:
return self.sensors_list
# Temperatures sensors
self.sensors_list.extend(self.build_sensors_list(SENSOR_TEMP_UNIT))
# Fans sensors
self.sensors_list.extend(self.build_sensors_list(SENSOR_FAN_UNIT))
return self.sensors_list
def build_sensors_list(self, type):
"""Build the sensors list depending of the type.
type: SENSOR_TEMP_UNIT or SENSOR_FAN_UNIT
output: a list
"""
ret = []
if type == SENSOR_TEMP_UNIT and self.init_temp:
input_list = self.sensor_temps
self.sensor_temps = psutil.sensors_temperatures()
elif type == SENSOR_FAN_UNIT and self.init_fan:
input_list = self.sensor_fans
self.sensor_fans = psutil.sensors_fans()
else:
return ret
for chip_name, chip in iteritems(input_list):
label_index = 1
for chip_name_index, feature in enumerate(chip):
sensors_current = {}
# Sensor name
if feature.label == '':
sensors_current['label'] = chip_name + ' ' + str(chip_name_index)
elif feature.label in [i['label'] for i in ret]:
sensors_current['label'] = feature.label + ' ' + str(label_index)
label_index += 1
else:
sensors_current['label'] = feature.label
# Sensors value, limit and unit
sensors_current['unit'] = type
sensors_current['value'] = int(getattr(feature, 'current', 0) if getattr(feature, 'current', 0) else 0)
system_warning = getattr(feature, 'high', None)
system_critical = getattr(feature, 'critical', None)
sensors_current['warning'] = int(system_warning) if system_warning is not None else None
sensors_current['critical'] = int(system_critical) if system_critical is not None else None
# Add sensor to the list
ret.append(sensors_current)
return ret
def get(self, sensor_type=SENSOR_TEMP_TYPE):
"""Get sensors list."""
self.__update__()
if sensor_type == SENSOR_TEMP_TYPE:
ret = [s for s in self.sensors_list if s['unit'] == SENSOR_TEMP_UNIT]
elif sensor_type == SENSOR_FAN_TYPE:
ret = [s for s in self.sensors_list if s['unit'] == SENSOR_FAN_UNIT]
else:
# Unknown type
logger.debug("Unknown sensor type %s" % sensor_type)
ret = []
return ret

View File

@ -1,367 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Sensors plugin."""
import psutil
import warnings
from glances.logger import logger
from glances.globals import iteritems, to_fahrenheit
from glances.timer import Counter
from glances.plugins.sensors.sensor.glances_batpercent import PluginModel as BatPercentPluginModel
from glances.plugins.sensors.sensor.glances_hddtemp import PluginModel as HddTempPluginModel
from glances.outputs.glances_unicode import unicode_message
from glances.plugins.plugin.model import GlancesPluginModel
SENSOR_TEMP_TYPE = 'temperature_core'
SENSOR_TEMP_UNIT = 'C'
SENSOR_FAN_TYPE = 'fan_speed'
SENSOR_FAN_UNIT = 'R'
class PluginModel(GlancesPluginModel):
"""Glances sensors plugin.
The stats list includes both sensors and hard disks stats, if any.
The sensors are already grouped by chip type and then sorted by name.
The hard disks are already sorted by name.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
start_duration = Counter()
# Init the sensor class
start_duration.reset()
self.glances_grab_sensors = GlancesGrabSensors()
logger.debug("Generic sensor plugin init duration: {} seconds".format(start_duration.get()))
# Instance for the HDDTemp Plugin in order to display the hard disks
# temperatures
start_duration.reset()
self.hddtemp_plugin = HddTempPluginModel(args=args, config=config)
logger.debug("HDDTemp sensor plugin init duration: {} seconds".format(start_duration.get()))
# Instance for the BatPercent in order to display the batteries
# capacities
start_duration.reset()
self.batpercent_plugin = BatPercentPluginModel(args=args, config=config)
logger.debug("Battery sensor plugin init duration: {} seconds".format(start_duration.get()))
# We want to display the stat in the curse interface
self.display_curse = True
# Not necessary to refresh every refresh time
# By default set to refresh * 2
if self.get_refresh() == args.time:
self.set_refresh(self.get_refresh() * 2)
def get_key(self):
"""Return the key of the list."""
return 'label'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update sensors stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the dedicated lib
stats = []
# Get the temperature
try:
temperature = self.__set_type(self.glances_grab_sensors.get(SENSOR_TEMP_TYPE), SENSOR_TEMP_TYPE)
except Exception as e:
logger.error("Cannot grab sensors temperatures (%s)" % e)
else:
# Append temperature
stats.extend(temperature)
# Get the FAN speed
try:
fan_speed = self.__set_type(self.glances_grab_sensors.get(SENSOR_FAN_TYPE), SENSOR_FAN_TYPE)
except Exception as e:
logger.error("Cannot grab FAN speed (%s)" % e)
else:
# Append FAN speed
stats.extend(fan_speed)
# Update HDDtemp stats
try:
hddtemp = self.__set_type(self.hddtemp_plugin.update(), 'temperature_hdd')
except Exception as e:
logger.error("Cannot grab HDD temperature (%s)" % e)
else:
# Append HDD temperature
stats.extend(hddtemp)
# Update batteries stats
try:
bat_percent = self.__set_type(self.batpercent_plugin.update(), 'battery')
except Exception as e:
logger.error("Cannot grab battery percent (%s)" % e)
else:
# Append Batteries %
stats.extend(bat_percent)
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard:
# http://www.net-snmp.org/wiki/index.php/Net-SNMP_and_lm-sensors_on_Ubuntu_10.04
pass
# Global change on stats
self.stats = self.get_init_value()
for stat in stats:
# Do not take hide stat into account
if not self.is_display(stat["label"].lower()):
continue
# Set the alias for each stat
# alias = self.has_alias(stat["label"].lower())
# if alias:
# stat["label"] = alias
stat["label"] = self.__get_alias(stat)
# Update the stats
self.stats.append(stat)
return self.stats
def __get_alias(self, stats):
"""Return the alias of the sensor."""
# Get the alias for each stat
if self.has_alias(stats["label"].lower()):
return self.has_alias(stats["label"].lower())
elif self.has_alias("{}_{}".format(stats["label"], stats["type"]).lower()):
return self.has_alias("{}_{}".format(stats["label"], stats["type"]).lower())
else:
return stats["label"]
def __set_type(self, stats, sensor_type):
"""Set the plugin type.
4 types of stats is possible in the sensors plugin:
- Core temperature: SENSOR_TEMP_TYPE
- Fan speed: SENSOR_FAN_TYPE
- HDD temperature: 'temperature_hdd'
- Battery capacity: 'battery'
"""
for i in stats:
# Set the sensors type
i.update({'type': sensor_type})
# also add the key name
i.update({'key': self.get_key()})
return stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
if not i['value']:
continue
# Alert processing
if i['type'] == SENSOR_TEMP_TYPE:
if self.is_limit('critical', stat_name='sensors_temperature_' + i['label']):
# By default use the thresholds configured in the glances.conf file (see #2058)
alert = self.get_alert(current=i['value'], header='temperature_' + i['label'])
else:
# Else use the system thresholds
if i['critical'] is None:
alert = 'DEFAULT'
elif i['value'] >= i['critical']:
alert = 'CRITICAL'
elif i['warning'] is None:
alert = 'DEFAULT'
elif i['value'] >= i['warning']:
alert = 'WARNING'
else:
alert = 'OK'
elif i['type'] == 'battery':
alert = self.get_alert(current=100 - i['value'], header=i['type'])
else:
alert = self.get_alert(current=i['value'], header=i['type'])
# Set the alert in the view
self.views[i[self.get_key()]]['value']['decoration'] = alert
def battery_trend(self, stats):
"""Return the trend character for the battery"""
if 'status' not in stats:
return ''
if stats['status'].startswith('Charg'):
return unicode_message('ARROW_UP')
elif stats['status'].startswith('Discharg'):
return unicode_message('ARROW_DOWN')
elif stats['status'].startswith('Full'):
return unicode_message('CHECK')
return ''
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Header
msg = '{:{width}}'.format('SENSORS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
# Stats
for i in self.stats:
# Do not display anything if no battery are detected
if i['type'] == 'battery' and i['value'] == []:
continue
# New line
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(i["label"][:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
if i['value'] in (b'ERR', b'SLP', b'UNK', b'NOS'):
msg = '{:>14}'.format(i['value'])
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='value', option='decoration'))
)
else:
if args.fahrenheit and i['type'] != 'battery' and i['type'] != SENSOR_FAN_TYPE:
trend = ''
value = to_fahrenheit(i['value'])
unit = 'F'
else:
trend = self.battery_trend(i)
value = i['value']
unit = i['unit']
try:
msg = '{:.0f}{}{}'.format(value, unit, trend)
msg = '{:>14}'.format(msg)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='value', option='decoration')
)
)
except (TypeError, ValueError):
pass
return ret
class GlancesGrabSensors(object):
"""Get sensors stats."""
def __init__(self):
"""Init sensors stats."""
# Temperatures
self.init_temp = False
self.sensor_temps = {}
try:
# psutil>=5.1.0, Linux-only
self.sensor_temps = psutil.sensors_temperatures()
except AttributeError:
logger.debug("Cannot grab temperatures. Platform not supported.")
else:
self.init_temp = True
# Solve an issue #1203 concerning a RunTimeError warning message displayed
# in the curses interface.
warnings.filterwarnings("ignore")
# Fans
self.init_fan = False
self.sensor_fans = {}
try:
# psutil>=5.2.0, Linux-only
self.sensor_fans = psutil.sensors_fans()
except AttributeError:
logger.debug("Cannot grab fans speed. Platform not supported.")
else:
self.init_fan = True
# Init the stats
self.reset()
def reset(self):
"""Reset/init the stats."""
self.sensors_list = []
def __update__(self):
"""Update the stats."""
# Reset the list
self.reset()
if not self.init_temp:
return self.sensors_list
# Temperatures sensors
self.sensors_list.extend(self.build_sensors_list(SENSOR_TEMP_UNIT))
# Fans sensors
self.sensors_list.extend(self.build_sensors_list(SENSOR_FAN_UNIT))
return self.sensors_list
def build_sensors_list(self, type):
"""Build the sensors list depending of the type.
type: SENSOR_TEMP_UNIT or SENSOR_FAN_UNIT
output: a list
"""
ret = []
if type == SENSOR_TEMP_UNIT and self.init_temp:
input_list = self.sensor_temps
self.sensor_temps = psutil.sensors_temperatures()
elif type == SENSOR_FAN_UNIT and self.init_fan:
input_list = self.sensor_fans
self.sensor_fans = psutil.sensors_fans()
else:
return ret
for chip_name, chip in iteritems(input_list):
label_index = 1
for chip_name_index, feature in enumerate(chip):
sensors_current = {}
# Sensor name
if feature.label == '':
sensors_current['label'] = chip_name + ' ' + str(chip_name_index)
elif feature.label in [i['label'] for i in ret]:
sensors_current['label'] = feature.label + ' ' + str(label_index)
label_index += 1
else:
sensors_current['label'] = feature.label
# Sensors value, limit and unit
sensors_current['unit'] = type
sensors_current['value'] = int(getattr(feature, 'current', 0) if getattr(feature, 'current', 0) else 0)
system_warning = getattr(feature, 'high', None)
system_critical = getattr(feature, 'critical', None)
sensors_current['warning'] = int(system_warning) if system_warning is not None else None
sensors_current['critical'] = int(system_critical) if system_critical is not None else None
# Add sensor to the list
ret.append(sensors_current)
return ret
def get(self, sensor_type=SENSOR_TEMP_TYPE):
"""Get sensors list."""
self.__update__()
if sensor_type == SENSOR_TEMP_TYPE:
ret = [s for s in self.sensors_list if s['unit'] == SENSOR_TEMP_UNIT]
elif sensor_type == SENSOR_FAN_TYPE:
ret = [s for s in self.sensors_list if s['unit'] == SENSOR_FAN_UNIT]
else:
# Unknown type
logger.debug("Unknown sensor type %s" % sensor_type)
ret = []
return ret

View File

@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2018 Tim Nibert <docz2a@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""
Hard disk SMART attributes plugin.
Depends on pySMART and smartmontools
Must execute as root
"usermod -a -G disk USERNAME" is not sufficient unfortunately
SmartCTL (/usr/sbin/smartctl) must be in system path for python2.
Regular PySMART is a python2 library.
We are using the pySMART.smartx updated library to support both python 2 and 3.
If we only have disk group access (no root):
$ smartctl -i /dev/sda
smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.15.0-30-generic] (local build)
Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
Probable ATA device behind a SAT layer
Try an additional '-d ata' or '-d sat' argument.
This is not very hopeful: https://medium.com/opsops/why-smartctl-could-not-be-run-without-root-7ea0583b1323
So, here is what we are going to do:
Check for admin access. If no admin access, disable SMART plugin.
If smartmontools is not installed, we should catch the error upstream in plugin initialization.
"""
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
from glances.main import disable
from glances.globals import is_admin
# Import plugin specific dependency
try:
from pySMART import DeviceList
except ImportError as e:
import_error_tag = True
logger.warning("Missing Python Lib ({}), HDD Smart plugin is disabled".format(e))
else:
import_error_tag = False
def convert_attribute_to_dict(attr):
return {
'name': attr.name,
'num': attr.num,
'flags': attr.flags,
'raw': attr.raw,
'value': attr.value,
'worst': attr.worst,
'threshold': attr.thresh,
'type': attr.type,
'updated': attr.updated,
'when_failed': attr.when_failed,
}
def get_smart_data():
"""
Get SMART attribute data
:return: list of multi leveled dictionaries
each dict has a key "DeviceName" with the identification of the device in smartctl
also has keys of the SMART attribute id, with value of another dict of the attributes
[
{
"DeviceName": "/dev/sda blahblah",
"1":
{
"flags": "..",
"raw": "..",
etc,
}
...
}
]
"""
stats = []
# get all devices
try:
devlist = DeviceList()
except TypeError as e:
# Catch error (see #1806)
logger.debug('Smart plugin error - Can not grab device list ({})'.format(e))
global import_error_tag
import_error_tag = True
return stats
for dev in devlist.devices:
stats.append(
{
'DeviceName': '{} {}'.format(dev.name, dev.model),
}
)
for attribute in dev.attributes:
if attribute is None:
pass
else:
attrib_dict = convert_attribute_to_dict(attribute)
# we will use the attribute number as the key
num = attrib_dict.pop('num', None)
try:
assert num is not None
except Exception as e:
# we should never get here, but if we do, continue to next iteration and skip this attribute
logger.debug('Smart plugin error - Skip the attribute {} ({})'.format(attribute, e))
continue
stats[-1][num] = attrib_dict
return stats
class PluginModel(GlancesPluginModel):
"""Glances' HDD SMART plugin."""
def __init__(self, args=None, config=None, stats_init_value=[]):
"""Init the plugin."""
# check if user is admin
if not is_admin():
disable(args, "smart")
logger.debug("Current user is not admin, HDD SMART plugin disabled.")
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update SMART stats using the input method."""
# Init new stats
stats = self.get_init_value()
if import_error_tag:
return self.stats
if self.input_method == 'local':
stats = get_smart_data()
elif self.input_method == 'snmp':
pass
# Update the stats
self.stats = stats
return self.stats
def get_key(self):
"""Return the key of the list."""
return 'DeviceName'
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist...
if import_error_tag or not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 6
# Header
msg = '{:{width}}'.format('SMART disks', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
# Data
for device_stat in self.stats:
# New line
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(device_stat['DeviceName'][:max_width], width=max_width)
ret.append(self.curse_add_line(msg))
for smart_stat in sorted([i for i in device_stat.keys() if i != 'DeviceName'], key=int):
ret.append(self.curse_new_line())
msg = ' {:{width}}'.format(
device_stat[smart_stat]['name'][: name_max_width - 1].replace('_', ' '), width=name_max_width - 1
)
ret.append(self.curse_add_line(msg))
msg = '{:>8}'.format(device_stat[smart_stat]['raw'])
ret.append(self.curse_add_line(msg))
return ret

View File

@ -1,191 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2018 Tim Nibert <docz2a@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""
Hard disk SMART attributes plugin.
Depends on pySMART and smartmontools
Must execute as root
"usermod -a -G disk USERNAME" is not sufficient unfortunately
SmartCTL (/usr/sbin/smartctl) must be in system path for python2.
Regular PySMART is a python2 library.
We are using the pySMART.smartx updated library to support both python 2 and 3.
If we only have disk group access (no root):
$ smartctl -i /dev/sda
smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.15.0-30-generic] (local build)
Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
Probable ATA device behind a SAT layer
Try an additional '-d ata' or '-d sat' argument.
This is not very hopeful: https://medium.com/opsops/why-smartctl-could-not-be-run-without-root-7ea0583b1323
So, here is what we are going to do:
Check for admin access. If no admin access, disable SMART plugin.
If smartmontools is not installed, we should catch the error upstream in plugin initialization.
"""
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
from glances.main import disable
from glances.globals import is_admin
# Import plugin specific dependency
try:
from pySMART import DeviceList
except ImportError as e:
import_error_tag = True
logger.warning("Missing Python Lib ({}), HDD Smart plugin is disabled".format(e))
else:
import_error_tag = False
def convert_attribute_to_dict(attr):
return {
'name': attr.name,
'num': attr.num,
'flags': attr.flags,
'raw': attr.raw,
'value': attr.value,
'worst': attr.worst,
'threshold': attr.thresh,
'type': attr.type,
'updated': attr.updated,
'when_failed': attr.when_failed,
}
def get_smart_data():
"""
Get SMART attribute data
:return: list of multi leveled dictionaries
each dict has a key "DeviceName" with the identification of the device in smartctl
also has keys of the SMART attribute id, with value of another dict of the attributes
[
{
"DeviceName": "/dev/sda blahblah",
"1":
{
"flags": "..",
"raw": "..",
etc,
}
...
}
]
"""
stats = []
# get all devices
try:
devlist = DeviceList()
except TypeError as e:
# Catch error (see #1806)
logger.debug('Smart plugin error - Can not grab device list ({})'.format(e))
global import_error_tag
import_error_tag = True
return stats
for dev in devlist.devices:
stats.append(
{
'DeviceName': '{} {}'.format(dev.name, dev.model),
}
)
for attribute in dev.attributes:
if attribute is None:
pass
else:
attrib_dict = convert_attribute_to_dict(attribute)
# we will use the attribute number as the key
num = attrib_dict.pop('num', None)
try:
assert num is not None
except Exception as e:
# we should never get here, but if we do, continue to next iteration and skip this attribute
logger.debug('Smart plugin error - Skip the attribute {} ({})'.format(attribute, e))
continue
stats[-1][num] = attrib_dict
return stats
class PluginModel(GlancesPluginModel):
"""Glances' HDD SMART plugin."""
def __init__(self, args=None, config=None, stats_init_value=[]):
"""Init the plugin."""
# check if user is admin
if not is_admin():
disable(args, "smart")
logger.debug("Current user is not admin, HDD SMART plugin disabled.")
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update SMART stats using the input method."""
# Init new stats
stats = self.get_init_value()
if import_error_tag:
return self.stats
if self.input_method == 'local':
stats = get_smart_data()
elif self.input_method == 'snmp':
pass
# Update the stats
self.stats = stats
return self.stats
def get_key(self):
"""Return the key of the list."""
return 'DeviceName'
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist...
if import_error_tag or not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 6
# Header
msg = '{:{width}}'.format('SMART disks', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
# Data
for device_stat in self.stats:
# New line
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(device_stat['DeviceName'][:max_width], width=max_width)
ret.append(self.curse_add_line(msg))
for smart_stat in sorted([i for i in device_stat.keys() if i != 'DeviceName'], key=int):
ret.append(self.curse_new_line())
msg = ' {:{width}}'.format(
device_stat[smart_stat]['name'][: name_max_width - 1].replace('_', ' '), width=name_max_width - 1
)
ret.append(self.curse_add_line(msg))
msg = '{:>8}'.format(device_stat[smart_stat]['raw'])
ret.append(self.curse_add_line(msg))
return ret

View File

@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""System plugin."""
import os
import platform
import re
from io import open
from glances.globals import iteritems
from glances.plugins.plugin.model import GlancesPluginModel
# SNMP OID
snmp_oid = {
'default': {'hostname': '1.3.6.1.2.1.1.5.0', 'system_name': '1.3.6.1.2.1.1.1.0'},
'netapp': {
'hostname': '1.3.6.1.2.1.1.5.0',
'system_name': '1.3.6.1.2.1.1.1.0',
'platform': '1.3.6.1.4.1.789.1.1.5.0',
},
}
# SNMP to human read
# Dict (key: OS short name) of dict (reg exp OID to human)
# Windows:
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms724832%28v=vs.85%29.aspx
snmp_to_human = {
'windows': {
'Windows Version 10.0': 'Windows 10 or Server 2016',
'Windows Version 6.3': 'Windows 8.1 or Server 2012R2',
'Windows Version 6.2': 'Windows 8 or Server 2012',
'Windows Version 6.1': 'Windows 7 or Server 2008R2',
'Windows Version 6.0': 'Windows Vista or Server 2008',
'Windows Version 5.2': 'Windows XP 64bits or 2003 server',
'Windows Version 5.1': 'Windows XP',
'Windows Version 5.0': 'Windows 2000',
}
}
def _linux_os_release():
"""Try to determine the name of a Linux distribution.
This function checks for the /etc/os-release file.
It takes the name from the 'NAME' field and the version from 'VERSION_ID'.
An empty string is returned if the above values cannot be determined.
"""
pretty_name = ''
ashtray = {}
keys = ['NAME', 'VERSION_ID']
try:
with open(os.path.join('/etc', 'os-release')) as f:
for line in f:
for key in keys:
if line.startswith(key):
ashtray[key] = re.sub(r'^"|"$', '', line.strip().split('=')[1])
except (OSError, IOError):
return pretty_name
if ashtray:
if 'NAME' in ashtray:
pretty_name = ashtray['NAME']
if 'VERSION_ID' in ashtray:
pretty_name += ' {}'.format(ashtray['VERSION_ID'])
return pretty_name
class PluginModel(GlancesPluginModel):
"""Glances' host/system plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Set default rate to 60 seconds
if self.get_refresh():
self.set_refresh(60)
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the host/system info using the input method.
:return: the stats dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
stats['os_name'] = platform.system()
stats['hostname'] = platform.node()
stats['platform'] = platform.architecture()[0]
if stats['os_name'] == "Linux":
try:
linux_distro = platform.linux_distribution()
except AttributeError:
stats['linux_distro'] = _linux_os_release()
else:
if linux_distro[0] == '':
stats['linux_distro'] = _linux_os_release()
else:
stats['linux_distro'] = ' '.join(linux_distro[:2])
stats['os_version'] = platform.release()
elif stats['os_name'].endswith('BSD') or stats['os_name'] == 'SunOS':
stats['os_version'] = platform.release()
elif stats['os_name'] == "Darwin":
stats['os_version'] = platform.mac_ver()[0]
elif stats['os_name'] == "Windows":
os_version = platform.win32_ver()
stats['os_version'] = ' '.join(os_version[::2])
# if the python version is 32 bit perhaps the windows operating
# system is 64bit
if stats['platform'] == '32bit' and 'PROCESSOR_ARCHITEW6432' in os.environ:
stats['platform'] = '64bit'
else:
stats['os_version'] = ""
# Add human readable name
if stats['os_name'] == "Linux":
stats['hr_name'] = stats['linux_distro']
else:
stats['hr_name'] = '{} {}'.format(stats['os_name'], stats['os_version'])
stats['hr_name'] += ' {}'.format(stats['platform'])
elif self.input_method == 'snmp':
# Update stats using SNMP
try:
stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name])
except KeyError:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
# Default behavior: display all the information
stats['os_name'] = stats['system_name']
# Windows OS tips
if self.short_system_name == 'windows':
for r, v in iteritems(snmp_to_human['windows']):
if re.search(r, stats['system_name']):
stats['os_name'] = v
break
# Add human readable name
stats['hr_name'] = stats['os_name']
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# Build the string message
if args.client:
# Client mode
if args.cs_status.lower() == "connected":
msg = 'Connected to '
ret.append(self.curse_add_line(msg, 'OK'))
elif args.cs_status.lower() == "snmp":
msg = 'SNMP from '
ret.append(self.curse_add_line(msg, 'OK'))
elif args.cs_status.lower() == "disconnected":
msg = 'Disconnected from '
ret.append(self.curse_add_line(msg, 'CRITICAL'))
# Hostname is mandatory
msg = self.stats['hostname']
ret.append(self.curse_add_line(msg, "TITLE"))
# System info
if self.stats['os_name'] == "Linux" and self.stats['linux_distro']:
msg = ' ({} {} / {} {})'.format(
self.stats['linux_distro'], self.stats['platform'], self.stats['os_name'], self.stats['os_version']
)
else:
try:
msg = ' ({} {} {})'.format(self.stats['os_name'], self.stats['os_version'], self.stats['platform'])
except Exception:
msg = ' ({})'.format(self.stats['os_name'])
ret.append(self.curse_add_line(msg, optional=True))
# Return the message with decoration
return ret

View File

@ -1,200 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""System plugin."""
import os
import platform
import re
from io import open
from glances.globals import iteritems
from glances.plugins.plugin.model import GlancesPluginModel
# SNMP OID
snmp_oid = {
'default': {'hostname': '1.3.6.1.2.1.1.5.0', 'system_name': '1.3.6.1.2.1.1.1.0'},
'netapp': {
'hostname': '1.3.6.1.2.1.1.5.0',
'system_name': '1.3.6.1.2.1.1.1.0',
'platform': '1.3.6.1.4.1.789.1.1.5.0',
},
}
# SNMP to human read
# Dict (key: OS short name) of dict (reg exp OID to human)
# Windows:
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms724832%28v=vs.85%29.aspx
snmp_to_human = {
'windows': {
'Windows Version 10.0': 'Windows 10 or Server 2016',
'Windows Version 6.3': 'Windows 8.1 or Server 2012R2',
'Windows Version 6.2': 'Windows 8 or Server 2012',
'Windows Version 6.1': 'Windows 7 or Server 2008R2',
'Windows Version 6.0': 'Windows Vista or Server 2008',
'Windows Version 5.2': 'Windows XP 64bits or 2003 server',
'Windows Version 5.1': 'Windows XP',
'Windows Version 5.0': 'Windows 2000',
}
}
def _linux_os_release():
"""Try to determine the name of a Linux distribution.
This function checks for the /etc/os-release file.
It takes the name from the 'NAME' field and the version from 'VERSION_ID'.
An empty string is returned if the above values cannot be determined.
"""
pretty_name = ''
ashtray = {}
keys = ['NAME', 'VERSION_ID']
try:
with open(os.path.join('/etc', 'os-release')) as f:
for line in f:
for key in keys:
if line.startswith(key):
ashtray[key] = re.sub(r'^"|"$', '', line.strip().split('=')[1])
except (OSError, IOError):
return pretty_name
if ashtray:
if 'NAME' in ashtray:
pretty_name = ashtray['NAME']
if 'VERSION_ID' in ashtray:
pretty_name += ' {}'.format(ashtray['VERSION_ID'])
return pretty_name
class PluginModel(GlancesPluginModel):
"""Glances' host/system plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Set default rate to 60 seconds
if self.get_refresh():
self.set_refresh(60)
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the host/system info using the input method.
:return: the stats dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
stats['os_name'] = platform.system()
stats['hostname'] = platform.node()
stats['platform'] = platform.architecture()[0]
if stats['os_name'] == "Linux":
try:
linux_distro = platform.linux_distribution()
except AttributeError:
stats['linux_distro'] = _linux_os_release()
else:
if linux_distro[0] == '':
stats['linux_distro'] = _linux_os_release()
else:
stats['linux_distro'] = ' '.join(linux_distro[:2])
stats['os_version'] = platform.release()
elif stats['os_name'].endswith('BSD') or stats['os_name'] == 'SunOS':
stats['os_version'] = platform.release()
elif stats['os_name'] == "Darwin":
stats['os_version'] = platform.mac_ver()[0]
elif stats['os_name'] == "Windows":
os_version = platform.win32_ver()
stats['os_version'] = ' '.join(os_version[::2])
# if the python version is 32 bit perhaps the windows operating
# system is 64bit
if stats['platform'] == '32bit' and 'PROCESSOR_ARCHITEW6432' in os.environ:
stats['platform'] = '64bit'
else:
stats['os_version'] = ""
# Add human readable name
if stats['os_name'] == "Linux":
stats['hr_name'] = stats['linux_distro']
else:
stats['hr_name'] = '{} {}'.format(stats['os_name'], stats['os_version'])
stats['hr_name'] += ' {}'.format(stats['platform'])
elif self.input_method == 'snmp':
# Update stats using SNMP
try:
stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name])
except KeyError:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
# Default behavior: display all the information
stats['os_name'] = stats['system_name']
# Windows OS tips
if self.short_system_name == 'windows':
for r, v in iteritems(snmp_to_human['windows']):
if re.search(r, stats['system_name']):
stats['os_name'] = v
break
# Add human readable name
stats['hr_name'] = stats['os_name']
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# Build the string message
if args.client:
# Client mode
if args.cs_status.lower() == "connected":
msg = 'Connected to '
ret.append(self.curse_add_line(msg, 'OK'))
elif args.cs_status.lower() == "snmp":
msg = 'SNMP from '
ret.append(self.curse_add_line(msg, 'OK'))
elif args.cs_status.lower() == "disconnected":
msg = 'Disconnected from '
ret.append(self.curse_add_line(msg, 'CRITICAL'))
# Hostname is mandatory
msg = self.stats['hostname']
ret.append(self.curse_add_line(msg, "TITLE"))
# System info
if self.stats['os_name'] == "Linux" and self.stats['linux_distro']:
msg = ' ({} {} / {} {})'.format(
self.stats['linux_distro'], self.stats['platform'], self.stats['os_name'], self.stats['os_version']
)
else:
try:
msg = ' ({} {} {})'.format(self.stats['os_name'], self.stats['os_version'], self.stats['platform'])
except Exception:
msg = ' ({})'.format(self.stats['os_name'])
ret.append(self.curse_add_line(msg, optional=True))
# Return the message with decoration
return ret

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Uptime plugin."""
from datetime import datetime, timedelta
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# SNMP OID
snmp_oid = {'_uptime': '1.3.6.1.2.1.1.3.0'}
class PluginModel(GlancesPluginModel):
"""Glances uptime plugin.
stats is date (string)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'right'
# Init the stats
self.uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time())
def get_export(self):
"""Overwrite the default export method.
Export uptime in seconds.
"""
# Convert the delta time to seconds (with cast)
# Correct issue #1092 (thanks to @IanTAtWork)
return {'seconds': int(self.uptime.total_seconds())}
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update uptime stat using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
self.uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time())
# Convert uptime to string (because datetime is not JSONifi)
stats = str(self.uptime).split('.')[0]
elif self.input_method == 'snmp':
# Update stats using SNMP
uptime = self.get_stats_snmp(snmp_oid=snmp_oid)['_uptime']
try:
# In hundredths of seconds
stats = str(timedelta(seconds=int(uptime) / 100))
except Exception:
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
ret = [self.curse_add_line('Uptime: {}'.format(self.stats))]
return ret

View File

@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Uptime plugin."""
from datetime import datetime, timedelta
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# SNMP OID
snmp_oid = {'_uptime': '1.3.6.1.2.1.1.3.0'}
class PluginModel(GlancesPluginModel):
"""Glances uptime plugin.
stats is date (string)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'right'
# Init the stats
self.uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time())
def get_export(self):
"""Overwrite the default export method.
Export uptime in seconds.
"""
# Convert the delta time to seconds (with cast)
# Correct issue #1092 (thanks to @IanTAtWork)
return {'seconds': int(self.uptime.total_seconds())}
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update uptime stat using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
self.uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time())
# Convert uptime to string (because datetime is not JSONifi)
stats = str(self.uptime).split('.')[0]
elif self.input_method == 'snmp':
# Update stats using SNMP
uptime = self.get_stats_snmp(snmp_oid=snmp_oid)['_uptime']
try:
# In hundredths of seconds
stats = str(timedelta(seconds=int(uptime) / 100))
except Exception:
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
ret = [self.curse_add_line('Uptime: {}'.format(self.stats))]
return ret

View File

@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Wifi plugin.
Stats are retreived from the nmcli command line (Linux only):
# nmcli -t -f active,ssid,signal,security,chan dev wifi
# nmcli -t -f active,ssid,signal dev wifi
no:Livebox-C820:77
yes:Livebox-C820:72
or the /proc/net/wireless file (Linux only):
# cat /proc/net/wireless
Inter-| sta-| Quality | Discarded packets | Missed | WE
face | tus | link level noise | nwid crypt frag retry misc | beacon | 22
wlp2s0: 0000 51. -59. -256 0 0 0 0 5881 0
"""
import operator
from shutil import which
import threading
import time
from glances.globals import nativestr, file_exists
from glances.plugins.plugin.model import GlancesPluginModel
from glances.secure import secure_popen
from glances.logger import logger
# Test if the nmcli command exists and is executable
# it allows to get the list of the available hotspots
NMCLI_COMMAND = which('nmcli')
NMCLI_ARGS = '-t -f active,ssid,signal,security dev wifi'
nmcli_command_exists = NMCLI_COMMAND is not None
# Backup solution is to use the /proc/net/wireless file
# but it only give signal information about the current hotspot
WIRELESS_FILE = '/proc/net/wireless'
wireless_file_exists = file_exists(WIRELESS_FILE)
if not nmcli_command_exists and not wireless_file_exists:
logger.debug("Wifi plugin is disabled (no %s command or %s file found)" % ('nmcli', WIRELESS_FILE))
class PluginModel(GlancesPluginModel):
"""Glances Wifi plugin.
Get stats of the current Wifi hotspots.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Global Thread running all the scans
self._thread = None
def exit(self):
"""Overwrite the exit method to close threads."""
if self._thread is not None:
self._thread.stop()
# Call the father class
super(PluginModel, self).exit()
def get_key(self):
"""Return the key of the list.
:returns: string -- SSID is the dict key
"""
return 'ssid'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update Wifi stats using the input method.
Stats is a list of dict (one dict per hotspot)
:returns: list -- Stats is a list of dict (hotspot)
"""
# Init new stats
stats = self.get_init_value()
# Exist if we can not grab the stats
if not nmcli_command_exists and not wireless_file_exists:
return stats
if self.input_method == 'local' and nmcli_command_exists:
# Only refresh if there is not other scanning thread
if self._thread is None:
thread_is_running = False
else:
thread_is_running = self._thread.is_alive()
if not thread_is_running:
# Run hotspot scanner thanks to the nmcli command
self._thread = ThreadHotspot(self.get_refresh_time())
self._thread.start()
# Get the result (or [] if the scan is ongoing)
stats = self._thread.stats
elif self.input_method == 'local' and wireless_file_exists:
# As a backup solution, use the /proc/net/wireless file
with open(WIRELESS_FILE, 'r') as f:
# The first two lines are header
f.readline()
f.readline()
# Others lines are Wifi stats
wifi_stats = f.readline()
while wifi_stats != '':
# Extract the stats
wifi_stats = wifi_stats.split()
# Add the Wifi link to the list
stats.append(
{
'key': self.get_key(),
'ssid': wifi_stats[0][:-1],
'signal': float(wifi_stats[3]),
'security': '',
}
)
# Next line
wifi_stats = f.readline()
elif self.input_method == 'snmp':
# Update stats using SNMP
# Not implemented yet
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, value):
"""Overwrite the default get_alert method.
Alert is on signal quality where lower is better...
:returns: string -- Signal alert
"""
ret = 'OK'
try:
if value <= self.get_limit('critical', stat_name=self.plugin_name):
ret = 'CRITICAL'
elif value <= self.get_limit('warning', stat_name=self.plugin_name):
ret = 'WARNING'
elif value <= self.get_limit('careful', stat_name=self.plugin_name):
ret = 'CAREFUL'
except (TypeError, KeyError):
# Catch TypeError for issue1373
ret = 'DEFAULT'
return ret
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert on signal thresholds
for i in self.stats:
self.views[i[self.get_key()]]['signal']['decoration'] = self.get_alert(i['signal'])
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or not wireless_file_exists or self.is_disabled():
return ret
# Max size for the interface name
if_name_max_width = max_width - 5
# Build the string message
# Header
msg = '{:{width}}'.format('WIFI', width=if_name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
msg = '{:>7}'.format('dBm')
ret.append(self.curse_add_line(msg))
# Hotspot list (sorted by name)
for i in sorted(self.stats, key=operator.itemgetter(self.get_key())):
# Do not display hotspot with no name (/ssid)...
# of ssid/signal None... See issue #1151 and #issue1973
if i['ssid'] == '' or i['ssid'] is None or i['signal'] is None:
continue
ret.append(self.curse_new_line())
# New hotspot
hotspot_name = i['ssid']
# Cut hotspot_name if it is too long
if len(hotspot_name) > if_name_max_width:
hotspot_name = '_' + hotspot_name[-if_name_max_width - len(i['security']) + 1 :]
# Add the new hotspot to the message
msg = '{:{width}} {security}'.format(
nativestr(hotspot_name), width=if_name_max_width - len(i['security']) - 1, security=i['security']
)
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format(
i['signal'],
)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='signal', option='decoration'))
)
return ret
class ThreadHotspot(threading.Thread):
"""
Specific thread for the Wifi hotspot scanner.
"""
def __init__(self, refresh_time=2):
"""Init the class."""
super(ThreadHotspot, self).__init__()
# Refresh time
self.refresh_time = refresh_time
# Event needed to stop properly the thread
self._stopper = threading.Event()
# Is part of Ports plugin
self.plugin_name = "wifi"
def run(self):
"""Get hotspots stats using the nmcli command line"""
while not self.stopped():
# Run the nmcli command
nmcli_raw = secure_popen(NMCLI_COMMAND + ' ' + NMCLI_ARGS).split('\n')
nmcli_result = []
for h in nmcli_raw:
h = h.split(':')
if len(h) != 4 or h[0] != 'yes':
# Do not process the line if it is not the active hotspot
continue
nmcli_result.append({'key': 'ssid', 'ssid': h[1], 'signal': -float(h[2]), 'security': h[3]})
self.thread_stats = nmcli_result
# Wait refresh time until next scan
# Note: nmcli cache the result for x seconds
time.sleep(self.refresh_time)
@property
def stats(self):
"""Stats getter."""
if hasattr(self, 'thread_stats'):
return self.thread_stats
else:
return []
def stop(self, timeout=None):
"""Stop the thread."""
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()

View File

@ -1,268 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Wifi plugin.
Stats are retreived from the nmcli command line (Linux only):
# nmcli -t -f active,ssid,signal,security,chan dev wifi
# nmcli -t -f active,ssid,signal dev wifi
no:Livebox-C820:77
yes:Livebox-C820:72
or the /proc/net/wireless file (Linux only):
# cat /proc/net/wireless
Inter-| sta-| Quality | Discarded packets | Missed | WE
face | tus | link level noise | nwid crypt frag retry misc | beacon | 22
wlp2s0: 0000 51. -59. -256 0 0 0 0 5881 0
"""
import operator
from shutil import which
import threading
import time
from glances.globals import nativestr, file_exists
from glances.plugins.plugin.model import GlancesPluginModel
from glances.secure import secure_popen
from glances.logger import logger
# Test if the nmcli command exists and is executable
# it allows to get the list of the available hotspots
NMCLI_COMMAND = which('nmcli')
NMCLI_ARGS = '-t -f active,ssid,signal,security dev wifi'
nmcli_command_exists = NMCLI_COMMAND is not None
# Backup solution is to use the /proc/net/wireless file
# but it only give signal information about the current hotspot
WIRELESS_FILE = '/proc/net/wireless'
wireless_file_exists = file_exists(WIRELESS_FILE)
if not nmcli_command_exists and not wireless_file_exists:
logger.debug("Wifi plugin is disabled (no %s command or %s file found)" % ('nmcli', WIRELESS_FILE))
class PluginModel(GlancesPluginModel):
"""Glances Wifi plugin.
Get stats of the current Wifi hotspots.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Global Thread running all the scans
self._thread = None
def exit(self):
"""Overwrite the exit method to close threads."""
if self._thread is not None:
self._thread.stop()
# Call the father class
super(PluginModel, self).exit()
def get_key(self):
"""Return the key of the list.
:returns: string -- SSID is the dict key
"""
return 'ssid'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update Wifi stats using the input method.
Stats is a list of dict (one dict per hotspot)
:returns: list -- Stats is a list of dict (hotspot)
"""
# Init new stats
stats = self.get_init_value()
# Exist if we can not grab the stats
if not nmcli_command_exists and not wireless_file_exists:
return stats
if self.input_method == 'local' and nmcli_command_exists:
# Only refresh if there is not other scanning thread
if self._thread is None:
thread_is_running = False
else:
thread_is_running = self._thread.is_alive()
if not thread_is_running:
# Run hotspot scanner thanks to the nmcli command
self._thread = ThreadHotspot(self.get_refresh_time())
self._thread.start()
# Get the result (or [] if the scan is ongoing)
stats = self._thread.stats
elif self.input_method == 'local' and wireless_file_exists:
# As a backup solution, use the /proc/net/wireless file
with open(WIRELESS_FILE, 'r') as f:
# The first two lines are header
f.readline()
f.readline()
# Others lines are Wifi stats
wifi_stats = f.readline()
while wifi_stats != '':
# Extract the stats
wifi_stats = wifi_stats.split()
# Add the Wifi link to the list
stats.append(
{
'key': self.get_key(),
'ssid': wifi_stats[0][:-1],
'signal': float(wifi_stats[3]),
'security': '',
}
)
# Next line
wifi_stats = f.readline()
elif self.input_method == 'snmp':
# Update stats using SNMP
# Not implemented yet
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, value):
"""Overwrite the default get_alert method.
Alert is on signal quality where lower is better...
:returns: string -- Signal alert
"""
ret = 'OK'
try:
if value <= self.get_limit('critical', stat_name=self.plugin_name):
ret = 'CRITICAL'
elif value <= self.get_limit('warning', stat_name=self.plugin_name):
ret = 'WARNING'
elif value <= self.get_limit('careful', stat_name=self.plugin_name):
ret = 'CAREFUL'
except (TypeError, KeyError):
# Catch TypeError for issue1373
ret = 'DEFAULT'
return ret
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert on signal thresholds
for i in self.stats:
self.views[i[self.get_key()]]['signal']['decoration'] = self.get_alert(i['signal'])
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or not wireless_file_exists or self.is_disabled():
return ret
# Max size for the interface name
if_name_max_width = max_width - 5
# Build the string message
# Header
msg = '{:{width}}'.format('WIFI', width=if_name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
msg = '{:>7}'.format('dBm')
ret.append(self.curse_add_line(msg))
# Hotspot list (sorted by name)
for i in sorted(self.stats, key=operator.itemgetter(self.get_key())):
# Do not display hotspot with no name (/ssid)...
# of ssid/signal None... See issue #1151 and #issue1973
if i['ssid'] == '' or i['ssid'] is None or i['signal'] is None:
continue
ret.append(self.curse_new_line())
# New hotspot
hotspot_name = i['ssid']
# Cut hotspot_name if it is too long
if len(hotspot_name) > if_name_max_width:
hotspot_name = '_' + hotspot_name[-if_name_max_width - len(i['security']) + 1 :]
# Add the new hotspot to the message
msg = '{:{width}} {security}'.format(
nativestr(hotspot_name), width=if_name_max_width - len(i['security']) - 1, security=i['security']
)
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format(
i['signal'],
)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='signal', option='decoration'))
)
return ret
class ThreadHotspot(threading.Thread):
"""
Specific thread for the Wifi hotspot scanner.
"""
def __init__(self, refresh_time=2):
"""Init the class."""
super(ThreadHotspot, self).__init__()
# Refresh time
self.refresh_time = refresh_time
# Event needed to stop properly the thread
self._stopper = threading.Event()
# Is part of Ports plugin
self.plugin_name = "wifi"
def run(self):
"""Get hotspots stats using the nmcli command line"""
while not self.stopped():
# Run the nmcli command
nmcli_raw = secure_popen(NMCLI_COMMAND + ' ' + NMCLI_ARGS).split('\n')
nmcli_result = []
for h in nmcli_raw:
h = h.split(':')
if len(h) != 4 or h[0] != 'yes':
# Do not process the line if it is not the active hotspot
continue
nmcli_result.append({'key': 'ssid', 'ssid': h[1], 'signal': -float(h[2]), 'security': h[3]})
self.thread_stats = nmcli_result
# Wait refresh time until next scan
# Note: nmcli cache the result for x seconds
time.sleep(self.refresh_time)
@property
def stats(self):
"""Stats getter."""
if hasattr(self, 'thread_stats'):
return self.thread_stats
else:
return []
def stop(self, timeout=None):
"""Stop the thread."""
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()

View File

@ -95,38 +95,29 @@ class GlancesStats(object):
def _load_plugin(self, plugin_path, args=None, config=None):
"""Load the plugin, init it and add to the _plugin dict."""
# The key is the plugin_path
# except when it starts with glances_xxx
# generate self._plugins_list["xxx"] = <instance of xxx Plugin>
if plugin_path.startswith('glances_'):
# Avoid circular loop when Glances plugin uses lib with same name
# Example: docker should be named to glances_docker
name = plugin_path.split('glances_')[1]
else:
name = plugin_path
# Load the plugin class
try:
# Import the plugin
plugin = import_module('glances.plugins.' + plugin_path + '.model')
plugin = import_module('glances.plugins.' + plugin_path)
# Init and add the plugin to the dictionary
self._plugins[name] = plugin.PluginModel(args=args, config=config)
self._plugins[plugin_path] = plugin.PluginModel(args=args, config=config)
except Exception as e:
# If a plugin can not be loaded, display a critical message
# on the console but do not crash
logger.critical("Error while initializing the {} plugin ({})".format(name, e))
logger.critical("Error while initializing the {} plugin ({})".format(plugin_path, e))
logger.error(traceback.format_exc())
# An error occurred, disable the plugin
if args is not None:
setattr(args, 'disable_' + name, False)
setattr(args, 'disable_' + plugin_path, False)
else:
# Manage the default status of the plugin (enable or disable)
if args is not None:
# If the all key is set in the disable_plugin option then look in the enable_plugin option
# If the all keys are set in the disable_plugin option then look in the enable_plugin option
if getattr(args, 'disable_all', False):
logger.debug('%s => %s', name, getattr(args, 'enable_' + name, False))
setattr(args, 'disable_' + name, not getattr(args, 'enable_' + name, False))
logger.debug('%s => %s', plugin_path, getattr(args, 'enable_' + plugin_path, False))
setattr(args, 'disable_' + plugin_path, not getattr(args, 'enable_' + plugin_path, False))
else:
setattr(args, 'disable_' + name, getattr(args, 'disable_' + name, False))
setattr(args, 'disable_' + plugin_path, getattr(args, 'disable_' + plugin_path, False))
def load_plugins(self, args=None):
"""Load all plugins in the 'plugins' folder."""