hpssa.chart.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. import os
  6. import re
  7. from copy import deepcopy
  8. from bases.FrameworkServices.ExecutableService import ExecutableService
  9. from bases.collection import find_binary
  10. disabled_by_default = True
  11. update_every = 5
  12. ORDER = [
  13. 'ctrl_status',
  14. 'ctrl_temperature',
  15. 'ld_status',
  16. 'pd_status',
  17. 'pd_temperature',
  18. ]
  19. CHARTS = {
  20. 'ctrl_status': {
  21. 'options': [
  22. None,
  23. 'Status 1 is OK, Status 0 is not OK',
  24. 'Status',
  25. 'Controller',
  26. 'hpssa.ctrl_status',
  27. 'line'
  28. ],
  29. 'lines': []
  30. },
  31. 'ctrl_temperature': {
  32. 'options': [
  33. None,
  34. 'Temperature',
  35. 'Celsius',
  36. 'Controller',
  37. 'hpssa.ctrl_temperature',
  38. 'line'
  39. ],
  40. 'lines': []
  41. },
  42. 'ld_status': {
  43. 'options': [
  44. None,
  45. 'Status 1 is OK, Status 0 is not OK',
  46. 'Status',
  47. 'Logical drives',
  48. 'hpssa.ld_status',
  49. 'line'
  50. ],
  51. 'lines': []
  52. },
  53. 'pd_status': {
  54. 'options': [
  55. None,
  56. 'Status 1 is OK, Status 0 is not OK',
  57. 'Status',
  58. 'Physical drives',
  59. 'hpssa.pd_status',
  60. 'line'
  61. ],
  62. 'lines': []
  63. },
  64. 'pd_temperature': {
  65. 'options': [
  66. None,
  67. 'Temperature',
  68. 'Celsius',
  69. 'Physical drives',
  70. 'hpssa.pd_temperature',
  71. 'line'
  72. ],
  73. 'lines': []
  74. }
  75. }
  76. adapter_regex = re.compile(r'^(?P<adapter_type>.+) in Slot (?P<slot>\d+)')
  77. ignored_sections_regex = re.compile(
  78. r'''
  79. ^
  80. Physical[ ]Drives
  81. | None[ ]attached
  82. | (?:Expander|Enclosure|SEP|Port[ ]Name:)[ ].+
  83. | .+[ ]at[ ]Port[ ]\S+,[ ]Box[ ]\d+,[ ].+
  84. | Mirror[ ]Group[ ]\d+:
  85. $
  86. ''',
  87. re.X
  88. )
  89. mirror_group_regex = re.compile(r'^Mirror Group \d+:$')
  90. disk_partition_regex = re.compile(r'^Disk Partition Information$')
  91. array_regex = re.compile(r'^Array: (?P<id>[A-Z]+)$')
  92. drive_regex = re.compile(
  93. r'''
  94. ^
  95. Logical[ ]Drive:[ ](?P<logical_drive_id>\d+)
  96. | physicaldrive[ ](?P<fqn>[^:]+:\d+:\d+)
  97. $
  98. ''',
  99. re.X
  100. )
  101. key_value_regex = re.compile(r'^(?P<key>[^:]+): ?(?P<value>.*)$')
  102. ld_status_regex = re.compile(r'^Status: (?P<status>[^,]+)(?:, (?P<percentage>[0-9.]+)% complete)?$')
  103. error_match = re.compile(r'Error:')
  104. class HPSSAException(Exception):
  105. pass
  106. class HPSSA(object):
  107. def __init__(self, lines):
  108. self.lines = [line.strip() for line in lines if line.strip()]
  109. self.current_line = 0
  110. self.adapters = []
  111. self.parse()
  112. def __iter__(self):
  113. return self
  114. def __next__(self):
  115. if self.current_line == len(self.lines):
  116. raise StopIteration
  117. line = self.lines[self.current_line]
  118. self.current_line += 1
  119. return line
  120. def next(self):
  121. """
  122. This is for Python 2.7 compatibility
  123. """
  124. return self.__next__()
  125. def rewind(self):
  126. self.current_line = max(self.current_line - 1, 0)
  127. @staticmethod
  128. def match_any(line, *regexes):
  129. return any([regex.match(line) for regex in regexes])
  130. def parse(self):
  131. for line in self:
  132. match = adapter_regex.match(line)
  133. if match:
  134. self.adapters.append(self.parse_adapter(**match.groupdict()))
  135. def parse_adapter(self, slot, adapter_type):
  136. adapter = {
  137. 'slot': int(slot),
  138. 'type': adapter_type,
  139. 'controller': {
  140. 'status': None,
  141. 'temperature': None,
  142. },
  143. 'cache': {
  144. 'present': False,
  145. 'status': None,
  146. 'temperature': None,
  147. },
  148. 'battery': {
  149. 'status': None,
  150. 'count': 0,
  151. },
  152. 'logical_drives': [],
  153. 'physical_drives': [],
  154. }
  155. for line in self:
  156. if error_match.match(line):
  157. raise HPSSAException('Error: {}'.format(line))
  158. elif adapter_regex.match(line):
  159. self.rewind()
  160. break
  161. elif array_regex.match(line):
  162. self.parse_array(adapter)
  163. elif line == 'Unassigned' or line == 'HBA Drives':
  164. self.parse_physical_drives(adapter)
  165. elif ignored_sections_regex.match(line):
  166. self.parse_ignored_section()
  167. else:
  168. match = key_value_regex.match(line)
  169. if match:
  170. key, value = match.group('key', 'value')
  171. if key == 'Controller Status':
  172. adapter['controller']['status'] = value == 'OK'
  173. elif key == 'Controller Temperature (C)':
  174. adapter['controller']['temperature'] = int(value)
  175. elif key == 'Cache Board Present':
  176. adapter['cache']['present'] = value == 'True'
  177. elif key == 'Cache Status':
  178. adapter['cache']['status'] = value == 'OK'
  179. elif key == 'Cache Module Temperature (C)':
  180. adapter['cache']['temperature'] = int(value)
  181. elif key == 'Battery/Capacitor Count':
  182. adapter['battery']['count'] = int(value)
  183. elif key == 'Battery/Capacitor Status':
  184. adapter['battery']['status'] = value == 'OK'
  185. else:
  186. raise HPSSAException('Cannot parse line: {}'.format(line))
  187. return adapter
  188. def parse_array(self, adapter):
  189. for line in self:
  190. if HPSSA.match_any(line, adapter_regex, array_regex, ignored_sections_regex):
  191. self.rewind()
  192. break
  193. match = drive_regex.match(line)
  194. if match:
  195. data = match.groupdict()
  196. if data['logical_drive_id']:
  197. self.parse_logical_drive(adapter, int(data['logical_drive_id']))
  198. else:
  199. self.parse_physical_drive(adapter, data['fqn'])
  200. elif not key_value_regex.match(line):
  201. self.rewind()
  202. break
  203. def parse_physical_drives(self, adapter):
  204. for line in self:
  205. match = drive_regex.match(line)
  206. if match:
  207. self.parse_physical_drive(adapter, match.group('fqn'))
  208. else:
  209. self.rewind()
  210. break
  211. def parse_logical_drive(self, adapter, logical_drive_id):
  212. ld = {
  213. 'id': logical_drive_id,
  214. 'status': None,
  215. 'status_complete': None,
  216. }
  217. for line in self:
  218. if HPSSA.match_any(line, mirror_group_regex, disk_partition_regex):
  219. self.parse_ignored_section()
  220. continue
  221. match = ld_status_regex.match(line)
  222. if match:
  223. ld['status'] = match.group('status') == 'OK'
  224. if match.group('percentage'):
  225. ld['status_complete'] = float(match.group('percentage')) / 100
  226. elif HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex) \
  227. or not key_value_regex.match(line):
  228. self.rewind()
  229. break
  230. adapter['logical_drives'].append(ld)
  231. def parse_physical_drive(self, adapter, fqn):
  232. pd = {
  233. 'fqn': fqn,
  234. 'status': None,
  235. 'temperature': None,
  236. }
  237. for line in self:
  238. if HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex):
  239. self.rewind()
  240. break
  241. match = key_value_regex.match(line)
  242. if match:
  243. key, value = match.group('key', 'value')
  244. if key == 'Status':
  245. pd['status'] = value == 'OK'
  246. elif key == 'Current Temperature (C)':
  247. pd['temperature'] = int(value)
  248. else:
  249. self.rewind()
  250. break
  251. adapter['physical_drives'].append(pd)
  252. def parse_ignored_section(self):
  253. for line in self:
  254. if HPSSA.match_any(line, adapter_regex, array_regex, drive_regex, ignored_sections_regex) \
  255. or not key_value_regex.match(line):
  256. self.rewind()
  257. break
  258. class Service(ExecutableService):
  259. def __init__(self, configuration=None, name=None):
  260. super(Service, self).__init__(configuration=configuration, name=name)
  261. self.order = ORDER
  262. self.definitions = deepcopy(CHARTS)
  263. self.ssacli_path = self.configuration.get('ssacli_path', 'ssacli')
  264. self.use_sudo = self.configuration.get('use_sudo', True)
  265. self.cmd = []
  266. def get_adapters(self):
  267. try:
  268. adapters = HPSSA(self._get_raw_data(command=self.cmd)).adapters
  269. if not adapters:
  270. # If no adapters are returned, run the command again but capture stderr
  271. err = self._get_raw_data(command=self.cmd, stderr=True)
  272. if err:
  273. raise HPSSAException('Error executing cmd {}: {}'.format(' '.join(self.cmd), '\n'.join(err)))
  274. return adapters
  275. except HPSSAException as ex:
  276. self.error(ex)
  277. return []
  278. def check(self):
  279. if not os.path.isfile(self.ssacli_path):
  280. ssacli_path = find_binary(self.ssacli_path)
  281. if ssacli_path:
  282. self.ssacli_path = ssacli_path
  283. else:
  284. self.error('Cannot locate "{}" binary'.format(self.ssacli_path))
  285. return False
  286. if self.use_sudo:
  287. sudo = find_binary('sudo')
  288. if not sudo:
  289. self.error('Cannot locate "{}" binary'.format('sudo'))
  290. return False
  291. allowed = self._get_raw_data(command=[sudo, '-n', '-l', self.ssacli_path])
  292. if not allowed or allowed[0].strip() != os.path.realpath(self.ssacli_path):
  293. self.error('Not allowed to run sudo for command {}'.format(self.ssacli_path))
  294. return False
  295. self.cmd = [sudo, '-n']
  296. self.cmd.extend([self.ssacli_path, 'ctrl', 'all', 'show', 'config', 'detail'])
  297. self.info('Command: {}'.format(self.cmd))
  298. adapters = self.get_adapters()
  299. self.info('Discovered adapters: {}'.format([adapter['type'] for adapter in adapters]))
  300. if not adapters:
  301. self.error('No adapters discovered')
  302. return False
  303. return True
  304. def get_data(self):
  305. netdata = {}
  306. for adapter in self.get_adapters():
  307. status_key = '{}_status'.format(adapter['slot'])
  308. temperature_key = '{}_temperature'.format(adapter['slot'])
  309. ld_key = 'ld_{}_'.format(adapter['slot'])
  310. data = {
  311. 'ctrl_status': {
  312. 'ctrl_' + status_key: adapter['controller']['status'],
  313. 'cache_' + status_key: adapter['cache']['present'] and adapter['cache']['status'],
  314. 'battery_' + status_key:
  315. adapter['battery']['status'] if adapter['battery']['count'] > 0 else None
  316. },
  317. 'ctrl_temperature': {
  318. 'ctrl_' + temperature_key: adapter['controller']['temperature'],
  319. 'cache_' + temperature_key: adapter['cache']['temperature'],
  320. },
  321. 'ld_status': {
  322. ld_key + '{}_status'.format(ld['id']): ld['status'] for ld in adapter['logical_drives']
  323. },
  324. 'pd_status': {},
  325. 'pd_temperature': {},
  326. }
  327. for pd in adapter['physical_drives']:
  328. pd_key = 'pd_{}_{}'.format(adapter['slot'], pd['fqn'])
  329. data['pd_status'][pd_key + '_status'] = pd['status']
  330. data['pd_temperature'][pd_key + '_temperature'] = pd['temperature']
  331. for chart, dimension_data in data.items():
  332. for dimension_id, value in dimension_data.items():
  333. if value is None:
  334. continue
  335. if dimension_id not in self.charts[chart]:
  336. self.charts[chart].add_dimension([dimension_id])
  337. netdata[dimension_id] = value
  338. return netdata