adaptec_raid python module (#4429)
* adaptec_raid module init version * adaptec_raid minor * adaptec_raid minor * adaptec_raid minor * adaptec_raid arcconf command fix * adaptec_raid minor fixes * adaptec_raid add alarms * adaptec_raid add link to screenshot to the readme
Ilya Mashchenko committed
Oct 23, 2018 at 20:48 UTC
b85833f08135bb7b3c2a0001dac8742d1d62db91
7 files changed
+370
collectors/python.d.plugin/Makefile.am
+1
@@ -37,6 +37,7 @@ dist_pythonconfig_DATA = \
37
$(top_srcdir)/installer/.keep \
38
$(NULL)
39
40
+include adaptec_raid/Makefile.inc
41
include apache/Makefile.inc
42
include beanstalk/Makefile.inc
43
include bind_rndc/Makefile.inc
collectors/python.d.plugin/adaptec_raid/Makefile.inc
new
+13
@@ -0,0 +1,13 @@
1
+# SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+# THIS IS NOT A COMPLETE Makefile
4
+# IT IS INCLUDED BY ITS PARENT'S Makefile.am
5
+# IT IS REQUIRED TO REFERENCE ALL FILES RELATIVE TO THE PARENT
6
+
7
+# install these files
8
+dist_python_DATA += adaptec_raid/adaptec_raid.chart.py
9
+dist_pythonconfig_DATA += adaptec_raid/adaptec_raid.conf
10
+
11
+# do not install these files, but include them in the distribution
12
+dist_noinst_DATA += adaptec_raid/README.md adaptec_raid/Makefile.inc
13
+
collectors/python.d.plugin/adaptec_raid/README.md
new
+27
@@ -0,0 +1,27 @@
1
+# adaptec raid
2
+
3
+Module collects logical and physical devices health metrics.
4
+
5
+**Requirements:**
6
+ * `netdata` user needs to be able to sudo the `arcconf` program without password
7
+
8
+To grab stats it executes:
9
+ * `sudo -n arcconf GETCONFIG 1 LD`
10
+ * `sudo -n arcconf GETCONFIG 1 PD`
11
+
12
+
13
+It produces:
14
+
15
+1. **Logical Device Status**
16
+
17
+2. **Physical Device State**
18
+
19
+3. **Physical Device S.M.A.R.T warnings**
20
+
21
+4. **Physical Device Temperature**
22
+
23
+Screenshot:
24
+
25
+
26
+
27
+---
collectors/python.d.plugin/adaptec_raid/adaptec_raid.chart.py
new
+245
@@ -0,0 +1,245 @@
1
+# -*- coding: utf-8 -*-
2
+# Description: adaptec_raid netdata python.d module
3
+# Author: Ilya Mashchenko (l2isbad)
4
+# SPDX-License-Identifier: GPL-3.0-or-later
5
+
6
+
7
+import re
8
+
9
+from copy import deepcopy
10
+
11
+from bases.FrameworkServices.ExecutableService import ExecutableService
12
+from bases.collection import find_binary
13
+
14
+
15
+update_every = 5
16
+
17
+ORDER = [
18
+ 'ld_status',
19
+ 'pd_state',
20
+ 'pd_smart_warnings',
21
+ 'pd_temperature',
22
+]
23
+
24
+CHARTS = {
25
+ 'ld_status': {
26
+ 'options': [None, 'Status Is Not OK', 'bool', 'logical devices', 'adapter_raid.ld_status', 'line'],
27
+ 'lines': []
28
+ },
29
+ 'pd_state': {
30
+ 'options': [None, 'State Is Not OK', 'bool', 'physical devices', 'adapter_raid.pd_state', 'line'],
31
+ 'lines': []
32
+ },
33
+ 'pd_smart_warnings': {
34
+ 'options': [None, 'S.M.A.R.T warnings', 'count', 'physical devices',
35
+ 'adapter_raid.smart_warnings', 'line'],
36
+ 'lines': []
37
+ },
38
+ 'pd_temperature': {
39
+ 'options': [None, 'Temperature', 'celsius', 'physical devices', 'adapter_raid.temperature', 'line'],
40
+ 'lines': []
41
+ },
42
+}
43
+
44
+SUDO = 'sudo'
45
+ARCCONF = 'arcconf'
46
+
47
+BAD_LD_STATUS = (
48
+ 'Degraded',
49
+ 'Failed',
50
+)
51
+
52
+GOOD_PD_STATUS = (
53
+ 'Online',
54
+)
55
+
56
+RE_LD = re.compile(
57
+ r'Logical device number\s+([0-9]+).*?'
58
+ r'Status of logical device\s+: ([a-zA-Z]+)'
59
+)
60
+
61
+
62
+def find_lds(d):
63
+ d = ' '.join(v.strip() for v in d)
64
+ return [LD(*v) for v in RE_LD.findall(d)]
65
+
66
+
67
+def find_pds(d):
68
+ pds = list()
69
+ pd = PD()
70
+
71
+ for row in d:
72
+ row = row.strip()
73
+ if row.startswith('Device #'):
74
+ pd = PD()
75
+ pd.id = row.split('#')[-1]
76
+ elif not pd.id:
77
+ continue
78
+
79
+ if row.startswith('State'):
80
+ v = row.split()[-1]
81
+ pd.state = v
82
+ elif row.startswith('S.M.A.R.T. warnings'):
83
+ v = row.split()[-1]
84
+ pd.smart_warnings = v
85
+ elif row.startswith('Temperature'):
86
+ v = row.split(':')[-1].split()[0]
87
+ pd.temperature = v
88
+ elif row.startswith('NCQ status'):
89
+ if pd.id and pd.state and pd.smart_warnings:
90
+ pds.append(pd)
91
+ pd = PD()
92
+
93
+ return pds
94
+
95
+
96
+class LD:
97
+ def __init__(self, ld_id, status):
98
+ self.id = ld_id
99
+ self.status = status
100
+
101
+ def data(self):
102
+ return {
103
+ 'ld_{0}_status'.format(self.id): int(self.status in BAD_LD_STATUS)
104
+ }
105
+
106
+
107
+class PD:
108
+ def __init__(self):
109
+ self.id = None
110
+ self.state = None
111
+ self.smart_warnings = None
112
+ self.temperature = None
113
+
114
+ def data(self):
115
+ data = {
116
+ 'pd_{0}_state'.format(self.id): int(self.state not in GOOD_PD_STATUS),
117
+ 'pd_{0}_smart_warnings'.format(self.id): self.smart_warnings,
118
+ }
119
+ if self.temperature and self.temperature.isdigit():
120
+ data['pd_{0}_temperature'.format(self.id)] = self.temperature
121
+
122
+ return data
123
+
124
+
125
+class Arcconf:
126
+ def __init__(self, arcconf):
127
+ self.arcconf = arcconf
128
+
129
+ def ld_info(self):
130
+ return [self.arcconf, 'GETCONFIG', '1', 'LD']
131
+
132
+ def pd_info(self):
133
+ return [self.arcconf, 'GETCONFIG', '1', 'PD']
134
+
135
+
136
+# TODO: hardcoded sudo...
137
+class SudoArcconf:
138
+ def __init__(self, arcconf, sudo):
139
+ self.arcconf = Arcconf(arcconf)
140
+ self.sudo = sudo
141
+
142
+ def ld_info(self):
143
+ return [self.sudo, '-n'] + self.arcconf.ld_info()
144
+
145
+ def pd_info(self):
146
+ return [self.sudo, '-n'] + self.arcconf.pd_info()
147
+
148
+
149
+class Service(ExecutableService):
150
+ def __init__(self, configuration=None, name=None):
151
+ ExecutableService.__init__(self, configuration=configuration, name=name)
152
+ self.order = ORDER
153
+ self.definitions = deepcopy(CHARTS)
154
+ self.use_sudo = self.configuration.get('use_sudo', True)
155
+ self.arcconf = None
156
+
157
+ def execute(self, command, stderr=False):
158
+ return self._get_raw_data(command=command, stderr=stderr)
159
+
160
+ def check(self):
161
+ sudo = find_binary(SUDO)
162
+ if self.use_sudo:
163
+ if not sudo:
164
+ self.error('can\'t locate "{0}" binary'.format(SUDO))
165
+ return False
166
+ err = self.execute([sudo, '-n', '-v'], True)
167
+ if err:
168
+ self.error(' '.join(err))
169
+ return False
170
+
171
+ arcconf = find_binary(ARCCONF)
172
+ if not arcconf:
173
+ self.error('can\'t locate "{0}" binary'.format(ARCCONF))
174
+ return False
175
+
176
+ if self.use_sudo:
177
+ self.arcconf = SudoArcconf(arcconf, sudo)
178
+ else:
179
+ self.arcconf = Arcconf(arcconf)
180
+
181
+ lds = self.get_lds()
182
+ if not lds:
183
+ return False
184
+
185
+ self.debug('discovered logical devices ids: {0}'.format([ld.id for ld in lds]))
186
+
187
+ pds = self.get_pds()
188
+ if not pds:
189
+ return False
190
+
191
+ self.debug('discovered physical devices ids: {0}'.format([pd.id for pd in pds]))
192
+
193
+ self.update_charts(lds, pds)
194
+ return True
195
+
196
+ def get_data(self):
197
+ data = dict()
198
+
199
+ for ld in self.get_lds():
200
+ data.update(ld.data())
201
+
202
+ for pd in self.get_pds():
203
+ data.update(pd.data())
204
+
205
+ return data
206
+
207
+ def get_lds(self):
208
+ raw_lds = self.execute(self.arcconf.ld_info())
209
+ if not raw_lds:
210
+ return None
211
+
212
+ lds = find_lds(raw_lds)
213
+ if not lds:
214
+ self.error('failed to parse "{0}" output'.format(' '.join(self.arcconf.ld_info())))
215
+ self.debug('output: {0}'.format(raw_lds))
216
+ return None
217
+ return lds
218
+
219
+ def get_pds(self):
220
+ raw_pds = self.execute(self.arcconf.pd_info())
221
+ if not raw_pds:
222
+ return None
223
+
224
+ pds = find_pds(raw_pds)
225
+ if not pds:
226
+ self.error('failed to parse "{0}" output'.format(' '.join(self.arcconf.pd_info())))
227
+ self.debug('output: {0}'.format(raw_pds))
228
+ return None
229
+ return pds
230
+
231
+ def update_charts(self, lds, pds):
232
+ charts = self.definitions
233
+ for ld in lds:
234
+ dim = ['ld_{0}_status'.format(ld.id), 'ld {0}'.format(ld.id)]
235
+ charts['ld_status']['lines'].append(dim)
236
+
237
+ for pd in pds:
238
+ dim = ['pd_{0}_state'.format(pd.id), 'pd {0}'.format(pd.id)]
239
+ charts['pd_state']['lines'].append(dim)
240
+
241
+ dim = ['pd_{0}_smart_warnings'.format(pd.id), 'pd {0}'.format(pd.id)]
242
+ charts['pd_smart_warnings']['lines'].append(dim)
243
+
244
+ dim = ['pd_{0}_temperature'.format(pd.id), 'pd {0}'.format(pd.id)]
245
+ charts['pd_temperature']['lines'].append(dim)
collectors/python.d.plugin/adaptec_raid/adaptec_raid.conf
new
+59
@@ -0,0 +1,59 @@
1
+# netdata python.d.plugin configuration for adaptec raid
2
+#
3
+# This file is in YaML format. Generally the format is:
4
+#
5
+# name: value
6
+#
7
+
8
+# ----------------------------------------------------------------------
9
+# Global Variables
10
+# These variables set the defaults for all JOBs, however each JOB
11
+# may define its own, overriding the defaults.
12
+
13
+# update_every sets the default data collection frequency.
14
+# If unset, the python.d.plugin default is used.
15
+# update_every: 1
16
+
17
+# priority controls the order of charts at the netdata dashboard.
18
+# Lower numbers move the charts towards the top of the page.
19
+# If unset, the default for python.d.plugin is used.
20
+# priority: 60000
21
+
22
+# retries sets the number of retries to be made in case of failures.
23
+# If unset, the default for python.d.plugin is used.
24
+# Attempts to restore the service are made once every update_every
25
+# and only if the module has collected values in the past.
26
+# retries: 60
27
+
28
+# autodetection_retry sets the job re-check interval in seconds.
29
+# The job is not deleted if check fails.
30
+# Attempts to start the job are made once every autodetection_retry.
31
+# This feature is disabled by default.
32
+# autodetection_retry: 0
33
+
34
+# ----------------------------------------------------------------------
35
+# JOBS (data collection sources)
36
+#
37
+# The default JOBS share the same *name*. JOBS with the same name
38
+# are mutually exclusive. Only one of them will be allowed running at
39
+# any time. This allows autodetection to try several alternatives and
40
+# pick the one that works.
41
+#
42
+# Any number of jobs is supported.
43
+#
44
+# All python.d.plugin JOBS (for all its modules) support a set of
45
+# predefined parameters. These are:
46
+#
47
+# job_name:
48
+# name: myname # the JOB's name as it will appear at the
49
+# # dashboard (by default is the job_name)
50
+# # JOBs sharing a name are mutually exclusive
51
+# update_every: 1 # the JOB's data collection frequency
52
+# priority: 60000 # the JOB's order on the dashboard
53
+# retries: 60 # the JOB's number of restoration attempts
54
+# autodetection_retry: 0 # the JOB's re-check interval in seconds
55
+# ----------------------------------------------------------------------
56
+
57
+# IMPORTANT
58
+# The netdata user needs to be able to sudo the arcconf program without password:
59
+# netdata ALL=(root) NOPASSWD: /path/to/arcconf
health/Makefile.am
+1
@@ -22,6 +22,7 @@ dist_userhealthconfig_DATA = \
22
healthconfigdir=$(libconfigdir)/health.d
23
dist_healthconfig_DATA = \
24
$(top_srcdir)/installer/.keep \
25
+ health.d/adaptec_raid.conf \
26
health.d/apache.conf \
27
health.d/apcupsd.conf \
28
health.d/backend.conf \
health/health.d/adaptec_raid.conf
new
+24
@@ -0,0 +1,24 @@
1
+
2
+# logical device status check
3
+
4
+template: adapter_raid_ld_status
5
+ on: adapter_raid.ld_status
6
+ lookup: max -5s
7
+ units: bool
8
+ every: 10s
9
+ crit: $this > 0
10
+ delay: down 5m multiplier 1.5 max 1h
11
+ info: at least 1 logical device is failed or degraded
12
+ to: sysadmin
13
+
14
+# physical device state check
15
+
16
+template: adapter_raid_pd_state
17
+ on: adapter_raid.pd_state
18
+ lookup: max -5s
19
+ units: bool
20
+ every: 10s
21
+ crit: $this > 0
22
+ delay: down 5m multiplier 1.5 max 1h
23
+ info: at least 1 physical device is not in online state
24
+ to: sysadmin