mirror of https://github.com/nicolargo/glances.git
Refactor plugin folder in the same way than exports modules
This commit is contained in:
parent
30cc9f3e0e
commit
48251c8271
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue