@cryptotaxi247 / netdata-1 / commits / 55bdae135

Add HP Smart Storage Array python plugin (#7181)

* Add HP Smart Storage Array python plugin * Use sudo -l to check if the netdata user is allowed to execute the ssacli command * Don't prepend protected functions and properties with an underscore * Use match_any() helper instead of or to match with any regex * Simplify HPSSA.parse_logical_drive() * Document hpssa specific configuration parameters * Improve README.md with suggestions

Peter Gnodde committed Dec 6, 2019 at 14:36 UTC 55bdae1356071f8f9a263a5685ca609d826be13c
6 files changed +512
collectors/python.d.plugin/Makefile.am
+1
@@ -64,6 +64,7 @@ include go_expvar/Makefile.inc
64 include haproxy/Makefile.inc
65 include hddtemp/Makefile.inc
66 include httpcheck/Makefile.inc
67 +include hpssa/Makefile.inc
68 include icecast/Makefile.inc
69 include ipfs/Makefile.inc
70 include isc_dhcpd/Makefile.inc
collectors/python.d.plugin/hpssa/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 += hpssa/hpssa.chart.py
9 +dist_pythonconfig_DATA += hpssa/hpssa.conf
10 +
11 +# do not install these files, but include them in the distribution
12 +dist_noinst_DATA += hpssa/README.md hpssa/Makefile.inc
13 +
collectors/python.d.plugin/hpssa/README.md new
+44
@@ -0,0 +1,44 @@
1 +# hpssa
2 +
3 +This module collects controller, logical, and physical device health metrics from HP Smart Storage Arrays.
4 +
5 +## Requirements:
6 +
7 +- `ssacli` program
8 +- `sudo` program
9 +- `netdata` user needs to be able to sudo the `ssacli` program without password
10 +
11 +To collect metrics, the module executes: `sudo -n ssacli ctrl all show config detail`
12 +
13 +This module produces:
14 +
15 +1. Controller state and temperature
16 +2. Cache module state and temperature
17 +3. Logical drive state
18 +4. Physical drive state and temperature
19 +
20 +## Prerequisite
21 +
22 +This module uses `ssacli`, which can only be executed by root. It uses
23 +`sudo` and assumes that it is configured such that the `netdata` user can
24 +execute `ssacli` as root without password.
25 +
26 +Add to `sudoers`:
27 +
28 +```
29 +netdata ALL=(root) NOPASSWD: /path/to/ssacli
30 +```
31 +
32 +## Configuration
33 +
34 +**hpssa** is disabled by default. Should be explicitly enabled in `python.d.conf`.
35 +
36 +```yaml
37 +hpssa: yes
38 +```
39 +
40 +If `ssacli` cannot be found in the `PATH`, configure it in `hpssa.conf`.
41 +
42 +```yaml
43 +ssacli_path: /usr/sbin/ssacli
44 +```
collectors/python.d.plugin/hpssa/hpssa.chart.py new
+392
@@ -0,0 +1,392 @@
1 +# -*- coding: utf-8 -*-
2 +# Description: hpssa netdata python.d module
3 +# Author: Peter Gnodde (gnoddep)
4 +# SPDX-License-Identifier: GPL-3.0-or-later
5 +
6 +import os
7 +import re
8 +
9 +from bases.FrameworkServices.ExecutableService import ExecutableService
10 +from bases.collection import find_binary
11 +from copy import deepcopy
12 +
13 +disabled_by_default = True
14 +update_every = 5
15 +
16 +ORDER = [
17 + 'ctrl_status',
18 + 'ctrl_temperature',
19 + 'ld_status',
20 + 'pd_status',
21 + 'pd_temperature',
22 +]
23 +
24 +CHARTS = {
25 + 'ctrl_status': {
26 + 'options': [
27 + None,
28 + 'Status 1 is OK, Status 0 is not OK',
29 + 'Status',
30 + 'Controller',
31 + 'hpssa.ctrl_status',
32 + 'line'
33 + ],
34 + 'lines': []
35 + },
36 + 'ctrl_temperature': {
37 + 'options': [
38 + None,
39 + 'Temperature',
40 + 'Celsius',
41 + 'Controller',
42 + 'hpssa.ctrl_temperature',
43 + 'line'
44 + ],
45 + 'lines': []
46 + },
47 + 'ld_status': {
48 + 'options': [
49 + None,
50 + 'Status 1 is OK, Status 0 is not OK',
51 + 'Status',
52 + 'Logical drives',
53 + 'hpssa.ld_status',
54 + 'line'
55 + ],
56 + 'lines': []
57 + },
58 + 'pd_status': {
59 + 'options': [
60 + None,
61 + 'Status 1 is OK, Status 0 is not OK',
62 + 'Status',
63 + 'Physical drives',
64 + 'hpssa.pd_status',
65 + 'line'
66 + ],
67 + 'lines': []
68 + },
69 + 'pd_temperature': {
70 + 'options': [
71 + None,
72 + 'Temperature',
73 + 'Celsius',
74 + 'Physical drives',
75 + 'hpssa.pd_temperature',
76 + 'line'
77 + ],
78 + 'lines': []
79 + }
80 +}
81 +
82 +adapter_regex = re.compile(r'^(?P<adapter_type>.+) in Slot (?P<slot>\d+)')
83 +ignored_sections_regex = re.compile(
84 + r'''
85 + ^
86 + Physical[ ]Drives
87 + | None[ ]attached
88 + | (?:Expander|Enclosure|SEP|Port[ ]Name:)[ ].+
89 + | .+[ ]at[ ]Port[ ]\S+,[ ]Box[ ]\d+,[ ].+
90 + | Mirror[ ]Group[ ]\d+:
91 + $
92 + ''',
93 + re.X
94 +)
95 +mirror_group_regex = re.compile(r'^Mirror Group \d+:$')
96 +array_regex = re.compile(r'^Array: (?P<id>[A-Z]+)$')
97 +drive_regex = re.compile(
98 + r'''
99 + ^
100 + Logical[ ]Drive:[ ](?P<logical_drive_id>\d+)
101 + | physicaldrive[ ](?P<fqn>[^:]+:\d+:\d+)
102 + $
103 + ''',
104 + re.X
105 +)
106 +key_value_regex = re.compile(r'^(?P<key>[^:]+): ?(?P<value>.*)$')
107 +ld_status_regex = re.compile(r'^Status: (?P<status>[^,]+)(?:, (?P<percentage>[0-9.]+)% complete)?$')
108 +error_match = re.compile(r'Error:')
109 +
110 +
111 +class HPSSAException(Exception):
112 + pass
113 +
114 +
115 +class HPSSA(object):
116 + def __init__(self, lines):
117 + self.lines = [line.strip() for line in lines if line.strip()]
118 + self.current_line = 0
119 + self.adapters = []
120 + self.parse()
121 +
122 + def __iter__(self):
123 + return self
124 +
125 + def __next__(self):
126 + if self.current_line == len(self.lines):
127 + raise StopIteration
128 +
129 + line = self.lines[self.current_line]
130 + self.current_line += 1
131 +
132 + return line
133 +
134 + def next(self):
135 + """
136 + This is for Python 2.7 compatibility
137 + """
138 + return self.__next__()
139 +
140 + def rewind(self):
141 + self.current_line = max(self.current_line - 1, 0)
142 +
143 + @staticmethod
144 + def match_any(line, *regexes):
145 + return any([regex.match(line) for regex in regexes])
146 +
147 + def parse(self):
148 + for line in self:
149 + match = adapter_regex.match(line)
150 + if match:
151 + self.adapters.append(self.parse_adapter(**match.groupdict()))
152 +
153 + def parse_adapter(self, slot, adapter_type):
154 + adapter = {
155 + 'slot': int(slot),
156 + 'type': adapter_type,
157 +
158 + 'controller': {
159 + 'status': None,
160 + 'temperature': None,
161 + },
162 + 'cache': {
163 + 'present': False,
164 + 'status': None,
165 + 'temperature': None,
166 + },
167 + 'battery': {
168 + 'status': None,
169 + 'count': 0,
170 + },
171 +
172 + 'logical_drives': [],
173 + 'physical_drives': [],
174 + }
175 +
176 + for line in self:
177 + if error_match.match(line):
178 + raise HPSSAException('Error: {}'.format(line))
179 + elif array_regex.match(line):
180 + self.parse_array(adapter)
181 + elif line == 'Unassigned':
182 + self.parse_unassigned_physical_drives(adapter)
183 + elif ignored_sections_regex.match(line):
184 + self.parse_ignored_section()
185 + else:
186 + match = key_value_regex.match(line)
187 + if match:
188 + key, value = match.group('key', 'value')
189 + if key == 'Controller Status':
190 + adapter['controller']['status'] = value == 'OK'
191 + elif key == 'Controller Temperature (C)':
192 + adapter['controller']['temperature'] = int(value)
193 + elif key == 'Cache Board Present':
194 + adapter['cache']['present'] = value == 'True'
195 + elif key == 'Cache Status':
196 + adapter['cache']['status'] = value == 'OK'
197 + elif key == 'Cache Module Temperature (C)':
198 + adapter['cache']['temperature'] = int(value)
199 + elif key == 'Battery/Capacitor Count':
200 + adapter['battery']['count'] = int(value)
201 + elif key == 'Battery/Capacitor Status':
202 + adapter['battery']['status'] = value == 'OK'
203 + else:
204 + raise HPSSAException('Cannot parse line: {}'.format(line))
205 +
206 + return adapter
207 +
208 + def parse_array(self, adapter):
209 + for line in self:
210 + if HPSSA.match_any(line, adapter_regex, array_regex, ignored_sections_regex):
211 + self.rewind()
212 + break
213 +
214 + match = drive_regex.match(line)
215 + if match:
216 + data = match.groupdict()
217 + if data['logical_drive_id']:
218 + self.parse_logical_drive(adapter, int(data['logical_drive_id']))
219 + else:
220 + self.parse_physical_drive(adapter, data['fqn'])
221 + elif not key_value_regex.match(line):
222 + self.rewind()
223 + break
224 +
225 + def parse_unassigned_physical_drives(self, adapter):
226 + for line in self:
227 + match = drive_regex.match(line)
228 + if match:
229 + self.parse_physical_drive(adapter, match.group('fqn'))
230 + else:
231 + self.rewind()
232 + break
233 +
234 + def parse_logical_drive(self, adapter, logical_drive_id):
235 + ld = {
236 + 'id': logical_drive_id,
237 + 'status': None,
238 + 'status_complete': None,
239 + }
240 +
241 + for line in self:
242 + if mirror_group_regex.match(line):
243 + self.parse_ignored_section()
244 + continue
245 +
246 + match = ld_status_regex.match(line)
247 + if match:
248 + ld['status'] = match.group('status') == 'OK'
249 +
250 + if match.group('percentage'):
251 + ld['status_complete'] = float(match.group('percentage')) / 100
252 + elif HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex) \
253 + or not key_value_regex.match(line):
254 + self.rewind()
255 + break
256 +
257 + adapter['logical_drives'].append(ld)
258 +
259 + def parse_physical_drive(self, adapter, fqn):
260 + pd = {
261 + 'fqn': fqn,
262 + 'status': None,
263 + 'temperature': None,
264 + }
265 +
266 + for line in self:
267 + if HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex):
268 + self.rewind()
269 + break
270 +
271 + match = key_value_regex.match(line)
272 + if match:
273 + key, value = match.group('key', 'value')
274 + if key == 'Status':
275 + pd['status'] = value == 'OK'
276 + elif key == 'Current Temperature (C)':
277 + pd['temperature'] = int(value)
278 + else:
279 + self.rewind()
280 + break
281 +
282 + adapter['physical_drives'].append(pd)
283 +
284 + def parse_ignored_section(self):
285 + for line in self:
286 + if HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex) \
287 + or not key_value_regex.match(line):
288 + self.rewind()
289 + break
290 +
291 +
292 +class Service(ExecutableService):
293 + def __init__(self, configuration=None, name=None):
294 + super(Service, self).__init__(configuration=configuration, name=name)
295 + self.order = ORDER
296 + self.definitions = deepcopy(CHARTS)
297 + self.ssacli_path = self.configuration.get('ssacli_path', 'ssacli')
298 + self.use_sudo = self.configuration.get('use_sudo', True)
299 + self.cmd = []
300 +
301 + def get_adapters(self):
302 + try:
303 + adapters = HPSSA(self._get_raw_data(command=self.cmd)).adapters
304 + if not adapters:
305 + # If no adapters are returned, run the command again but capture stderr
306 + err = self._get_raw_data(command=self.cmd, stderr=True)
307 + if err:
308 + raise HPSSAException('Error executing cmd {}: {}'.format(' '.join(self.cmd), '\n'.join(err)))
309 + return adapters
310 + except HPSSAException as ex:
311 + self.error(ex)
312 + return []
313 +
314 + def check(self):
315 + if not os.path.isfile(self.ssacli_path):
316 + ssacli_path = find_binary(self.ssacli_path)
317 + if ssacli_path:
318 + self.ssacli_path = ssacli_path
319 + else:
320 + self.error('Cannot locate "{}" binary'.format(self.ssacli_path))
321 + return False
322 +
323 + if self.use_sudo:
324 + sudo = find_binary('sudo')
325 + if not sudo:
326 + self.error('Cannot locate "{}" binary'.format('sudo'))
327 + return False
328 +
329 + allowed = self._get_raw_data(command=[sudo, '-n', '-l', self.ssacli_path])
330 + if not allowed or allowed[0].strip() != os.path.realpath(self.ssacli_path):
331 + self.error('Not allowed to run sudo for command {}'.format(self.ssacli_path))
332 + return False
333 +
334 + self.cmd = [sudo, '-n']
335 +
336 + self.cmd.extend([self.ssacli_path, 'ctrl', 'all', 'show', 'config', 'detail'])
337 + self.info('Command: {}'.format(self.cmd))
338 +
339 + adapters = self.get_adapters()
340 +
341 + self.info('Discovered adapters: {}'.format([adapter['type'] for adapter in adapters]))
342 + if not adapters:
343 + self.error('No adapters discovered')
344 + return False
345 +
346 + return True
347 +
348 + def get_data(self):
349 + netdata = {}
350 +
351 + for adapter in self.get_adapters():
352 + status_key = '{}_status'.format(adapter['slot'])
353 + temperature_key = '{}_temperature'.format(adapter['slot'])
354 + ld_key = 'ld_{}_'.format(adapter['slot'])
355 +
356 + data = {
357 + 'ctrl_status': {
358 + 'ctrl_' + status_key: adapter['controller']['status'],
359 + 'cache_' + status_key: adapter['cache']['present'] and adapter['cache']['status'],
360 + 'battery_' + status_key:
361 + adapter['battery']['status'] if adapter['battery']['count'] > 0 else None
362 + },
363 +
364 + 'ctrl_temperature': {
365 + 'ctrl_' + temperature_key: adapter['controller']['temperature'],
366 + 'cache_' + temperature_key: adapter['cache']['temperature'],
367 + },
368 +
369 + 'ld_status': {
370 + ld_key + '{}_status'.format(ld['id']): ld['status'] for ld in adapter['logical_drives']
371 + },
372 +
373 + 'pd_status': {},
374 + 'pd_temperature': {},
375 + }
376 +
377 + for pd in adapter['physical_drives']:
378 + pd_key = 'pd_{}_{}'.format(adapter['slot'], pd['fqn'])
379 + data['pd_status'][pd_key + '_status'] = pd['status']
380 + data['pd_temperature'][pd_key + '_temperature'] = pd['temperature']
381 +
382 + for chart, dimension_data in data.items():
383 + for dimension_id, value in dimension_data.items():
384 + if value is None:
385 + continue
386 +
387 + if dimension_id not in self.charts[chart]:
388 + self.charts[chart].add_dimension([dimension_id])
389 +
390 + netdata[dimension_id] = value
391 +
392 + return netdata
collectors/python.d.plugin/hpssa/hpssa.conf new
+61
@@ -0,0 +1,61 @@
1 +# netdata python.d.plugin configuration for hpssa
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: 5
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 +# penalty indicates whether to apply penalty to update_every in case of failures.
23 +# Penalty will increase every 5 failed updates in a row. Maximum penalty is 10 minutes.
24 +# penalty: yes
25 +
26 +# autodetection_retry sets the job re-check interval in seconds.
27 +# The job is not deleted if check fails.
28 +# Attempts to start the job are made once every autodetection_retry.
29 +# This feature is disabled by default.
30 +# autodetection_retry: 0
31 +
32 +# ----------------------------------------------------------------------
33 +# JOBS (data collection sources)
34 +#
35 +# The default JOBS share the same *name*. JOBS with the same name
36 +# are mutually exclusive. Only one of them will be allowed running at
37 +# any time. This allows autodetection to try several alternatives and
38 +# pick the one that works.
39 +#
40 +# Any number of jobs is supported.
41 +#
42 +# All python.d.plugin JOBS (for all its modules) support a set of
43 +# predefined parameters. These are:
44 +#
45 +# job_name:
46 +# name: myname # the JOB's name as it will appear at the
47 +# # dashboard (by default is the job_name)
48 +# # JOBs sharing a name are mutually exclusive
49 +# update_every: 5 # the JOB's data collection frequency
50 +# priority: 60000 # the JOB's order on the dashboard
51 +# penalty: yes # the JOB's penalty
52 +# autodetection_retry: 0 # the JOB's re-check interval in seconds
53 +#
54 +# Additionally to the above, hpssa also supports the following:
55 +#
56 +# ssacli_path: /usr/sbin/ssacli # The path to the ssacli executable
57 +# use_sudo: True # Whether to use sudo or not
58 +# ----------------------------------------------------------------------
59 +
60 +# ssacli_path: /usr/sbin/ssacli
61 +# use_sudo: True
collectors/python.d.plugin/python.d.conf
+1
@@ -58,6 +58,7 @@ gunicorn_log: no
58 # haproxy: yes
59 # hddtemp: yes
60 # httpcheck: yes
61 +hpssa: no
62 # icecast: yes
63 # ipfs: yes
64 # isc_dhcpd: yes