# -*- coding: utf-8 -*- # Description: hpssa netdata python.d module # Author: Peter Gnodde (gnoddep) # SPDX-License-Identifier: GPL-3.0-or-later import os import re from copy import deepcopy from bases.FrameworkServices.ExecutableService import ExecutableService from bases.collection import find_binary disabled_by_default = True update_every = 5 ORDER = [ 'ctrl_status', 'ctrl_temperature', 'ld_status', 'pd_status', 'pd_temperature', ] CHARTS = { 'ctrl_status': { 'options': [ None, 'Status 1 is OK, Status 0 is not OK', 'Status', 'Controller', 'hpssa.ctrl_status', 'line' ], 'lines': [] }, 'ctrl_temperature': { 'options': [ None, 'Temperature', 'Celsius', 'Controller', 'hpssa.ctrl_temperature', 'line' ], 'lines': [] }, 'ld_status': { 'options': [ None, 'Status 1 is OK, Status 0 is not OK', 'Status', 'Logical drives', 'hpssa.ld_status', 'line' ], 'lines': [] }, 'pd_status': { 'options': [ None, 'Status 1 is OK, Status 0 is not OK', 'Status', 'Physical drives', 'hpssa.pd_status', 'line' ], 'lines': [] }, 'pd_temperature': { 'options': [ None, 'Temperature', 'Celsius', 'Physical drives', 'hpssa.pd_temperature', 'line' ], 'lines': [] } } adapter_regex = re.compile(r'^(?P.+) in Slot (?P\d+)') ignored_sections_regex = re.compile( r''' ^ Physical[ ]Drives | None[ ]attached | (?:Expander|Enclosure|SEP|Port[ ]Name:)[ ].+ | .+[ ]at[ ]Port[ ]\S+,[ ]Box[ ]\d+,[ ].+ | Mirror[ ]Group[ ]\d+: $ ''', re.X ) mirror_group_regex = re.compile(r'^Mirror Group \d+:$') disk_partition_regex = re.compile(r'^Disk Partition Information$') array_regex = re.compile(r'^Array: (?P[A-Z]+)$') drive_regex = re.compile( r''' ^ Logical[ ]Drive:[ ](?P\d+) | physicaldrive[ ](?P[^:]+:\d+:\d+) $ ''', re.X ) key_value_regex = re.compile(r'^(?P[^:]+): ?(?P.*)$') ld_status_regex = re.compile(r'^Status: (?P[^,]+)(?:, (?P[0-9.]+)% complete)?$') error_match = re.compile(r'Error:') class HPSSAException(Exception): pass class HPSSA(object): def __init__(self, lines): self.lines = [line.strip() for line in lines if line.strip()] self.current_line = 0 self.adapters = [] self.parse() def __iter__(self): return self def __next__(self): if self.current_line == len(self.lines): raise StopIteration line = self.lines[self.current_line] self.current_line += 1 return line def next(self): """ This is for Python 2.7 compatibility """ return self.__next__() def rewind(self): self.current_line = max(self.current_line - 1, 0) @staticmethod def match_any(line, *regexes): return any([regex.match(line) for regex in regexes]) def parse(self): for line in self: match = adapter_regex.match(line) if match: self.adapters.append(self.parse_adapter(**match.groupdict())) def parse_adapter(self, slot, adapter_type): adapter = { 'slot': int(slot), 'type': adapter_type, 'controller': { 'status': None, 'temperature': None, }, 'cache': { 'present': False, 'status': None, 'temperature': None, }, 'battery': { 'status': None, 'count': 0, }, 'logical_drives': [], 'physical_drives': [], } for line in self: if error_match.match(line): raise HPSSAException('Error: {}'.format(line)) elif adapter_regex.match(line): self.rewind() break elif array_regex.match(line): self.parse_array(adapter) elif line == 'Unassigned' or line == 'HBA Drives': self.parse_physical_drives(adapter) elif ignored_sections_regex.match(line): self.parse_ignored_section() else: match = key_value_regex.match(line) if match: key, value = match.group('key', 'value') if key == 'Controller Status': adapter['controller']['status'] = value == 'OK' elif key == 'Controller Temperature (C)': adapter['controller']['temperature'] = int(value) elif key == 'Cache Board Present': adapter['cache']['present'] = value == 'True' elif key == 'Cache Status': adapter['cache']['status'] = value == 'OK' elif key == 'Cache Module Temperature (C)': adapter['cache']['temperature'] = int(value) elif key == 'Battery/Capacitor Count': adapter['battery']['count'] = int(value) elif key == 'Battery/Capacitor Status': adapter['battery']['status'] = value == 'OK' else: raise HPSSAException('Cannot parse line: {}'.format(line)) return adapter def parse_array(self, adapter): for line in self: if HPSSA.match_any(line, adapter_regex, array_regex, ignored_sections_regex): self.rewind() break match = drive_regex.match(line) if match: data = match.groupdict() if data['logical_drive_id']: self.parse_logical_drive(adapter, int(data['logical_drive_id'])) else: self.parse_physical_drive(adapter, data['fqn']) elif not key_value_regex.match(line): self.rewind() break def parse_physical_drives(self, adapter): for line in self: match = drive_regex.match(line) if match: self.parse_physical_drive(adapter, match.group('fqn')) else: self.rewind() break def parse_logical_drive(self, adapter, logical_drive_id): ld = { 'id': logical_drive_id, 'status': None, 'status_complete': None, } for line in self: if HPSSA.match_any(line, mirror_group_regex, disk_partition_regex): self.parse_ignored_section() continue match = ld_status_regex.match(line) if match: ld['status'] = match.group('status') == 'OK' if match.group('percentage'): ld['status_complete'] = float(match.group('percentage')) / 100 elif HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex) \ or not key_value_regex.match(line): self.rewind() break adapter['logical_drives'].append(ld) def parse_physical_drive(self, adapter, fqn): pd = { 'fqn': fqn, 'status': None, 'temperature': None, } for line in self: if HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex): self.rewind() break match = key_value_regex.match(line) if match: key, value = match.group('key', 'value') if key == 'Status': pd['status'] = value == 'OK' elif key == 'Current Temperature (C)': pd['temperature'] = int(value) else: self.rewind() break adapter['physical_drives'].append(pd) def parse_ignored_section(self): for line in self: if HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex) \ or not key_value_regex.match(line): self.rewind() break class Service(ExecutableService): def __init__(self, configuration=None, name=None): super(Service, self).__init__(configuration=configuration, name=name) self.order = ORDER self.definitions = deepcopy(CHARTS) self.ssacli_path = self.configuration.get('ssacli_path', 'ssacli') self.use_sudo = self.configuration.get('use_sudo', True) self.cmd = [] def get_adapters(self): try: adapters = HPSSA(self._get_raw_data(command=self.cmd)).adapters if not adapters: # If no adapters are returned, run the command again but capture stderr err = self._get_raw_data(command=self.cmd, stderr=True) if err: raise HPSSAException('Error executing cmd {}: {}'.format(' '.join(self.cmd), '\n'.join(err))) return adapters except HPSSAException as ex: self.error(ex) return [] def check(self): if not os.path.isfile(self.ssacli_path): ssacli_path = find_binary(self.ssacli_path) if ssacli_path: self.ssacli_path = ssacli_path else: self.error('Cannot locate "{}" binary'.format(self.ssacli_path)) return False if self.use_sudo: sudo = find_binary('sudo') if not sudo: self.error('Cannot locate "{}" binary'.format('sudo')) return False allowed = self._get_raw_data(command=[sudo, '-n', '-l', self.ssacli_path]) if not allowed or allowed[0].strip() != os.path.realpath(self.ssacli_path): self.error('Not allowed to run sudo for command {}'.format(self.ssacli_path)) return False self.cmd = [sudo, '-n'] self.cmd.extend([self.ssacli_path, 'ctrl', 'all', 'show', 'config', 'detail']) self.info('Command: {}'.format(self.cmd)) adapters = self.get_adapters() self.info('Discovered adapters: {}'.format([adapter['type'] for adapter in adapters])) if not adapters: self.error('No adapters discovered') return False return True def get_data(self): netdata = {} for adapter in self.get_adapters(): status_key = '{}_status'.format(adapter['slot']) temperature_key = '{}_temperature'.format(adapter['slot']) ld_key = 'ld_{}_'.format(adapter['slot']) data = { 'ctrl_status': { 'ctrl_' + status_key: adapter['controller']['status'], 'cache_' + status_key: adapter['cache']['present'] and adapter['cache']['status'], 'battery_' + status_key: adapter['battery']['status'] if adapter['battery']['count'] > 0 else None }, 'ctrl_temperature': { 'ctrl_' + temperature_key: adapter['controller']['temperature'], 'cache_' + temperature_key: adapter['cache']['temperature'], }, 'ld_status': { ld_key + '{}_status'.format(ld['id']): ld['status'] for ld in adapter['logical_drives'] }, 'pd_status': {}, 'pd_temperature': {}, } for pd in adapter['physical_drives']: pd_key = 'pd_{}_{}'.format(adapter['slot'], pd['fqn']) data['pd_status'][pd_key + '_status'] = pd['status'] data['pd_temperature'][pd_key + '_temperature'] = pd['temperature'] for chart, dimension_data in data.items(): for dimension_id, value in dimension_data.items(): if value is None: continue if dimension_id not in self.charts[chart]: self.charts[chart].add_dimension([dimension_id]) netdata[dimension_id] = value return netdata