summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md7
-rw-r--r--conf.d/Makefile.am2
-rw-r--r--conf.d/health.d/megacli.conf48
-rw-r--r--conf.d/python.d/megacli.conf68
-rw-r--r--configs.signatures3
-rwxr-xr-xnetdata-installer.sh1
-rw-r--r--python.d/Makefile.am1
-rw-r--r--python.d/README.md29
-rw-r--r--python.d/megacli.chart.py280
-rw-r--r--python.d/python_modules/bases/FrameworkServices/ExecutableService.py6
-rw-r--r--python.d/python_modules/bases/loaders.py12
11 files changed, 452 insertions, 5 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 31d74c06a1..72c5fe5be5 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -65,11 +65,13 @@ This is the list of contributors that have signed this agreement:
username|name|email (optional)
:--------:|:----:|:----------------
+@lets00|Luís Eduardo|leduardo@lsd.ufcg.edu.br
@ktsaou|Costa Tsaousis|costa@tsaousis.gr
@tycho|Steven Noonan|steven@uplinklabs.net
@philwhineray|Phil Whineray|
@paulfantom|Pawel Krupa|pawel@krupa.net.pl
@Ferroin|Austin S. Hemmelgarn|ahferroin7@gmail.com
+@glensc|Elan Ruusamäe|
@l2isbad|Ilya Mashchenko|ilyamaschenko@gmail.com
@rlefevre|Rémi Lefèvre|
@vlvkobal|Vladimir Kobal|vlad@prokk.net
@@ -94,3 +96,8 @@ username|name|email (optional)
@Flums|Philip Gabrielsen|philip@digno.no
@domschl|Dominik Schlösser|dominik.schloesser@gmail.com
@tioumen|Guillaume Hospital|
+@arch273|Jacob Ayres
+@x4FF3|David Fuellgraf|
+@jasonwbarnett|Jason Barnett|
+@ecowed|Ed Wade|
+@wungad|Rob Man|
diff --git a/conf.d/Makefile.am b/conf.d/Makefile.am
index 7bd3622f65..0026e3f130 100644
--- a/conf.d/Makefile.am
+++ b/conf.d/Makefile.am
@@ -53,6 +53,7 @@ dist_pythonconfig_DATA = \
python.d/litespeed.conf \
python.d/logind.conf \
python.d/mdstat.conf \
+ python.d/megacli.conf \
python.d/memcached.conf \
python.d/mongodb.conf \
python.d/mysql.conf \
@@ -111,6 +112,7 @@ dist_healthconfig_DATA = \
health.d/isc_dhcpd.conf \
health.d/lighttpd.conf \
health.d/mdstat.conf \
+ health.d/megacli.conf \
health.d/memcached.conf \
health.d/memory.conf \
health.d/mongodb.conf \
diff --git a/conf.d/health.d/megacli.conf b/conf.d/health.d/megacli.conf
new file mode 100644
index 0000000000..1881a7be14
--- /dev/null
+++ b/conf.d/health.d/megacli.conf
@@ -0,0 +1,48 @@
+ alarm: adapter_state
+ on: megacli.adapter_degraded
+ units: is degraded
+ lookup: sum -10s
+ every: 10s
+ crit: $this > 0
+ info: adapter state
+ to: sysadmin
+
+ template: bbu_relative_charge
+ on: megacli.bbu_relative_charge
+ units: percent
+ lookup: average -10s
+ every: 10s
+ warn: $this <= (($status >= $WARNING) ? (85) : (80))
+ crit: $this <= (($status == $CRITICAL) ? (50) : (40))
+ info: BBU relative state of charge
+ to: sysadmin
+
+ template: bbu_cycle_count
+ on: megacli.bbu_cycle_count
+ units: cycle count
+ lookup: average -10s
+ every: 10s
+ warn: $this >= 100
+ crit: $this >= 500
+ info: BBU cycle count
+ to: sysadmin
+
+ alarm: pd_media_errors
+ on: megacli.pd_media_error
+ units: media errors
+ lookup: sum -10s
+ every: 10s
+ warn: $this > 0
+ delay: down 1m multiplier 2 max 10m
+ info: physical drive media errors
+ to: sysadmin
+
+ alarm: pd_predictive_failures
+ on: megacli.pd_predictive_failure
+ units: predictive failures
+ lookup: sum -10s
+ every: 10s
+ warn: $this > 0
+ delay: down 1m multiplier 2 max 10m
+ info: physical drive predictive failures
+ to: sysadmin
diff --git a/conf.d/python.d/megacli.conf b/conf.d/python.d/megacli.conf
new file mode 100644
index 0000000000..d84078ecb1
--- /dev/null
+++ b/conf.d/python.d/megacli.conf
@@ -0,0 +1,68 @@
+# netdata python.d.plugin configuration for megacli
+#
+# This file is in YaML format. Generally the format is:
+#
+# name: value
+#
+
+# ----------------------------------------------------------------------
+# Global Variables
+# These variables set the defaults for all JOBs, however each JOB
+# may define its own, overriding the defaults.
+
+# update_every sets the default data collection frequency.
+# If unset, the python.d.plugin default is used.
+# update_every: 1
+
+# priority controls the order of charts at the netdata dashboard.
+# Lower numbers move the charts towards the top of the page.
+# If unset, the default for python.d.plugin is used.
+# priority: 60000
+
+# retries sets the number of retries to be made in case of failures.
+# If unset, the default for python.d.plugin is used.
+# Attempts to restore the service are made once every update_every
+# and only if the module has collected values in the past.
+# retries: 60
+
+# autodetection_retry sets the job re-check interval in seconds.
+# The job is not deleted if check fails.
+# Attempts to start the job are made once every autodetection_retry.
+# This feature is disabled by default.
+# autodetection_retry: 0
+
+# ----------------------------------------------------------------------
+# JOBS (data collection sources)
+#
+# The default JOBS share the same *name*. JOBS with the same name
+# are mutually exclusive. Only one of them will be allowed running at
+# any time. This allows autodetection to try several alternatives and
+# pick the one that works.
+#
+# Any number of jobs is supported.
+#
+# All python.d.plugin JOBS (for all its modules) support a set of
+# predefined parameters. These are:
+#
+# job_name:
+# name: myname # the JOB's name as it will appear at the
+# # dashboard (by default is the job_name)
+# # JOBs sharing a name are mutually exclusive
+# update_every: 1 # the JOB's data collection frequency
+# priority: 60000 # the JOB's order on the dashboard
+# retries: 60 # the JOB's number of restoration attempts
+# autodetection_retry: 0 # the JOB's re-check interval in seconds
+#
+# Additionally to the above, megacli also supports the following:
+#
+# do_battery: yes/no # default is no. Battery stats (adds additional call to megacli `megacli -AdpBbuCmd -a0`).
+#
+# ----------------------------------------------------------------------
+
+# IMPORTANT
+# The netdata user needs to be able to be able to sudo the megacli program without password:
+# netdata ALL=(root) NOPASSWD: /path/to/megacli
+
+
+# uncomment the line below to collect battery statistics
+# do_battery: yes
diff --git a/configs.signatures b/configs.signatures
index 04b2a79205..9f1dd24441 100644
--- a/configs.signatures
+++ b/configs.signatures
@@ -129,6 +129,7 @@ declare -A configs_signatures=(
['2da0a2e7117292ece11d69723a294bd7']='python.d/mongodb.conf'
['2ee5df033fe9c65a45566b6760b856e3']='python.d/web_log.conf'
['2f05e09b69ea20cda56d8f8b6fd3e86d']='health.d/couchdb.conf'
+ ['2f13a6b7d11eda826ff26569b2a77080']='health.d/apcupsd.conf'
['2f3a8e33df83f14e0af8ca2465697215']='python.d/exim.conf'
['2f4a85fedecce1bf425fa1039f6b021e']='apps_groups.conf'
['2fa8fb929fd597f2ab97b6efc540a043']='health_alarm_notify.conf'
@@ -251,6 +252,7 @@ declare -A configs_signatures=(
['579c7c41c756745f57afd11c94a879d7']='python.d.conf'
['57be306944cb09b7f024079728fd04b9']='apps_groups.conf'
['5829812db29598db5857c9f433e96fef']='python.d/apache.conf'
+ ['58439d9c1253e33c74b399e6853143ab']='health.d/apcupsd.conf'
['5855dd70d71c8497e5591b0690162c9c']='health.d/tcp_resets.conf'
['58660dfcc260f77deec94b328b3838e8']='health_alarm_notify.conf'
['58e835b7176865ec5a6f59f7aba832bf']='health.d/named.conf'
@@ -386,6 +388,7 @@ declare -A configs_signatures=(
['87615ae5ac2412d853c717383fa53781']='python.d/chrony.conf'
['87642c568093daf3b2c30c5beffe2225']='python.d/elasticsearch.conf'
['8810140ce9c09af1d18b9602c4003904']='health_alarm_notify.conf'
+ ['886975ecb9a4e856151dc71024b122e6']='health.d/apcupsd.conf'
['8891fb423f6b987281d7913bb6c1c024']='health.d/ipc.conf'
['88e3b51b6b3fe8f317df82a2d4fbb990']='python.d.conf'
['88f77865f75c9fb61c97d700bd4561ee']='python.d/mysql.conf'
diff --git a/netdata-installer.sh b/netdata-installer.sh
index 59436a2104..1c77e8f86a 100755
--- a/netdata-installer.sh
+++ b/netdata-installer.sh
@@ -80,6 +80,7 @@ REINSTALL_COMMAND="$(printf "%q " "$0" "${@}"; printf "\n")"
# remove options that shown not be inherited by netdata-updater.sh
REINSTALL_COMMAND="${REINSTALL_COMMAND// --dont-wait/}"
REINSTALL_COMMAND="${REINSTALL_COMMAND// --dont-start-it/}"
+[ "${REINSTALL_COMMAND:0:1}" != "." -a "${REINSTALL_COMMAND:0:1}" != "/" -a -f "./${0}" ] && REINSTALL_COMMAND="./${REINSTALL_COMMAND}"
setcap="$(which setcap 2>/dev/null || command -v setcap 2>/dev/null)"
diff --git a/python.d/Makefile.am b/python.d/Makefile.am
index caf4a6210b..43fcef6c69 100644
--- a/python.d/Makefile.am
+++ b/python.d/Makefile.am
@@ -41,6 +41,7 @@ dist_python_DATA = \
litespeed.chart.py \
logind.chart.py \
mdstat.chart.py \
+ megacli.chart.py \
memcached.chart.py \
mongodb.chart.py \
mysql.chart.py \
diff --git a/python.d/README.md b/python.d/README.md
index 2417fc4302..3a246cfbd1 100644
--- a/python.d/README.md
+++ b/python.d/README.md
@@ -1141,6 +1141,35 @@ No configuration is needed.
---
+# megacli
+
+Module collects adapter, physical drives and battery stats.
+
+**Requirements:**
+ * `netdata` user needs to be able to be able to sudo the `megacli` program without password
+
+To grab stats it executes:
+ * `sudo -n megacli -LDPDInfo -aAll`
+ * `sudo -n megacli -AdpBbuCmd -a0`
+
+
+It produces:
+
+1. **Adapter State**
+
+2. **Physical Drives Media Errors**
+
+3. **Physical Drives Predictive Failures**
+
+4. **Battery Relative State of Charge**
+
+5. **Battery Cycle Count**
+
+### configuration
+Battery stats disabled by default in the module configuration file.
+
+---
+
# memcached
Memcached monitoring module. Data grabbed from [stats interface](https://github.com/memcached/memcached/wiki/Commands#stats).
diff --git a/python.d/megacli.chart.py b/python.d/megacli.chart.py
new file mode 100644
index 0000000000..594c4fb17b
--- /dev/null
+++ b/python.d/megacli.chart.py
@@ -0,0 +1,280 @@
+# -*- coding: utf-8 -*-
+# Description: megacli netdata python.d module
+# Author: Ilya Mashchenko (l2isbad)
+# SPDX-License-Identifier: GPL-3.0+
+
+
+import re
+
+from bases.FrameworkServices.ExecutableService import ExecutableService
+from bases.collection import find_binary
+
+
+update_every = 5
+
+
+def adapter_charts(ads):
+ order = [
+ 'adapter_degraded',
+ ]
+
+ def dims(ad):
+ return [['adapter_{0}_degraded'.format(a.id), 'adapter {0}'.format(a.id)] for a in ad]
+
+ charts = {
+ 'adapter_degraded': {
+ 'options': [
+ None, 'Adapter State', 'is degraded', 'adapter', 'megacli.adapter_degraded', 'line'],
+ 'lines': dims(ads)
+ },
+ }
+
+ return order, charts
+
+
+def pd_charts(pds):
+ order = [
+ 'pd_media_error',
+ 'pd_predictive_failure',
+ ]
+
+ def dims(k, pd):
+ return [['slot_{0}_{1}'.format(p.id, k), 'slot {0}'.format(p.id), 'incremental'] for p in pd]
+
+ charts = {
+ 'pd_media_error': {
+ 'options': [
+ None, 'Physical Drives Media Errors', 'errors/s', 'pd', 'megacli.pd_media_error', 'line'
+ ],
+ 'lines': dims("media_error", pds)},
+ 'pd_predictive_failure': {
+ 'options': [
+ None, 'Physical Drives Predictive Failures', 'failures/s', 'pd', 'megacli.pd_predictive_failure', 'line'
+ ],
+ 'lines': dims("predictive_failure", pds)}
+ }
+
+ return order, charts
+
+
+def battery_charts(bats):
+ order = list()
+ charts = dict()
+
+ for b in bats:
+ order.append('bbu_{0}_relative_charge'.format(b.id))
+ charts.update(
+ {
+ 'bbu_{0}_relative_charge'.format(b.id): {
+ 'options': [
+ None, 'Relative State of Charge', '%', 'battery', 'megacli.bbu_relative_charge', 'line'],
+ 'lines': [
+ ['bbu_{0}_relative_charge'.format(b.id), 'adapter {0}'.format(b.id)],
+ ]
+ }
+ }
+ )
+
+ for b in bats:
+ order.append('bbu_{0}_cycle_count'.format(b.id))
+ charts.update(
+ {
+ 'bbu_{0}_cycle_count'.format(b.id): {
+ 'options': [
+ None, 'Cycle Count', 'cycle count', 'battery', 'megacli.bbu_cycle_count', 'line'],
+ 'lines': [
+ ['bbu_{0}_cycle_count'.format(b.id), 'adapter {0}'.format(b.id)],
+ ]
+ }
+ }
+ )
+
+ return order, charts
+
+
+RE_ADAPTER = re.compile(
+ r'Adapter #([0-9]+) State\s+: ([a-zA-Z]+)'
+)
+
+RE_VD = re.compile(
+ r'Slot Number: ([0-9]+) Media Error Count: ([0-9]+) Predictive Failure Count: ([0-9]+)'
+)
+
+RE_BATTERY = re.compile(
+ r'BBU Capacity Info for Adapter: ([0-9]+) Relative State of Charge: ([0-9]+) % Cycle Count: ([0-9]+)'
+)
+
+
+def find_adapters(d):
+ keys = ("Adapter #", "State")
+ d = ' '.join(v.strip() for v in d if v.startswith(keys))
+ return [Adapter(*v) for v in RE_ADAPTER.findall(d)]
+
+
+def find_pds(d):
+ keys = ("Slot Number", "Media Error Count", "Predictive Failure Count")
+ d = ' '.join(v.strip() for v in d if v.startswith(keys))
+ return [PD(*v) for v in RE_VD.findall(d)]
+
+
+def find_batteries(d):
+ keys = ('BBU Capacity Info for Adapter', 'Relative State of Charge', 'Cycle Count')
+ d = ' '.join(v.strip() for v in d if v.strip().startswith(keys))
+ return [Battery(*v) for v in RE_BATTERY.findall(d)]
+
+
+class Adapter:
+ def __init__(self, n, state):
+ self.id = n
+ self.state = int(state == 'Degraded')
+
+ def data(self):
+ return {
+ 'adapter_{0}_degraded'.format(self.id): self.state,
+ }
+
+
+class PD:
+ def __init__(self, n, media_err, predict_fail):
+ self.id = n
+ self.media_err = media_err
+ self.predict_fail = predict_fail
+
+ def data(self):
+ return {
+ 'slot_{0}_media_error'.format(self.id): self.media_err,
+ 'slot_{0}_predictive_failure'.format(self.id): self.predict_fail,
+ }
+
+
+class Battery:
+ def __init__(self, adapt_id, rel_charge, cycle_count):
+ self.id = adapt_id
+ self.rel_charge = rel_charge
+ self.cycle_count = cycle_count
+
+ def data(self):
+ return {
+ 'bbu_{0}_relative_charge'.format(self.id): self.rel_charge,
+ 'bbu_{0}_cycle_count'.format(self.id): self.cycle_count,
+ }
+
+
+# TODO: hardcoded sudo...
+class Megacli:
+ def __init__(self):
+ self.s = find_binary('sudo')
+ self.m = find_binary('megacli')
+ self.sudo_check = [self.s, '-n', '-v']
+ self.disk_info = [self.s, '-n', self.m, '-LDPDInfo', '-aAll']
+ self.battery_info = [self.s, '-n', self.m, '-AdpBbuCmd', '-a0']
+
+ def __bool__(self):
+ return bool(self.s and self.m)
+
+ def __nonzero__(self):
+ return self.__bool__()
+
+
+class Service(ExecutableService):
+ def __init__(self, configuration=None, name=None):
+ ExecutableService.__init__(self, configuration=configuration, name=name)
+ self.order = list()
+ self.definitions = dict()
+ self.megacli = Megacli()
+ self.do_battery = self.configuration.get('do_battery')
+
+ def check_sudo(self):
+ err = self._get_raw_data(command=self.megacli.sudo_check, stderr=True)
+ if err:
+ self.error(''.join(err))
+ return False
+ return True
+
+ def check_disk_info(self):
+ d = self._get_raw_data(command=self.megacli.disk_info)
+ if not d:
+ return False
+
+ ads = find_adapters(d)
+ pds = find_pds(d)
+
+ if not (ads and pds):
+ self.error('failed to parse "{0}" output'.format(' '.join(self.megacli.disk_info)))
+ return False
+
+ o, c = adapter_charts(ads)
+ self.order.extend(o)
+ self.definitions.update(c)
+
+ o, c = pd_charts(pds)
+ self.order.extend(o)
+ self.definitions.update(c)
+
+ return True
+
+ def check_battery(self):
+ d = self._get_raw_data(command=self.megacli.battery_info)
+ if not d:
+ return False
+
+ bats = find_batteries(d)
+
+ if not bats:
+ self.error('failed to parse "{0}" output'.format(' '.join(self.megacli.battery_info)))
+ return False
+
+ o, c = battery_charts(bats)
+ self.order.extend(o)
+ self.definitions.update(c)
+ return True
+
+ def check(self):
+ if not self.megacli:
+ self.error('can\'t locate "sudo" or "megacli" binary')
+ return None
+
+ if not (self.check_sudo() and self.check_disk_info()):
+ return False
+
+ if self.do_battery:
+ self.do_battery = self.check_battery()
+
+ return True
+
+ def get_data(self):
+ data = dict()
+
+ data.update(self.get_adapter_pd_data())
+
+ if self.do_battery:
+ data.update(self.get_battery_data())
+
+ return data or None
+
+ def get_adapter_pd_data(self):
+ raw = self._get_raw_data(command=self.megacli.disk_info)
+ data = dict()
+
+ if not raw:
+ return data
+
+ for a in find_adapters(raw):
+ data.update(a.data())
+
+ for p in find_pds(raw):
+ data.update(p.data())
+
+ return data
+
+ def get_battery_data(self):
+ raw = self._get_raw_data(command=self.megacli.battery_info)
+ data = dict()
+
+ if not raw:
+ return data
+
+ for b in find_batteries(raw):
+ data.update(b.data())
+
+ return data
diff --git a/python.d/python_modules/bases/FrameworkServices/ExecutableService.py b/python.d/python_modules/bases/FrameworkServices/ExecutableService.py
index 9b2e945e79..625f643234 100644
--- a/python.d/python_modules/bases/FrameworkServices/ExecutableService.py
+++ b/python.d/python_modules/bases/FrameworkServices/ExecutableService.py
@@ -17,15 +17,15 @@ class ExecutableService(SimpleService):
SimpleService.__init__(self, configuration=configuration, name=name)
self.command = None
- def _get_raw_data(self, stderr=False):
+ def _get_raw_data(self, stderr=False, command=None):
"""
Get raw data from executed command
:return: <list>
"""
try:
- p = Popen(self.command, stdout=PIPE, stderr=PIPE)
+ p = Popen(command if command else self.command, stdout=PIPE, stderr=PIPE)
except Exception as error:
- self.error('Executing command {command} resulted in error: {error}'.format(command=self.command,
+ self.error('Executing command {command} resulted in error: {error}'.format(command=command or self.command,
error=error))
return None
data = list()
diff --git a/python.d/python_modules/bases/loaders.py b/python.d/python_modules/bases/loaders.py
index 6f2cf7bd09..debc69f239 100644
--- a/python.d/python_modules/bases/loaders.py
+++ b/python.d/python_modules/bases/loaders.py
@@ -4,16 +4,24 @@
# SPDX-License-Identifier: GPL-3.0+
import types
+
from sys import version_info
PY_VERSION = version_info[:2]
+try:
+ if PY_VERSION > (3, 1):
+ from pyyaml3 import SafeLoader as YamlSafeLoader
+ else:
+ from pyyaml2 import SafeLoader as YamlSafeLoader
+except ImportError:
+ from yaml import SafeLoader as YamlSafeLoader
+
+
if PY_VERSION > (3, 1):
- from pyyaml3 import SafeLoader as YamlSafeLoader
from importlib.machinery import SourceFileLoader
DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map'
else:
- from pyyaml2 import SafeLoader as YamlSafeLoader
from imp import load_source as SourceFileLoader
DEFAULT_MAPPING_TAG = u'tag:yaml.org,2002:map'