Merge ~afreiberger/charm-hw-health:bug/1838457-refactor into charm-hw-health:master

Proposed by Drew Freiberger
Status: Superseded
Proposed branch: ~afreiberger/charm-hw-health:bug/1838457-refactor
Merge into: charm-hw-health:master
Diff against target: 24810 lines (+23252/-347)
56 files modified
.gitignore (+0/-2)
Makefile (+18/-14)
src/README.md (+1/-2)
src/files/common/check_hw_health_cron_output.py (+78/-0)
src/files/common/hw_health_lib.py (+263/-0)
src/files/hplog/cron_hplog.py (+304/-0)
src/files/ilorest/check_ilorest.py (+74/-0)
src/files/ilorest/cron_ilorest.py (+146/-0)
src/files/mdadm/check_mdadm.py (+31/-6)
src/files/mdadm/cron_mdadm.py (+36/-10)
src/files/megacli/check_megacli.py (+38/-8)
src/files/megacli/cron_megacli.sh (+13/-2)
src/files/nvme/check_nvme.py (+18/-1)
src/files/sas2ircu/check_sas2ircu.py (+28/-4)
src/files/sas2ircu/cron_sas2ircu.sh (+13/-2)
src/files/sas3ircu/check_sas3ircu.py (+32/-7)
src/files/sas3ircu/cron_sas3ircu.sh (+13/-2)
src/files/ssacli/cron_ssacli.py (+178/-0)
src/lib/hwhealth/discovery/lshw.py (+48/-29)
src/lib/hwhealth/discovery/supported_vendors.py (+17/-5)
src/lib/hwhealth/hwdiscovery.py (+55/-22)
src/lib/hwhealth/tools.py (+317/-70)
src/metadata.yaml (+1/-0)
src/reactive/hw_health.py (+31/-3)
src/tests/expected/lshw.py (+1917/-0)
src/tests/functional/requirements.txt (+1/-0)
src/tests/functional/test_hwhealth.py (+145/-60)
src/tests/hw-health-samples/cron_outputs/hplog.out.ok (+1/-0)
src/tests/hw-health-samples/cron_outputs/ipmi_sensors.out.critical (+1/-0)
src/tests/hw-health-samples/cron_outputs/ssacli.out.ok (+1/-0)
src/tests/hw-health-samples/hplog.f.ewah.out (+6/-0)
src/tests/hw-health-samples/hplog.p.ewah.out (+6/-0)
src/tests/hw-health-samples/hplog.t.ewah.out (+20/-0)
src/tests/hw-health-samples/hplog.v.ewah.out (+60/-0)
src/tests/hw-health-samples/ilorest.list.Chassis (+138/-0)
src/tests/hw-health-samples/ilorest.list.HpeSmartStorage (+452/-0)
src/tests/hw-health-samples/ilorest.list.Memory (+1376/-0)
src/tests/hw-health-samples/ilorest.list.Power (+7929/-0)
src/tests/hw-health-samples/ilorest.list.Processor (+286/-0)
src/tests/hw-health-samples/ilorest.list.Thermal (+1513/-0)
src/tests/hw-health-samples/lshw.hpe.nonvme.json (+7359/-0)
src/tests/hw-health-samples/ssacli.array.status.faileddrive.out (+7/-0)
src/tests/hw-health-samples/ssacli.array.status.ok.out (+6/-0)
src/tests/hw-health-samples/ssacli.ctrl.status.failedbattery.out (+7/-0)
src/tests/unit/requirements.txt (+4/-0)
src/tests/unit/test_actions.py (+3/-1)
src/tests/unit/test_check_mdadm.py (+14/-7)
src/tests/unit/test_check_megacli.py (+10/-3)
src/tests/unit/test_check_sas2ircu.py (+1/-1)
src/tests/unit/test_cron_hplog.py (+37/-0)
src/tests/unit/test_cron_ilorest.py (+24/-0)
src/tests/unit/test_cron_mdadm.py (+10/-5)
src/tests/unit/test_cron_ssacli.py (+33/-0)
src/tests/unit/test_hwdiscovery.py (+64/-75)
src/tests/unit/test_lshw.py (+61/-0)
src/tox.ini (+7/-6)
Reviewer Review Type Date Requested Status
Drew Freiberger (community) Needs Resubmitting
Xav Paice Pending
Review via email: mp+388004@code.launchpad.net

This proposal supersedes a proposal from 2020-07-21.

This proposal has been superseded by a proposal from 2020-08-07.

To post a comment you must log in.
Revision history for this message
Drew Freiberger (afreiberger) wrote : Posted in a previous version of this proposal
Revision history for this message
Xav Paice (xavpaice) wrote : Posted in a previous version of this proposal

This is a substantial size change, and could do with some improved test coverage.

Tests all pass - however there's a bunch of rebase errors in the change that should have been picked up by lint, let alone unit tests.

review: Needs Fixing
Revision history for this message
Xav Paice (xavpaice) wrote : Posted in a previous version of this proposal

also needs a rebase, there's conflicts that will cause issues

Revision history for this message
Drew Freiberger (afreiberger) wrote :

I've run through and rebased on top of master. turns out a lot of the test coverage was already in master for these bits.

----------- coverage: platform linux, python 3.8.2-final-0 -----------
Name Stmts Miss Cover
-----------------------------------------------------------------
actions/__init__.py 0 0 100%
actions/actions.py 40 8 80%
files/common/hw_health_lib.py 122 76 38%
files/hplog/cron_hplog.py 144 63 56%
files/ilorest/cron_ilorest.py 79 29 63%
files/mdadm/check_mdadm.py 32 5 84%
files/mdadm/cron_mdadm.py 141 19 87%
files/megacli/check_megacli.py 101 28 72%
files/nvme/check_nvme.py 43 13 70%
files/sas2ircu/check_sas2ircu.py 50 13 74%
files/sas3ircu/check_sas3ircu.py 100 30 70%
files/ssacli/cron_ssacli.py 93 47 49%
lib/hwhealth/__init__.py 0 0 100%
lib/hwhealth/discovery/lshw.py 149 37 75%
lib/hwhealth/discovery/supported_vendors.py 4 0 100%
lib/hwhealth/hwdiscovery.py 67 15 78%
lib/hwhealth/tools.py 324 188 42%
-----------------------------------------------------------------
TOTAL 1489 571 62%

lint/unit tests are passing, running functional now.

Revision history for this message
Drew Freiberger (afreiberger) wrote :

I've fleshed out a couple of functional test bugs...this is still WIP.

I'm trying to find a way to remove the test input data from this MP and submit a dependent merge with just those sample files so this review can be code oriented.

Revision history for this message
Drew Freiberger (afreiberger) wrote :

running functional tests after resolving what appear to be the issues with the hp repos and lack of focal support. Focal deploy is now succeeding. Will have to see if the resolution for the new GPG key on the hp mcp repo fixes bionic.

review: Needs Resubmitting
a60b332... by Drew Freiberger

Added older megacli checksum

Revision history for this message
Drew Freiberger (afreiberger) wrote :

test results not good on functional testing. Needs more review. Blocking for MR waiting for HP tools for focal to be released. Or refactor based on tools that are series-specific.

https://pastebin.canonical.com/p/vvHxtQcfSd/

25cb015... by Drew Freiberger

Update hp-health ssacli repository to current-gen9

d98a133... by Drew Freiberger

Revert "Add supported_series no-op code to AptVendorTool class"

This reverts commit 760d646c6fcb0cb5ea40fbd2eebc13e83250e025.

26aabf1... by Drew Freiberger

Update tools discovery code and tests to be series-aware

Also remove known-bad checksum (zero byte file checksum e3b...)
from approved list of megacli checksums.

Revision history for this message
Drew Freiberger (afreiberger) wrote :

focal repositories do not have the same gen9 hp tooling, so are not enabled.
Gen10 hp tooling is a future TODO and will then support focal for HP tools.

Unmerged commits

26aabf1... by Drew Freiberger

Update tools discovery code and tests to be series-aware

Also remove known-bad checksum (zero byte file checksum e3b...)
from approved list of megacli checksums.

d98a133... by Drew Freiberger

Revert "Add supported_series no-op code to AptVendorTool class"

This reverts commit 760d646c6fcb0cb5ea40fbd2eebc13e83250e025.

25cb015... by Drew Freiberger

Update hp-health ssacli repository to current-gen9

a60b332... by Drew Freiberger

Added older megacli checksum

760d646... by Drew Freiberger

Add supported_series no-op code to AptVendorTool class

9f33298... by Drew Freiberger

Update HPE tool repositories and keys

686d4b1... by Drew Freiberger

Skip apt-add-repo for undefined AptVendorTool repos

Update cron script paths for cron_$tool.sh tools

Fixed functional test errors and cleaned nits from black

b851dc8... by Drew Freiberger

Fixing up tests from merge conflict resolution errors for nagios check tools

8232cd1... by Drew Freiberger

Fixed lint and focal test environment issues

Updated checksum for new version of sas3ircu tool

85988a1... by Andrea Ieri

[test_hwhealth] pep8 compliance

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index e808ce9..06bf3e8 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -23,5 +23,3 @@ builds
6
7 # tools bundle (needed for functional testing)
8 tools.zip
9-tools-checksum.zip
10-tools-missing.zip
11diff --git a/Makefile b/Makefile
12index 70f7666..d314eb8 100644
13--- a/Makefile
14+++ b/Makefile
15@@ -27,11 +27,11 @@ lint:
16
17 test: lint unittest functional
18
19-unittest:
20- @cd src && tox -e unit
21+unittest: build
22+ @cd $(JUJU_REPOSITORY)/builds/hw-health && tox -e unit
23
24-functional: resource_check build
25- @cd src && tox -e functional
26+functional: resource_check build buildresources
27+ @cd $(JUJU_REPOSITORY)/builds/hw-health && tox -e functional
28
29 resource_check:
30 ifeq ("$(wildcard $(JUJU_REPOSITORY)/tools.zip)","")
31@@ -39,17 +39,21 @@ ifeq ("$(wildcard $(JUJU_REPOSITORY)/tools.zip)","")
32 before running functional tests. \
33 Check the README for instructions on how to do so.)
34 endif
35-ifeq ("$(wildcard $(JUJU_REPOSITORY)/tools-checksum.zip)","")
36- $(error ERROR: You must save the tools resource in $(JUJU_REPOSITORY)/tools-checksum.zip \
37- before running functional tests. \
38- megacli tool needs to be replaced by an empty file within the zip archive.)
39-endif
40-ifeq ("$(wildcard $(JUJU_REPOSITORY)/tools-missing.zip)","")
41- $(error ERROR: You must save the tools resource in $(JUJU_REPOSITORY)/tools-missing.zip \
42- before running functional tests. \
43- megacli tool needs to be removed from the zip archive.)
44-endif
45
46+buildresources:
47+ @echo "Copying tools.zip to the builds directory"
48+ @cp $(JUJU_REPOSITORY)/tools.zip $(JUJU_REPOSITORY)/builds/tools.zip
49+ @echo "Building tools-missing.zip"
50+ @zip $(JUJU_REPOSITORY)/tools.zip --copy \
51+ --out $(JUJU_REPOSITORY)/builds/tools-missing.zip \
52+ --exclude megacli
53+ @echo "Building tools-checksum.zip"
54+ @zip $(JUJU_REPOSITORY)/tools.zip --copy \
55+ --out $(JUJU_REPOSITORY)/builds/tools-checksum.zip \
56+ --exclude megacli
57+ @touch $(JUJU_REPOSITORY)/builds/megacli
58+ @zip --move --junk-path $(JUJU_REPOSITORY)/builds/tools-checksum.zip \
59+ $(JUJU_REPOSITORY)/builds/megacli
60
61 build:
62 @echo "Building charm to base directory $(JUJU_REPOSITORY)"
63diff --git a/src/README.md b/src/README.md
64index 45b48ed..0cb9007 100644
65--- a/src/README.md
66+++ b/src/README.md
67@@ -20,8 +20,7 @@ Hardware-independent tools:
68 default)
69
70 In the backlog, hp-health logic still needs to be backported to support
71-Hewlett-Packard equipment (HP Smart Array Controllers and MSA Controllers with
72-hpacucli, hpssacli, ssacli).
73+Hewlett-Packard gen8 and older equipment (HP Controllers with hpacucli)
74
75 Furthermore, other hardware in the roadmap is:
76
77diff --git a/src/files/common/check_hw_health_cron_output.py b/src/files/common/check_hw_health_cron_output.py
78new file mode 100755
79index 0000000..ebb63e1
80--- /dev/null
81+++ b/src/files/common/check_hw_health_cron_output.py
82@@ -0,0 +1,78 @@
83+#!/usr/bin/env python3
84+# ------------------------------------------------
85+# This file is juju managed
86+# ------------------------------------------------
87+
88+# Copyright (C) 2008, 2009, 2012 James Troup <james.troup@canonical.com>
89+# Copyright (C) 2009 LaMont Jones <lamont.jones@canonical.com>
90+
91+###############################################################################
92+
93+from optparse import OptionParser
94+
95+from nagios_plugin3 import (check_file_freshness,
96+ try_check,
97+ WarnError,
98+ UnknownError,
99+ CriticalError)
100+
101+
102+###############################################################################
103+
104+
105+def check_and_open(filename, time_limit):
106+ check_file_freshness(filename, time_limit)
107+ return open(filename)
108+
109+
110+###############################################################################
111+
112+
113+def check_file_contents(input_filename, time_limit):
114+ input_file = check_and_open(input_filename, time_limit)
115+ for line in input_file.readlines():
116+ line = line.rstrip()
117+ if not line:
118+ continue
119+ if line.startswith("WARN"):
120+ raise WarnError(line)
121+ if line.startswith("UNKNOWN"):
122+ raise UnknownError(line)
123+ elif line.startswith("OK"):
124+ print(line)
125+ else:
126+ raise CriticalError(line)
127+
128+
129+###############################################################################
130+
131+
132+def main():
133+ parser = OptionParser()
134+ parser.add_option(
135+ "--time",
136+ dest="time_limit",
137+ help="freshness time limit [default=%default]",
138+ metavar="SECONDS",
139+ default=1200,
140+ type=int
141+ )
142+ parser.add_option(
143+ "-f", "--filename",
144+ dest="input_file",
145+ help=('file containing the output of '
146+ 'cron_ilorest.py [default=%default]'),
147+ metavar="FILE",
148+ nargs=1,
149+ # default="/var/lib/nagios/UNSETOUTPUTFILE.out",
150+ type=str
151+ )
152+
153+ (opts, args) = parser.parse_args()
154+
155+ try_check(check_file_contents, opts.input_file, opts.time_limit)
156+ print("All OK")
157+
158+
159+if __name__ == "__main__":
160+ main()
161diff --git a/src/files/common/hw_health_lib.py b/src/files/common/hw_health_lib.py
162new file mode 100644
163index 0000000..c75a4e5
164--- /dev/null
165+++ b/src/files/common/hw_health_lib.py
166@@ -0,0 +1,263 @@
167+# -----------------------------------------------
168+# This file is juju managed
169+# -----------------------------------------------
170+
171+# Copyright (C) 2008-2013 James Troup <james.troup@canonical.com>
172+# Copyright (c) 2020 Drew Freiberger <drew.freiberger@canonical.com>
173+
174+import argparse
175+import os
176+import re
177+import subprocess
178+import tempfile
179+import yaml
180+
181+from datetime import datetime, timedelta
182+from binascii import crc32
183+from socket import gethostname
184+
185+
186+ignoring_output = []
187+
188+
189+def read_ignore_file(ignore_file): # noqa C901
190+ """
191+ Reads a file to create a list of ignore dicts to use with
192+ the ignore function.
193+
194+ The directory /etc/nagios/ignores/ must be owned by nagios user,
195+ so that it can create and re-write the ignores files.
196+ Use the following to create it with correct perms:
197+ D=/etc/nagios/ignores; sudo mkdir $D; sudo chown nagios $D;
198+ MODULE=power # or array, controller, fans
199+ sudo -u nagios vim /etc/nagios/ignores/ignores-cat-hp-$MODULE.txt
200+
201+ The following is useful to paste at the top of an ignores file:
202+ # This file used by /usr/lib/nagios/plugins/check_hp_hardware.py
203+ # so that specific hardware errors can be ignored
204+ # and the nagios alert NOT downtimed or acknowledged,
205+ # thus allowing other hardware failures to alert.
206+ # ----------------------------------------------------------------
207+ # Ignore lines are an expiry date and a substring to match, like:
208+ # [YYYY-mm-dd HH:MM] nagios error string
209+ # Lines will be disabled when not matched so they can alert again,
210+ # but starting a line with '*' will persist a line when not matched
211+ # and should be used for flapping alerts/hardware only.
212+ # Comment lines (starting with '#') and blank lines are ignored.
213+ # Example ignore lines:
214+ # # do not alert on fan 1 until 2016-11-17 11:00
215+ # [2016-11-17 11:00] System Board (1): fans are not redundant
216+ # # ignore power supply 1 until 2016-11-17 0:00, even if unmatched
217+ # * [2016-11-17] Pwr. Supply Bay (1): power supply is 'Failed'
218+
219+ # example ignore dict
220+ # {'matched': False, # set to True if it matches, check before write
221+ # 'expired': False, # has ignore date passed
222+ # 'line': "[2016-11-01 23:00] substring to match"
223+ # 'ignore': "substring to match OR None", # None if expired
224+ # }
225+ """
226+ ignores = []
227+ if os.path.isfile(ignore_file):
228+ for line in open(ignore_file).readlines():
229+ d = {'matched': False, 'expired': False, 'line': line.rstrip(), 'ignore': None}
230+ line = line.strip()
231+ # special case lines starting with '*', do not disable if unmatched
232+ persist = False
233+ if line.startswith('*'):
234+ persist = True
235+ line = line.lstrip('*').strip()
236+ # parse date lines
237+ if line.startswith('['):
238+ parts = re.split('\\[|\\]', line, maxsplit=2)
239+ date = parts[1].strip()
240+ ignore = parts[2].strip()
241+ try:
242+ date = datetime.strptime(date, '%Y-%m-%d %H:%M')
243+ except ValueError:
244+ try:
245+ date = datetime.strptime(date, '%Y-%m-%d')
246+ except ValueError:
247+ print("Failed to parse ignore date: {}".format(d['line']))
248+ date = None
249+ if date:
250+ # Do not alert directly at midnight UTC
251+ host_crc = crc32(gethostname()) & 0xFFFFFFFF
252+ date += timedelta(seconds=(host_crc % 86400))
253+ # check if date valid and is in the future
254+ if date:
255+ if datetime.now().weekday() in (5, 6):
256+ # Ignore Saturday/Sunday to not annoy on-call
257+ d['ignore'] = ignore
258+ elif date > datetime.now():
259+ d['ignore'] = ignore
260+ else:
261+ d['expired'] = True
262+ if persist and not d['expired']:
263+ # set matched True so will not get disabled on non-match
264+ d['matched'] = True
265+ # comment lines and empty lines are just added
266+ elif line.startswith('#') or not line:
267+ # add with matched True so does not trigger a file rewrite
268+ d['matched'] = True
269+ # unrecognized lines left matched False so rewritten
270+ ignores.append(d)
271+ return ignores
272+
273+
274+def write_ignore_file(ignores, ignore_file):
275+ # if any ignores are not matched then write out file lines again
276+ if any([not i['matched'] for i in ignores]):
277+ dirname, basename = os.path.split(ignore_file)
278+ date = datetime.now()
279+ try:
280+ f = tempfile.NamedTemporaryFile(dir=dirname, prefix=basename, delete=False)
281+ for ignore in ignores:
282+ if not ignore['matched'] and ignore['ignore']:
283+ ignore['line'] = "# not matched at {} #{}".format(
284+ date.strftime("%Y-%m-%dT%H:%M:%S"), ignore['line']
285+ )
286+ elif ignore['expired']:
287+ # this won't get updated unless the alert has cleared
288+ ignore['line'] = "# expired #{}".format(ignore['line'])
289+ elif not ignore['matched']:
290+ ignore['line'] = "# unknown or bad format #{}".format(ignore['line'])
291+ f.write(ignore['line'] + '\n')
292+ f.flush()
293+ os.fsync(f.fileno())
294+ f.close()
295+ # NamedTemporaryFile has restricted permissions, open them up a bit
296+ os.chmod(f.name, os.stat(f.name).st_mode | 0o044)
297+ os.rename(f.name, ignore_file)
298+ except OSError:
299+ print("Failed to write {}".format(ignore_file))
300+ pass
301+
302+
303+def ignore(line, ignores):
304+ # check if each ignore is in line
305+ for ignore in ignores:
306+ if ignore['ignore'] and ignore['ignore'] in line:
307+ ignoring_output.append("Ignoring: {}".format(line))
308+ # note: ignores can be updated since it is passed by reference
309+ # matched=True to keep using this ignore (see write_ignore_file)
310+ ignore['matched'] = True
311+ return True
312+ return False
313+
314+
315+def get_hp_controller_slots():
316+ """
317+ TODO search for the controller slot
318+
319+ Use the utility to determine the current controller slot(s) available for probing
320+ """
321+ slots = []
322+ cmd = ['/usr/sbin/hpssacli', 'ctrl', 'all', 'show']
323+ try:
324+ results = subprocess.check_output(cmd).decode('UTF-8')
325+ except subprocess.CalledProcessError:
326+ return slots
327+ for line in results.splitlines():
328+ if 'in Slot' in line:
329+ slots.append(line.split()[5])
330+ return slots
331+
332+
333+class HWCronArgumentParser(argparse.ArgumentParser):
334+ def __init__(
335+ self,
336+ def_write_file=None,
337+ *args,
338+ **kwargs
339+ ):
340+ super().__init__(
341+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
342+ *args,
343+ **kwargs
344+ )
345+ # self.prog is populated by ArgumentParser
346+ self._def_write_file = \
347+ def_write_file or '/var/lib/nagios/{}.out'.format(self.prog)
348+
349+ def parse_args(self, *args, **kwargs):
350+ self.add_argument(
351+ '-w', '--write', dest='write', type=str,
352+ default=self._def_write_file,
353+ help='cache tool output in this file',
354+ )
355+ super().parse_args(*args, **kwargs)
356+
357+
358+class HWCheckArgumentParser(argparse.ArgumentParser):
359+ def __init__(
360+ self,
361+ def_input_file=None,
362+ *args,
363+ **kwargs
364+ ):
365+ super().__init__(
366+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
367+ *args,
368+ **kwargs
369+ )
370+ # self.prog is populated by ArgumentParser
371+ self._def_input_file = \
372+ def_input_file or '/var/lib/nagios/{}.out'.format(self.prog)
373+
374+ def parse_args(self, *args, **kwargs):
375+ self.add_argument(
376+ '-i', '--input', dest='input_file', type=str,
377+ default=self._def_input_file,
378+ help='read cached tool output from this file',
379+ )
380+ super().parse_args(*args, **kwargs)
381+
382+
383+class HPArgumentParser(HWCronArgumentParser):
384+ def __init__(
385+ self,
386+ def_exclude_file=None,
387+ *args,
388+ **kwargs
389+ ):
390+ super().__init__(*args, **kwargs)
391+ self._def_exclude_file = \
392+ def_exclude_file or '/etc/nagios/{}.exclude.yaml'.format(self.prog)
393+
394+ def _expired(self, exclusion):
395+ return 'expires' in exclusion and exclusion['expires'] < datetime.now()
396+
397+ def parse_args(self, *args, **kwargs):
398+ self.add_argument(
399+ '--debug', dest='debug', action='store_true',
400+ help='Extra debugging',
401+ )
402+
403+ self.add_argument(
404+ '--exclude', dest='exclude', type=str, action='append',
405+ help='Errors to ignore (multiple)',
406+ )
407+
408+ self.add_argument(
409+ '--exclude-file', dest='exclude_file', type=str,
410+ default=self._def_exclude_file,
411+ help='YAML file with errors to ignore',
412+ )
413+
414+ # Ensure we initialize a namespace if needed,
415+ # and have a reference to it
416+ namespace = kwargs.get('namespace') or argparse.Namespace()
417+ kwargs['namespace'] = namespace
418+ # now parse args and put them in the namespace
419+ super().parse_args(*args, **kwargs)
420+
421+ if namespace.exclude is None:
422+ namespace.exclude = []
423+ if os.path.exists(namespace.exclude_file):
424+ with open(namespace.exclude_file) as f:
425+ for i in yaml.safe_load(f):
426+ if not self._expired(i):
427+ namespace.exclude.append(i['error'])
428+
429+ return namespace
430diff --git a/src/files/hplog/cron_hplog.py b/src/files/hplog/cron_hplog.py
431new file mode 100755
432index 0000000..d2b21cf
433--- /dev/null
434+++ b/src/files/hplog/cron_hplog.py
435@@ -0,0 +1,304 @@
436+#!/usr/bin/env python3
437+# ------------------------------------------------
438+# This file is juju managed
439+# -----------------------------------------------
440+
441+"""
442+Copyright (C) 2008-2013 James Troup <james.troup@canonical.com>
443+Copyright (c) 2020 Rewrite: Drew Freiberger <drew.freiberger@canonical.com>
444+
445+hplog lives in either /usr/sbin or /sbin depending on version of the
446+hp-health package and cron jobs don't get a sane $PATH
447+"""
448+import os
449+import subprocess
450+import sys
451+
452+try:
453+ # shared lib will be under $NAGIOS_PLUGIN_DIR at runtime
454+ from hw_health_lib import (
455+ read_ignore_file,
456+ write_ignore_file,
457+ ignore,
458+ HPArgumentParser,
459+ )
460+except ImportError:
461+ # shared lib will be under $CHARM_SOURCE_DIR/files/common during unit tests
462+ common_libs_dir = os.path.abspath(
463+ os.path.join(os.path.dirname(__file__), '..', 'common')
464+ )
465+ if common_libs_dir not in sys.path:
466+ sys.path.append(common_libs_dir)
467+ from hw_health_lib import (
468+ read_ignore_file,
469+ write_ignore_file,
470+ ignore,
471+ HPArgumentParser,
472+ )
473+
474+FLAG_PROCESSOR = {
475+ 't': 'parse_temperature',
476+ 'f': 'parse_fans',
477+ 'p': 'parse_power',
478+ 'v': 'save_log',
479+}
480+OUTPUT_FILE = '/var/lib/nagios/hplog.out'
481+EXCLUDE_FILE = '/etc/nagios/hplog.exclude.yaml'
482+DEBUG_FILE = '/var/lib/nagios/hplog.debug'
483+
484+
485+def parse_args(argv=None):
486+ parser = HPArgumentParser(
487+ prog='cron_hplog',
488+ def_write_file=OUTPUT_FILE,
489+ def_exclude_file=EXCLUDE_FILE
490+ )
491+ parser.add_argument(
492+ '-f', '--hplog_flags', dest='hplog_flags', type=str,
493+ default=','.join(FLAG_PROCESSOR.keys()),
494+ help='Flags to call hplog with',
495+ )
496+ parser.add_argument(
497+ '-l', '--log_path', dest='log_path', type=str,
498+ default=DEBUG_FILE,
499+ help='Where to write hplog -v output for troubleshooting',
500+ )
501+ parser.add_argument(
502+ '-s', '--single_psu', dest='single_psu', action='store_true',
503+ help='Do not alert on lack of PSU redundancy', default=False,
504+ )
505+ return parser.parse_args(args=argv)
506+
507+
508+def call_hplog(flag):
509+ """
510+ Grab hplogs from hpasm if available
511+
512+ The output of this cron script will be checked by
513+ nagios via check_hplog.py. The
514+ """
515+ env = 'export PATH=$PATH:/usr/sbin:/sbin'
516+
517+ cmd = 'hplog -{}'.format(flag)
518+ try:
519+ cmdline = '{}; {}'.format(env, cmd)
520+ output = subprocess.check_output(cmdline, shell=True).decode('UTF-8')
521+ except subprocess.CalledProcessError as e:
522+ return ('Failed running command "{}" Return Code {}: {}'
523+ ''.format(cmd, e.returncode, e.output))
524+
525+ funcname = FLAG_PROCESSOR[flag]
526+ return globals()[funcname](output)
527+
528+
529+def parse_temperature(result):
530+ """
531+ Temperature [hplog -t]
532+ 10 20 30 40 50 60 70
533+ 01234567890123456789012345678901234567890123456789012345678901234567890
534+ ID TYPE LOCATION STATUS CURRENT THRESHOLD
535+ 1 Basic Sensor I/O Zone Normal 102F/ 39C 158F/ 70C
536+ 2 Basic Sensor Ambient Normal 73F/ 23C 102F/ 39C
537+ 3 Basic Sensor CPU (1) Normal 95F/ 35C 260F/127C
538+ 4 Basic Sensor CPU (1) Normal 95F/ 35C 260F/127C
539+ 5 Basic Sensor Pwr. Supply Bay Normal 111F/ 44C 170F/ 77C
540+ 6 Basic Sensor CPU (2) Normal ---F/---C 260F/127C
541+ 7 Basic Sensor CPU (2) Normal ---F/---C 260F/127C
542+ """
543+ input_file = result.splitlines()
544+ if os.path.isfile('/etc/nagios/skip-cat-hp-temperature.txt'):
545+ return
546+ header_line = input_file.pop(0).strip()
547+ if header_line != "ID TYPE LOCATION STATUS CURRENT THRESHOLD": # noqa E501
548+ return "UNKNOWN Unrecognised header line in 'hplog -t' output"
549+ for line in input_file:
550+ line = line.rstrip()
551+ if not line:
552+ continue
553+ temp_id = line[0:3].strip()
554+ # temp_type = line[4:16].strip()
555+ temp_location = line[17:32].strip()
556+ temp_location = "%s (%s)" % (temp_location, temp_id)
557+ temp_status = line[33:41].strip()
558+ temp_current = line[42:51].split("/")[1].strip()
559+ temp_threshold = line[52:].split("/")[1].strip()
560+ if temp_status not in ["Normal", "Nominal", "Absent"]:
561+ return (
562+ "%s: temperature is '%s' (%s / %s)" % (temp_location,
563+ temp_status,
564+ temp_current,
565+ temp_threshold)
566+ )
567+
568+ return
569+
570+
571+def parse_fans(result):
572+ """
573+ Fans [hplog -f]
574+ 10 20 30 40 50 60 70
575+ 01234567890123456789012345678901234567890123456789012345678901234567890
576+ ID TYPE LOCATION STATUS REDUNDANT FAN SPEED
577+ 1 Var. Speed I/O Zone Normal Yes Medium ( 45)
578+ 2 Var. Speed I/O Zone Normal Yes Medium ( 45)
579+ 3 Var. Speed Processor Zone Normal Yes Medium ( 41)
580+ 4 Var. Speed Processor Zone Normal Yes Low ( 36)
581+ 5 Var. Speed Processor Zone Normal Yes Low ( 36)
582+ 6 Var. Speed Processor Zone Normal Yes Low ( 36)
583+ """
584+ input_file = result.splitlines()
585+ if os.path.isfile('/etc/nagios/skip-cat-hp-fans.txt'):
586+ return
587+ header_line = input_file.pop(0).strip()
588+ if header_line != "ID TYPE LOCATION STATUS REDUNDANT FAN SPEED": # noqa E501
589+ return "UNKNOWN Unrecognised header line in 'hplog -f' output"
590+
591+ ignore_file = '/etc/nagios/ignores/ignores-cat-hp-fans.txt'
592+ ignores = read_ignore_file(ignore_file)
593+ for line in input_file:
594+ line = line.rstrip()
595+ if not line:
596+ continue
597+ fan_id = line[0:3].strip()
598+ fan_type = line[4:16].strip()
599+ fan_location = line[17:32].strip()
600+ fan_location = "%s (%s)" % (fan_location, fan_id)
601+ fan_status = line[33:40].strip()
602+ fan_redundant = line[41:50].strip()
603+ fan_speed = line[51:].strip()
604+
605+ (return_now, msg) = process_fan_line(
606+ fan_type,
607+ fan_location,
608+ fan_status,
609+ fan_speed,
610+ fan_redundant,
611+ ignores
612+ )
613+ if return_now:
614+ return msg
615+
616+ write_ignore_file(ignores, ignore_file)
617+
618+
619+def process_fan_line(
620+ fan_type,
621+ fan_location,
622+ fan_status,
623+ fan_speed,
624+ fan_redundant,
625+ ignores
626+):
627+ if fan_type == "Basic Fan":
628+ return(False, None)
629+
630+ if fan_type not in ["Var. Speed", "Pwr. Supply", "Auto. Speed"]:
631+ return(True, "UNKNOWN %s: Unrecognised fan type '%s'" % (fan_location,
632+ fan_type))
633+
634+ if fan_status not in ["Normal", "Nominal"]:
635+ err = "%s: fans are '%s' (%s / Redundant: %s)" % (fan_location,
636+ fan_status,
637+ fan_speed,
638+ fan_redundant)
639+ if not ignore(err, ignores):
640+ return(True, err)
641+
642+ if fan_redundant not in ["Yes", "N/A"] and fan_type == "Var. Speed":
643+ err = "%s: fans are not redundant (%s / Status: %s)" % (fan_location,
644+ fan_speed,
645+ fan_redundant)
646+ if not ignore(err, ignores):
647+ return(True, err)
648+
649+ return(False, None)
650+
651+
652+def parse_power(result):
653+ """
654+ Power supplies [hplog -p]
655+ 10 20 30 40 50 60 70
656+ 01234567890123456789012345678901234567890123456789012345678901234567890
657+ ID TYPE LOCATION STATUS REDUNDANT
658+ 1 Standard Pwr. Supply Bay Normal Yes
659+ 2 Standard Pwr. Supply Bay Normal Yes
660+ """
661+ input_file = result.splitlines()
662+ if os.path.isfile('/etc/nagios/skip-cat-hp-power.txt'):
663+ return
664+ header_line = input_file.pop(0).strip()
665+ if header_line != "ID TYPE LOCATION STATUS REDUNDANT":
666+ return "UNKNOWN Unrecognised header line in 'hplog -p' output"
667+
668+ ignore_file = '/etc/nagios/ignores/ignores-cat-hp-power.txt'
669+ ignores = read_ignore_file(ignore_file)
670+
671+ for line in input_file:
672+ line = line.rstrip()
673+ if not line:
674+ continue
675+ power_id = line[0:3].strip()
676+ power_type = line[4:16].strip()
677+ power_location = line[17:32].strip()
678+ power_location = "%s (%s)" % (power_location, power_id)
679+ power_status = line[33:40].strip()
680+ # power_redundant = line[41:50].strip()
681+ if power_type != "Standard":
682+ err = "%s: Unrecognised power type '%s'" % (power_location,
683+ power_type)
684+ if not ignore(err, ignores):
685+ return 'UNKNOWN {}'.format(err)
686+ if not ARGS.single_psu and power_status not in ["Normal", "Nominal"]:
687+ err = "%s: power supply is '%s'" % (power_location, power_status)
688+ if not ignore(err, ignores):
689+ return err
690+ write_ignore_file(ignores, ignore_file)
691+
692+
693+def save_log(result):
694+ """
695+ Save full hplog -v output for troubleshooting after alert
696+ """
697+ with open(ARGS.log_path, 'w') as f:
698+ f.write(result)
699+ return
700+
701+
702+def main():
703+ global ARGS
704+ ARGS = parse_args()
705+
706+ try:
707+ # This matches hpasmlited on latest packages for bionic on <= gen9
708+ subprocess.check_call('ps -ef | grep -q hp[a]sm', shell=True)
709+ except subprocess.CalledProcessError as e:
710+ msg = (
711+ 'UNKNOWN hp[a]sm daemon not found running, cannot run hplog: '
712+ '{}'.format(e.output)
713+ )
714+ exit = 3
715+ else:
716+ errors = []
717+ for flag in ARGS.hplog_flags.split(','):
718+ log_output = call_hplog(flag)
719+ if log_output:
720+ errors.append(log_output)
721+
722+ if len(errors) > 0:
723+ msg = ('CRIT {} error(s): {}'
724+ ''.format(len(errors), ' - '.join(errors)))
725+ exit = 2
726+ else:
727+ msg = 'OK No errors found'
728+ exit = 0
729+
730+ if ARGS.write:
731+ with open(ARGS.write, 'w') as f:
732+ f.write(msg)
733+ else:
734+ print(msg)
735+ sys.exit(exit)
736+
737+
738+if __name__ == '__main__':
739+ main()
740diff --git a/src/files/ilorest/check_ilorest.py b/src/files/ilorest/check_ilorest.py
741new file mode 100755
742index 0000000..430c632
743--- /dev/null
744+++ b/src/files/ilorest/check_ilorest.py
745@@ -0,0 +1,74 @@
746+#!/usr/bin/env python3
747+# ------------------------------------------------
748+# This file is juju managed
749+# ------------------------------------------------
750+
751+# Copyright (C) 2008, 2009, 2012 James Troup <james.troup@canonical.com>
752+# Copyright (C) 2009 LaMont Jones <lamont.jones@canonical.com>
753+
754+###############################################################################
755+
756+from optparse import OptionParser
757+
758+from nagios_plugin3 import (check_file_freshness,
759+ try_check,
760+ WarnError,
761+ CriticalError)
762+
763+
764+###############################################################################
765+
766+
767+def check_and_open(filename, time_limit):
768+ check_file_freshness(filename, time_limit)
769+ return open(filename)
770+
771+
772+###############################################################################
773+
774+
775+def check_file_contents(input_filename, time_limit):
776+ input_file = check_and_open(input_filename, time_limit)
777+ for line in input_file.readlines():
778+ line = line.rstrip()
779+ if not line:
780+ continue
781+ if line.startswith("WARN"):
782+ raise WarnError(line)
783+ elif line.startswith("OK"):
784+ print(line)
785+ else:
786+ raise CriticalError(line)
787+
788+
789+###############################################################################
790+
791+
792+def main():
793+ parser = OptionParser()
794+ parser.add_option(
795+ "--time",
796+ dest="time_limit",
797+ help="freshness time limit [default=%default]",
798+ metavar="SECONDS",
799+ default=1200,
800+ type=int
801+ )
802+ parser.add_option(
803+ "-f", "--filename",
804+ dest="input_file",
805+ help=('file containing the output of '
806+ 'cron_ilorest.py [default=%default]'),
807+ metavar="FILE",
808+ default="/var/lib/nagios/ilorest.nagios",
809+ type=str
810+ )
811+
812+ (opts, args) = parser.parse_args()
813+
814+ try_check(check_file_contents, opts.input_file, opts.time_limit)
815+ print("All OK")
816+
817+
818+if __name__ == "__main__":
819+ main()
820diff --git a/src/files/ilorest/cron_ilorest.py b/src/files/ilorest/cron_ilorest.py
821new file mode 100755
822index 0000000..739438d
823--- /dev/null
824+++ b/src/files/ilorest/cron_ilorest.py
825@@ -0,0 +1,146 @@
826+#!/usr/bin/env python3
827+# ------------------------------------------------
828+# This file is juju managed
829+# ------------------------------------------------
830+
831+# ilorest interface for Gen10 HPE machines
832+# Original author: Ryan Finnie <ryan.finnie@canonical.com>
833+# Porting for the HW health charm: Andrea Ieri <andrea.ieri@canonical.com>
834+
835+import json
836+import sys
837+import os
838+from subprocess import check_output
839+
840+try:
841+ # shared lib will be under $NAGIOS_PLUGIN_DIR at runtime
842+ from hw_health_lib import HPArgumentParser
843+except ImportError:
844+ # shared lib will be under $CHARM_SOURCE_DIR/files/common during unit tests
845+ common_libs_dir = os.path.abspath(
846+ os.path.join(os.path.dirname(__file__), '..', 'common')
847+ )
848+ if common_libs_dir not in sys.path:
849+ sys.path.append(common_libs_dir)
850+ from hw_health_lib import HPArgumentParser
851+
852+
853+DEFAULT_SELECTORS = [
854+ 'Chassis',
855+ 'HpeSmartStorage',
856+ 'Memory',
857+ 'Power',
858+ 'Processor',
859+ 'Thermal',
860+]
861+
862+EXCLUDE_FILE = '/etc/nagios/cron_ilorest.exclude.yaml'
863+OUTPUT_FILE = '/var/lib/nagios/ilorest.nagios'
864+
865+
866+class CronILOrest:
867+ def __init__(self, argv=[]):
868+ self.args = self.parse_args(argv)
869+
870+ def parse_args(self, argv=None):
871+ parser = HPArgumentParser(
872+ prog='cron_ilorest',
873+ description=('Convert the output of ilorest into an appropriate '
874+ 'Nagios status line'),
875+ def_write_file=OUTPUT_FILE,
876+ def_exclude_file=EXCLUDE_FILE
877+ )
878+
879+ parser.add_argument(
880+ '--selectors', dest='selectors', type=str, nargs='+',
881+ default=DEFAULT_SELECTORS,
882+ help='iLO selectors to run',
883+ )
884+
885+ return parser.parse_args(args=argv)
886+
887+ def check_selector(self, selector):
888+ if self.args.debug:
889+ print('Checking selector {}'.format(selector), file=sys.stderr)
890+ ilorest_output = self._get_json_ilorest_output(selector)
891+
892+ errors = []
893+ jsonidx = -1
894+ # Disregard the first chunk of data, it's banner/debug/etc
895+ for jsondata in ilorest_output.split('\n{\n')[1:]:
896+ # The output will be one or more JSON defs
897+ jsonidx += 1
898+ j = json.loads('{' + jsondata)
899+ errors += self._walk_selector(j, [selector, str(jsonidx)])
900+ return errors
901+
902+ def _get_json_ilorest_output(self, selector):
903+ cmd = ['ilorest', 'list', '-j', '--selector={}'.format(selector)]
904+ return check_output(cmd).decode('UTF-8')
905+
906+ def _get_health_status_message(self, j, crumb_trail=[]):
907+ desc = j['Name']
908+ if 'SerialNumber' in j:
909+ desc += ' ({})'.format(j['SerialNumber'])
910+ state = j.get('Status', 'null').get('State', 'unknown')
911+ health = j.get('Status', 'null').get('Health', 'unknown')
912+ msg = '{} ({}): {} health {}'.format(' '.join(crumb_trail),
913+ desc,
914+ state,
915+ health)
916+ if self.args.debug:
917+ print(msg, file=sys.stderr)
918+
919+ if msg in self.args.exclude and self.args.debug:
920+ print('Ignoring excluded error: {}'.format(msg), file=sys.stderr)
921+ return []
922+ else:
923+ return [msg]
924+
925+ def _walk_selector(self, j, crumb_trail=[]):
926+ errors = []
927+ if j.get('Status') and j.get('Status').get('Health') != 'OK':
928+ errors.extend(self._get_health_status_message(j, crumb_trail))
929+
930+ for keyname in j.keys():
931+ # If the key value is a list, it might be a list of
932+ # dicts with Status data
933+ if type(j[keyname]) != list:
934+ continue
935+ for i in range(len(j[keyname])):
936+ if type(j[keyname][i]) != dict:
937+ continue
938+ if 'Status' not in j[keyname][i]:
939+ continue
940+ self._walk_selector(j[keyname][i],
941+ (crumb_trail + [keyname, str(i)]))
942+ return errors
943+
944+
945+def main(argv=None):
946+ cronilorest = CronILOrest(argv)
947+
948+ errors = [cronilorest.check_selector(selector)
949+ for selector in cronilorest.args.selectors]
950+
951+ if len(errors) > 0:
952+ msg = 'CRIT {} error(s): {}'.format(len(errors), ' - '.join(errors))
953+ exit = 2
954+ else:
955+ msg = 'OK No errors found'
956+ exit = 0
957+
958+ if cronilorest.args.write:
959+ if cronilorest.args.write == '-':
960+ print(msg)
961+ else:
962+ with open(cronilorest.args.write, 'w') as f:
963+ f.write(msg)
964+ else:
965+ # This should never happen since 'write' has a default value
966+ print(msg)
967+ sys.exit(exit)
968+
969+
970+if __name__ == '__main__':
971+ main(sys.argv[1:])
972diff --git a/src/files/mdadm/check_mdadm.py b/src/files/mdadm/check_mdadm.py
973index 56e37a8..29a82c6 100755
974--- a/src/files/mdadm/check_mdadm.py
975+++ b/src/files/mdadm/check_mdadm.py
976@@ -2,17 +2,33 @@
977 # -*- coding: us-ascii -*-
978
979 import os
980-
981+import sys
982+import argparse
983 from nagios_plugin3 import CriticalError, WarnError, UnknownError, try_check
984
985+try:
986+ # shared lib will be under $NAGIOS_PLUGIN_DIR at runtime
987+ from hw_health_lib import HWCheckArgumentParser
988+except ImportError:
989+ # shared lib will be under $CHARM_SOURCE_DIR/files/common during unit tests
990+ common_libs_dir = os.path.abspath(
991+ os.path.join(os.path.dirname(__file__), '..', 'common')
992+ )
993+ if common_libs_dir not in sys.path:
994+ sys.path.append(common_libs_dir)
995+ from hw_health_lib import HWCheckArgumentParser
996+
997 INPUT_FILE = '/var/lib/nagios/mdadm.out'
998+ARGS = argparse.Namespace()
999
1000
1001 def parse_output():
1002- if not os.path.exists(INPUT_FILE):
1003- raise UnknownError('UNKNOWN: file not found ({})'.format(INPUT_FILE))
1004+ if not os.path.exists(ARGS.input_file):
1005+ raise UnknownError(
1006+ 'UNKNOWN: file not found ({})'.format(ARGS.input_file)
1007+ )
1008
1009- with open(INPUT_FILE, 'r') as fd:
1010+ with open(ARGS.input_file, 'r') as fd:
1011 for line in fd.readlines():
1012 line = line.strip()
1013 if line.startswith('CRITICAL: '):
1014@@ -23,9 +39,18 @@ def parse_output():
1015 print(line)
1016
1017
1018-def main():
1019+def parse_args(argv=None):
1020+ parser = HWCheckArgumentParser(
1021+ prog='check_mdadm',
1022+ def_input_file=INPUT_FILE,
1023+ )
1024+ return parser.parse_args(args=argv, namespace=ARGS)
1025+
1026+
1027+def main(argv):
1028+ parse_args(argv)
1029 try_check(parse_output)
1030
1031
1032 if __name__ == '__main__':
1033- main()
1034+ main(sys.argv[1:])
1035diff --git a/src/files/mdadm/cron_mdadm.py b/src/files/mdadm/cron_mdadm.py
1036index 5aaafa4..ddf5d5e 100755
1037--- a/src/files/mdadm/cron_mdadm.py
1038+++ b/src/files/mdadm/cron_mdadm.py
1039@@ -5,18 +5,31 @@ import re
1040 import shutil
1041 import subprocess
1042 import sys
1043+import argparse
1044
1045-OUTPUT_FILE = '/var/lib/nagios/mdadm.out'
1046-TEMP_FILE = '/tmp/mdadm.out'
1047+try:
1048+ # shared lib will be under $NAGIOS_PLUGIN_DIR at runtime
1049+ from hw_health_lib import HWCronArgumentParser
1050+except ImportError:
1051+ # shared lib will be under $CHARM_SOURCE_DIR/files/common during unit tests
1052+ common_libs_dir = os.path.abspath(
1053+ os.path.join(os.path.dirname(__file__), '..', 'common')
1054+ )
1055+ if common_libs_dir not in sys.path:
1056+ sys.path.append(common_libs_dir)
1057+ from hw_health_lib import HWCronArgumentParser
1058+
1059+OUTPUT_FILE = "/var/lib/nagios/mdadm.out"
1060+TEMP_FILE = "/tmp/mdadm.out"
1061+ARGS = argparse.Namespace()
1062
1063
1064 def get_devices():
1065 if os.path.exists('/sbin/mdadm'):
1066 try:
1067- devices_raw = subprocess.check_output(
1068- ['/sbin/mdadm', '--detail', '--scan']
1069- )
1070- devices_re = re.compile(r'^ARRAY\s+([^ ]+) ')
1071+ cmd = ["/sbin/mdadm", "--detail", "--scan"]
1072+ devices_raw = subprocess.check_output(cmd)
1073+ devices_re = re.compile(r"^ARRAY\s+([^ ]+) ")
1074 devices = set()
1075 for line in devices_raw.decode().split('\n'):
1076 line = line.strip()
1077@@ -26,7 +39,7 @@ def get_devices():
1078 return devices
1079 except subprocess.CalledProcessError as error:
1080 rc = generate_output(
1081- 'CRITICAL: get_devices error - {}'.format(error)
1082+ "CRITICAL: get_devices error - {}".format(error)
1083 )
1084 if rc:
1085 sys.exit(0)
1086@@ -37,7 +50,7 @@ def generate_output(msg):
1087 try:
1088 with open(TEMP_FILE, 'w') as fd:
1089 fd.write(msg)
1090- shutil.move(TEMP_FILE, OUTPUT_FILE)
1091+ shutil.move(TEMP_FILE, ARGS.write)
1092 return True
1093 except Exception as error:
1094 print('Unable to generate output file:', error)
1095@@ -129,7 +142,7 @@ def parse_output():
1096 devices_stats = get_devices_stats(devices)
1097 except subprocess.CalledProcessError as error:
1098 return generate_output(
1099- 'WARNING: error executing mdadm: {}'.format(error)
1100+ "WARNING: error executing mdadm: {}".format(error)
1101 )
1102
1103 msg = []
1104@@ -178,5 +191,18 @@ def parse_output():
1105 return generate_output(msg)
1106
1107
1108-if __name__ == '__main__':
1109+def parse_args(argv=None):
1110+ parser = HWCronArgumentParser(
1111+ prog='cron_mdadm',
1112+ def_write_file=OUTPUT_FILE,
1113+ )
1114+ return parser.parse_args(args=argv, namespace=ARGS)
1115+
1116+
1117+def main(argv):
1118+ parse_args(argv)
1119 parse_output()
1120+
1121+
1122+if __name__ == "__main__":
1123+ main(sys.argv[1:])
1124diff --git a/src/files/megacli/check_megacli.py b/src/files/megacli/check_megacli.py
1125index ea28462..ed1e8d3 100755
1126--- a/src/files/megacli/check_megacli.py
1127+++ b/src/files/megacli/check_megacli.py
1128@@ -1,14 +1,31 @@
1129 #!/usr/bin/env python3
1130 # -*- coding: us-ascii -*-
1131
1132+import argparse
1133+import os
1134 import re
1135-
1136+import sys
1137 from nagios_plugin3 import CriticalError, WarnError, try_check
1138
1139+try:
1140+ # shared lib will be under $NAGIOS_PLUGIN_DIR at runtime
1141+ from hw_health_lib import HWCheckArgumentParser
1142+except ImportError:
1143+ # shared lib will be under $CHARM_SOURCE_DIR/files/common during unit tests
1144+ common_libs_dir = os.path.abspath(
1145+ os.path.join(os.path.dirname(__file__), '..', 'common')
1146+ )
1147+ if common_libs_dir not in sys.path:
1148+ sys.path.append(common_libs_dir)
1149+ from hw_health_lib import HWCheckArgumentParser
1150+
1151 INPUT_FILE = '/var/lib/nagios/megacli.out'
1152+ARGS = argparse.Namespace()
1153
1154
1155-def handle_results(nlines, match, critical, errors, num_ldrive, num_pdrive, policy):
1156+def handle_results(
1157+ nlines, match, critical, errors, num_ldrive, num_pdrive, policy
1158+):
1159 if nlines == 0:
1160 raise WarnError('WARNING: controller not found')
1161 elif not match:
1162@@ -17,7 +34,8 @@ def handle_results(nlines, match, critical, errors, num_ldrive, num_pdrive, poli
1163 if len(errors) > 0:
1164 msg = ', '.join([
1165 '{}({})'.format(cnt, vars()[cnt])
1166- for cnt in ('failed_ld', 'wrg_policy_ld') if vars().get(cnt, 0) > 0
1167+ for cnt in ('failed_ld', 'wrg_policy_ld')
1168+ if vars().get(cnt, 0) > 0
1169 ])
1170 msg += '; '.join(errors)
1171 else:
1172@@ -30,7 +48,8 @@ def handle_results(nlines, match, critical, errors, num_ldrive, num_pdrive, poli
1173 if num_ldrive == 0:
1174 msg = 'OK: no disks configured for RAID'
1175 else:
1176- msg = 'OK: Optimal, ldrives[{}], pdrives[{}]'.format(num_ldrive, num_pdrive)
1177+ msg = ('OK: Optimal, ldrives[{}], pdrives[{}]'
1178+ ''.format(num_ldrive, num_pdrive))
1179 if policy:
1180 msg += ', policy[{}]'.format(policy)
1181 print(msg)
1182@@ -57,7 +76,7 @@ def parse_output(policy=False):
1183 match = critical = False
1184 adapter_id = ldrive_id = None
1185
1186- with open(INPUT_FILE) as devices_raw:
1187+ with open(ARGS.input_file) as devices_raw:
1188 for line in devices_raw.readlines():
1189 if len(line.strip()) and not line.startswith('Exit Code'):
1190 nlines += 1
1191@@ -107,12 +126,23 @@ def parse_output(policy=False):
1192 critical = True
1193 continue
1194
1195- handle_results(nlines, match, critical, errors, num_ldrive, num_pdrive, policy)
1196+ handle_results(
1197+ nlines, match, critical, errors, num_ldrive, num_pdrive, policy
1198+ )
1199+
1200+
1201+def parse_args(argv=None):
1202+ parser = HWCheckArgumentParser(
1203+ prog='check_megacli',
1204+ def_input_file=INPUT_FILE,
1205+ )
1206+ return parser.parse_args(args=argv, namespace=ARGS)
1207
1208
1209-def main(policy=False):
1210+def main(argv, policy=False):
1211+ parse_args(argv)
1212 try_check(parse_output, policy)
1213
1214
1215 if __name__ == '__main__':
1216- main()
1217+ main(sys.argv[1:])
1218diff --git a/src/files/megacli/check_megacli.sh b/src/files/megacli/cron_megacli.sh
1219similarity index 50%
1220rename from src/files/megacli/check_megacli.sh
1221rename to src/files/megacli/cron_megacli.sh
1222index cca1f9f..313cb96 100755
1223--- a/src/files/megacli/check_megacli.sh
1224+++ b/src/files/megacli/cron_megacli.sh
1225@@ -3,9 +3,20 @@
1226 PATH="/snap/bin:/usr/local/bin:$PATH"
1227 FILE=/var/lib/nagios/megacli.out
1228 TMP_FILE=/tmp/megacli.out
1229+RC_FILE=/var/lib/nagios/megacli.rc
1230+CMD="megacli -LDInfo -LALL -aALL -NoLog"
1231
1232-megacli -LDInfo -LALL -aALL -NoLog 2>&1 > $TMP_FILE
1233+if [ $# -gt 0 ]; then
1234+ echo "This program will cache the output of '$CMD' as follows"
1235+ echo " stdout + stderr -> $FILE"
1236+ echo " return code -> $RC_FILE"
1237+ echo
1238+ echo "It does not accept any option or argument"
1239+ exit 0
1240+fi
1241+
1242+$CMD 2>&1 > $TMP_FILE
1243 RC=$?
1244-echo $RC > /var/lib/nagios/megacli.rc
1245+echo $RC > $RC_FILE
1246 mv $TMP_FILE $FILE
1247 exit 0
1248diff --git a/src/files/nvme/check_nvme.py b/src/files/nvme/check_nvme.py
1249index 7009c90..d4117c9 100755
1250--- a/src/files/nvme/check_nvme.py
1251+++ b/src/files/nvme/check_nvme.py
1252@@ -1,6 +1,8 @@
1253 #!/usr/bin/env python3
1254 # -*- coding: us-ascii -*-
1255
1256+import argparse
1257+import sys
1258 import glob
1259 import re
1260 import subprocess
1261@@ -50,5 +52,20 @@ def parse_output():
1262 print('\n'.join(alloutputs))
1263
1264
1265-if __name__ == '__main__':
1266+def parse_args(argv=None):
1267+ parser = argparse.ArgumentParser(
1268+ prog='check_nvme',
1269+ description=('this program reads the nvme smart-log and outputs an '
1270+ 'appropriate Nagios status line'),
1271+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
1272+ )
1273+ return parser.parse_args(argv)
1274+
1275+
1276+def main(argv):
1277+ parse_args(argv)
1278 try_check(parse_output)
1279+
1280+
1281+if __name__ == '__main__':
1282+ main(sys.argv[1:])
1283diff --git a/src/files/sas2ircu/check_sas2ircu.py b/src/files/sas2ircu/check_sas2ircu.py
1284index 7a4c861..b38e131 100755
1285--- a/src/files/sas2ircu/check_sas2ircu.py
1286+++ b/src/files/sas2ircu/check_sas2ircu.py
1287@@ -1,11 +1,26 @@
1288 #!/usr/bin/env python3
1289 # -*- coding: us-ascii -*-
1290
1291+import argparse
1292+import os
1293 import re
1294-
1295+import sys
1296 from nagios_plugin3 import CriticalError, WarnError, try_check
1297
1298+try:
1299+ # shared lib will be under $NAGIOS_PLUGIN_DIR at runtime
1300+ from hw_health_lib import HWCheckArgumentParser
1301+except ImportError:
1302+ # shared lib will be under $CHARM_SOURCE_DIR/files/common during unit tests
1303+ common_libs_dir = os.path.abspath(
1304+ os.path.join(os.path.dirname(__file__), '..', 'common')
1305+ )
1306+ if common_libs_dir not in sys.path:
1307+ sys.path.append(common_libs_dir)
1308+ from hw_health_lib import HWCheckArgumentParser
1309+
1310 INPUT_FILE = '/var/lib/nagios/sas2ircu.out'
1311+ARGS = argparse.Namespace()
1312
1313
1314 def parse_output():
1315@@ -22,7 +37,7 @@ def parse_output():
1316 devices = {}
1317 device = []
1318 critical = False
1319- with open(INPUT_FILE) as devices_raw:
1320+ with open(ARGS.input_file) as devices_raw:
1321 for line in devices_raw.readlines():
1322 line = line.rstrip()
1323 for cre in encl_slot_state_cre:
1324@@ -49,9 +64,18 @@ def parse_output():
1325 print('OK: {}'.format(msg))
1326
1327
1328-def main():
1329+def parse_args(argv=None):
1330+ parser = HWCheckArgumentParser(
1331+ prog='check_sas2ircu',
1332+ def_input_file=INPUT_FILE,
1333+ )
1334+ return parser.parse_args(args=argv, namespace=ARGS)
1335+
1336+
1337+def main(argv):
1338+ parse_args(argv)
1339 try_check(parse_output)
1340
1341
1342 if __name__ == '__main__':
1343- main()
1344+ main(sys.argv[1:])
1345diff --git a/src/files/sas2ircu/check_sas2ircu.sh b/src/files/sas2ircu/cron_sas2ircu.sh
1346similarity index 50%
1347rename from src/files/sas2ircu/check_sas2ircu.sh
1348rename to src/files/sas2ircu/cron_sas2ircu.sh
1349index 23594a0..544df4d 100755
1350--- a/src/files/sas2ircu/check_sas2ircu.sh
1351+++ b/src/files/sas2ircu/cron_sas2ircu.sh
1352@@ -3,9 +3,20 @@
1353 PATH="/snap/bin:/usr/local/bin:$PATH"
1354 FILE=/var/lib/nagios/sas2ircu.out
1355 TMP_FILE=/tmp/sas2ircu.out
1356+RC_FILE=/var/lib/nagios/sas2ircu.rc
1357+CMD="sas2ircu 0 DISPLAY"
1358
1359-sas2ircu 0 DISPLAY 2>&1 > $TMP_FILE
1360+if [ $# -gt 0 ]; then
1361+ echo "This program will cache the output of 'sas2ircu 0 DISPLAY' as follows"
1362+ echo " stdout + stderr -> $FILE"
1363+ echo " return code -> $RC_FILE"
1364+ echo
1365+ echo "It does not accept any option or argument"
1366+ exit 0
1367+fi
1368+
1369+$CMD 2>&1 > $TMP_FILE
1370 RC=$?
1371-echo $RC > /var/lib/nagios/sas2ircu.rc
1372+echo $RC > $RC_FILE
1373 mv $TMP_FILE $FILE
1374 exit 0
1375diff --git a/src/files/sas3ircu/check_sas3ircu.py b/src/files/sas3ircu/check_sas3ircu.py
1376index 3e49d84..d62a90f 100755
1377--- a/src/files/sas3ircu/check_sas3ircu.py
1378+++ b/src/files/sas3ircu/check_sas3ircu.py
1379@@ -1,11 +1,27 @@
1380 #!/usr/bin/env python3
1381 # -*- coding: us-ascii -*-
1382
1383+import argparse
1384+import os
1385 import re
1386+import sys
1387 from collections import defaultdict
1388 from nagios_plugin3 import CriticalError, WarnError, UnknownError, try_check
1389
1390+try:
1391+ # shared lib will be under $NAGIOS_PLUGIN_DIR at runtime
1392+ from hw_health_lib import HWCheckArgumentParser
1393+except ImportError:
1394+ # shared lib will be under $CHARM_SOURCE_DIR/files/common during unit tests
1395+ common_libs_dir = os.path.abspath(
1396+ os.path.join(os.path.dirname(__file__), '..', 'common')
1397+ )
1398+ if common_libs_dir not in sys.path:
1399+ sys.path.append(common_libs_dir)
1400+ from hw_health_lib import HWCheckArgumentParser
1401+
1402 INPUT_FILE = '/var/lib/nagios/sas3ircu.out'
1403+ARGS = argparse.Namespace()
1404
1405
1406 def parse_output(input_file):
1407@@ -51,10 +67,10 @@ def parse_output(input_file):
1408 enclosure = _kv_parse(sections['encl'])
1409
1410 return {
1411- 'controller': controller,
1412- 'volumes': volumes,
1413- 'disks': topology,
1414- 'enclosure': enclosure
1415+ 'controller': controller,
1416+ 'volumes': volumes,
1417+ 'disks': topology,
1418+ 'enclosure': enclosure,
1419 }
1420
1421
1422@@ -183,10 +199,19 @@ class Status:
1423 print(self._status)
1424
1425
1426-def main():
1427- data = parse_output(INPUT_FILE)
1428+def parse_args(argv=None):
1429+ parser = HWCheckArgumentParser(
1430+ prog='check_sas3ircu',
1431+ def_input_file=INPUT_FILE,
1432+ )
1433+ return parser.parse_args(args=argv, namespace=ARGS)
1434+
1435+
1436+def main(argv=None):
1437+ parse_args(argv)
1438+ data = parse_output(ARGS.input_file)
1439 try_check(eval_status, data)
1440
1441
1442 if __name__ == '__main__':
1443- main()
1444+ main(sys.argv[1:])
1445diff --git a/src/files/sas3ircu/check_sas3ircu.sh b/src/files/sas3ircu/cron_sas3ircu.sh
1446similarity index 50%
1447rename from src/files/sas3ircu/check_sas3ircu.sh
1448rename to src/files/sas3ircu/cron_sas3ircu.sh
1449index 0b26e96..a73c0f1 100755
1450--- a/src/files/sas3ircu/check_sas3ircu.sh
1451+++ b/src/files/sas3ircu/cron_sas3ircu.sh
1452@@ -3,9 +3,20 @@
1453 PATH="/snap/bin:/usr/local/bin:$PATH"
1454 FILE=/var/lib/nagios/sas3ircu.out
1455 TMP_FILE=/tmp/sas3ircu.out
1456+RC_FILE=/var/lib/nagios/sas3ircu.rc
1457+CMD="sas3ircu 0 DISPLAY"
1458
1459-sas3ircu 0 DISPLAY 2>&1 > $TMP_FILE
1460+if [ $# -gt 0 ]; then
1461+ echo "This program will cache the output of 'sas3ircu 0 DISPLAY' as follows"
1462+ echo " stdout + stderr -> $FILE"
1463+ echo " return code -> $RC_FILE"
1464+ echo
1465+ echo "It does not accept any option or argument"
1466+ exit 0
1467+fi
1468+
1469+$CMD 2>&1 > $TMP_FILE
1470 RC=$?
1471-echo $RC > /var/lib/nagios/sas3ircu.rc
1472+echo $RC > $RC_FILE
1473 mv $TMP_FILE $FILE
1474 exit 0
1475diff --git a/src/files/ssacli/cron_ssacli.py b/src/files/ssacli/cron_ssacli.py
1476new file mode 100755
1477index 0000000..94c7ef7
1478--- /dev/null
1479+++ b/src/files/ssacli/cron_ssacli.py
1480@@ -0,0 +1,178 @@
1481+#!/usr/bin/env python3
1482+# -----------------------------------------------
1483+# This file is juju managed
1484+# -----------------------------------------------
1485+
1486+# Copyright (C) 2008-2013 James Troup <james.troup@canonical.com>
1487+# Copyright (c) 2020 Rewrite: Drew Freiberger <drew.freiberger@canonical.com>
1488+
1489+# ssacli lives in either /usr/sbin or /sbin depending on version of the
1490+# package and cron jobs don't get a sane $PATH
1491+import os
1492+import re
1493+import subprocess
1494+import sys
1495+
1496+try:
1497+ # shared lib will be under $NAGIOS_PLUGIN_DIR at runtime
1498+ from hw_health_lib import (
1499+ read_ignore_file,
1500+ write_ignore_file,
1501+ ignore,
1502+ get_hp_controller_slots,
1503+ HPArgumentParser,
1504+ )
1505+except ImportError:
1506+ # shared lib will be under $CHARM_SOURCE_DIR/files/common during unit tests
1507+ common_libs_dir = os.path.abspath(
1508+ os.path.join(os.path.dirname(__file__), '..', 'common')
1509+ )
1510+ if common_libs_dir not in sys.path:
1511+ sys.path.append(common_libs_dir)
1512+ from hw_health_lib import (
1513+ read_ignore_file,
1514+ write_ignore_file,
1515+ ignore,
1516+ get_hp_controller_slots,
1517+ HPArgumentParser,
1518+ )
1519+
1520+SSACLI_BIN = '/opt/smartstorageadmin/ssacli/bin/ssacli'
1521+OUTPUT_FILE = '/var/lib/nagios/ssacli.out'
1522+EXCLUDE_FILE = '/etc/nagios/ssacli.exclude.yaml'
1523+
1524+
1525+def parse_args(argv=None):
1526+ parser = HPArgumentParser(
1527+ prog='cron_ssacli',
1528+ def_write_file=OUTPUT_FILE,
1529+ def_exclude_file=EXCLUDE_FILE
1530+ )
1531+ return parser.parse_args(args=argv)
1532+
1533+
1534+def check_array(slot):
1535+ """
1536+ RAID arrays [hpacucli]
1537+
1538+ logicaldrive 1 (68.3 GB, RAID RAID 1+0): OK
1539+ logicaldrive 1 (2.8 TB, RAID RAID 6 (ADG)): OK
1540+ physicaldrive 1:1 (box 1:bay 1, 72 GB): OK
1541+ physicaldrive 1:2 (box 1:bay 2, 72 GB): OK
1542+ """
1543+ if os.path.isfile('/etc/nagios/skip-cat-hp-array.txt'):
1544+ return
1545+
1546+ cmd = (
1547+ '{ssacli} ctrl slot={slot} ld all show status; '
1548+ '{ssacli} ctrl slot={slot} pd all show status'.format(ssacli=SSACLI_BIN, slot=slot)
1549+ )
1550+ try:
1551+ result = subprocess.check_output(cmd, shell=True).decode('UTF-8')
1552+ return _parse_array_output(result)
1553+ except subprocess.CalledProcessError as e:
1554+ return (
1555+ 'UNKNOWN Call to ssacli to show ld/pd info failed. '
1556+ 'Array Slot {} - Return Code {} - {}'
1557+ ''.format(slot, e.returncode, e.output)
1558+ )
1559+
1560+
1561+def _parse_array_output(output):
1562+ innocuous_errors = re.compile(r'^Error: The specified (device|controller) '
1563+ 'does not have any (logical|physical)')
1564+ drive_status_line = re.compile(r'^\s*(logicaldrive|physicaldrive)')
1565+ ignore_file = '/etc/nagios/ignores/ignores-cat-hp-array.txt'
1566+ ignores = read_ignore_file(ignore_file)
1567+
1568+ for line in output.splitlines():
1569+ line = line.strip()
1570+ if not line or innocuous_errors.search(line) or not drive_status_line.search(line):
1571+ continue
1572+ (drivetype, number) = line.split()[:2]
1573+ status = line.split('):')[1].lstrip().upper()
1574+ if status != 'OK':
1575+ err = '{} {} is "{}"'.format(drivetype, number, status)
1576+ if not ignore(err, ignores):
1577+ return err
1578+ write_ignore_file(ignores, ignore_file)
1579+
1580+
1581+def check_controller(slot):
1582+ """
1583+ RAID controllers [hpacucli]
1584+
1585+ Smart Array 6400 in Slot 1
1586+ Controller Status: OK
1587+ Cache Status: OK
1588+ Battery Status: Failed (Replace Batteries)
1589+ """
1590+ if os.path.isfile('/etc/nagios/skip-cat-hp-controller.txt'):
1591+ return
1592+
1593+ cmd = '{ssacli} ctrl slot={slot} show status'.format(ssacli=SSACLI_BIN, slot=slot)
1594+ try:
1595+ result = subprocess.check_output(cmd, shell=True).decode('UTF-8')
1596+ return _parse_controller_output(result)
1597+ except subprocess.CalledProcessError as e:
1598+ return (
1599+ 'UNKNOWN Call to ssacli to show ld/pd info failed. '
1600+ 'Array Slot {} - Return Code {} - {}'
1601+ ''.format(slot, e.returncode, e.output)
1602+ )
1603+
1604+
1605+def _parse_controller_output(output):
1606+ controller = "Unknown"
1607+ ignore_file = '/etc/nagios/ignores/ignores-cat-hp-controller.txt'
1608+ ignores = read_ignore_file(ignore_file)
1609+ for line in output.splitlines():
1610+ line = line.strip()
1611+ if not line:
1612+ continue
1613+ if line.startswith("Smart Array") or line.startswith("Smart HBA"):
1614+ controller = line
1615+ else:
1616+ if ":" not in line:
1617+ err = "UNKNOWN Unrecognised line for controller '%s'" % (line)
1618+ return err
1619+ (part, status) = line.split(":")
1620+ status = status.strip().upper()
1621+ if status != "OK":
1622+ err = "%s on %s is '%s'" % (part, controller, status)
1623+ if not ignore(err, ignores):
1624+ return err
1625+ write_ignore_file(ignores, ignore_file)
1626+
1627+
1628+def main():
1629+ global ARGS
1630+ ARGS = parse_args()
1631+
1632+ slots = get_hp_controller_slots()
1633+ if not slots:
1634+ msg = 'OK: no controller/array found to check'
1635+ exit = 0
1636+
1637+ errors = []
1638+ for slot in slots:
1639+ errors += check_controller(slot)
1640+ errors += check_array(slot)
1641+
1642+ if len(errors) > 0:
1643+ msg = 'CRIT {} error(s): {}'.format(len(errors), ' - '.join(errors))
1644+ exit = 2
1645+ else:
1646+ msg = 'OK No errors found'
1647+ exit = 0
1648+
1649+ if ARGS.write:
1650+ with open(ARGS.write, 'w') as f:
1651+ f.write(msg)
1652+ else:
1653+ print(msg)
1654+ sys.exit(exit)
1655+
1656+
1657+if __name__ == '__main__':
1658+ main()
1659diff --git a/src/lib/hwhealth/discovery/lshw.py b/src/lib/hwhealth/discovery/lshw.py
1660index 7a2af8f..c2f6653 100644
1661--- a/src/lib/hwhealth/discovery/lshw.py
1662+++ b/src/lib/hwhealth/discovery/lshw.py
1663@@ -18,7 +18,8 @@ class Hardware(object):
1664 hwinfo = json.load(fd)
1665 else:
1666 output = subprocess.check_output(['lshw', '-json'])
1667- # Note(aluria): py35 does not support extra args on subprocess.check_output
1668+ # Note(aluria): py35 does not support extra args on
1669+ # subprocess.check_output
1670 output_str = output.decode(errors='ignore')
1671 hwinfo = json.loads(output_str)
1672 with open(self.__filename, 'w') as fd:
1673@@ -26,13 +27,16 @@ class Hardware(object):
1674
1675 return hwinfo
1676 except PermissionError as error:
1677- hookenv.log('lshw io error: {}'.format(error), hookenv.ERROR)
1678+ hookenv.log('lshw io error: {}'.format(error),
1679+ hookenv.ERROR)
1680 return {}
1681 except subprocess.CalledProcessError as error:
1682- hookenv.log('lshw subprocess error: {}'.format(error), hookenv.ERROR)
1683+ hookenv.log('lshw subprocess error: {}'.format(error),
1684+ hookenv.ERROR)
1685 return {}
1686 except json.JSONDecodeError as error:
1687- hookenv.log('lshw json error: {}'.format(error), hookenv.ERROR)
1688+ hookenv.log('lshw json error: {}'.format(error),
1689+ hookenv.ERROR)
1690 return {}
1691
1692 @property
1693@@ -64,7 +68,9 @@ class Hardware(object):
1694 bridge_class_items = []
1695 for item in bridge_item.get('children', [{}]):
1696 if item.get('class', '') == 'bridge':
1697- bridge_class_items.extend(self._get_inspect_bridges(item, bridge_class))
1698+ bridge_class_items.extend(
1699+ self._get_inspect_bridges(item, bridge_class)
1700+ )
1701 elif item.get('class', '') == bridge_class:
1702 bridge_class_items.append(item)
1703 return bridge_class_items
1704@@ -74,8 +80,8 @@ class Hardware(object):
1705 """returns all the storage classes.
1706
1707 Function is used by get_storage_class_info and get_disk_class_info.
1708- The aim of this function is to easily parse products to detect which tool(s)
1709- need to be used.
1710+ The aim of this function is to easily parse products to detect which
1711+ tool(s) need to be used.
1712 """
1713 storage = []
1714 # system -> bus -> bridge -> storage
1715@@ -87,7 +93,9 @@ class Hardware(object):
1716 continue
1717 for item in bridge.get('children', [{}]):
1718 if item.get('class', '') == 'bridge':
1719- storage.extend(self._get_inspect_bridges(item, 'storage'))
1720+ storage.extend(
1721+ self._get_inspect_bridges(item, 'storage')
1722+ )
1723 elif item.get('class', '') == 'storage':
1724 storage.append(item)
1725 return storage
1726@@ -96,16 +104,18 @@ class Hardware(object):
1727 def get_storage_class_info(self):
1728 """returns a list of storage controllers
1729
1730- Storage controllers have the following keys: vendor, product name, businfo and linux driver
1731- The aim of the function is to easily parse products to detect which tool(s)
1732- need to be used.
1733+ Storage controllers have the following keys: vendor, product name,
1734+ businfo and linux driver The aim of the function is to easily parse
1735+ products to detect which tool(s) need to be used.
1736 """
1737 keys = 'vendor product businfo'.split()
1738 config_keys = ['driver']
1739 storage = []
1740 for item in self._get_storage_class:
1741 storage_item = dict([(k, item.get(k)) for k in keys])
1742- storage_item.update(dict([(k, item.get('configuration', {}).get(k)) for k in config_keys]))
1743+ storage_item.update(
1744+ dict([(k, item.get('configuration', {}).get(k))
1745+ for k in config_keys]))
1746 storage_item.update({'has_children': 'children' in item})
1747 storage.append(storage_item)
1748
1749@@ -115,12 +125,13 @@ class Hardware(object):
1750 def get_disk_class_info(self):
1751 """returns a list of storage devices
1752
1753- Storage devices have the following keys: product name, serial number, PCI bus info,
1754- physical device and ID, size (capacity), logicalname (/dev/sdX) and storage parent
1755+ Storage devices have the following keys: product name, serial number,
1756+ PCI bus info, physical device and ID, size (capacity), logicalname
1757+ (/dev/sdX) and storage parent
1758 (ie. RAID controller)
1759
1760- The aim of the function is to easily parse products to detect which tool(s)
1761- need to be used.
1762+ The aim of the function is to easily parse products to detect which
1763+ tool(s) need to be used.
1764 """
1765 keys = 'product serial businfo physid dev size logicalname'.split()
1766 disks = []
1767@@ -139,8 +150,8 @@ class Hardware(object):
1768 vendor, product name, businfo, logicalname (eno1...), serial number,
1769 linux driver and driver version, NIC firmware version and speed
1770
1771- The aim of the function is to easily parse products to detect which tool(s)
1772- need to be used.
1773+ The aim of the function is to easily parse products to detect which
1774+ tool(s) need to be used.
1775 """
1776 keys = 'vendor product businfo logicalname serial'.split()
1777 config_keys = 'driver driverversion firmware speed'.split()
1778@@ -161,7 +172,9 @@ class Hardware(object):
1779 nics_filtered = []
1780 for nic in nics:
1781 nic_item = dict([(k, nic.get(k)) for k in keys])
1782- nic_item.update(dict([(k, nic.get('configuration', {}).get(k)) for k in config_keys]))
1783+ nic_item.update(
1784+ dict([(k, nic.get('configuration', {}).get(k))
1785+ for k in config_keys]))
1786 nics_filtered.append(nic_item)
1787 return nics_filtered
1788
1789@@ -169,22 +182,26 @@ class Hardware(object):
1790 def formatted_system_info(self):
1791 ctxt = self.get_system
1792 return (
1793- '{description}: vendor[{vendor}], product_name[{product}], version[{version}]'
1794- ', serial[{serial}], hostname[{hostname}]').format(**ctxt)
1795+ '{description}: vendor[{vendor}], product_name[{product}], '
1796+ 'version[{version}], serial[{serial}], hostname[{hostname}]'
1797+ ).format(**ctxt)
1798
1799 @property
1800 def formatted_motherboard_info(self):
1801 return '\n'.join([
1802- '{description}: vendor[{vendor}], product_name[{product}], version[{version}]'
1803- ', serial[{serial}]'.format(**ctxt) for ctxt in self.get_motherboard])
1804+ '{description}: vendor[{vendor}], product_name[{product}], '
1805+ 'version[{version}], serial[{serial}]'.format(**ctxt)
1806+ for ctxt in self.get_motherboard])
1807
1808 @property
1809 def formatted_storage_class_info(self):
1810- LINE = 'driver[{driver}], businfo[{businfo}], has_children[{has_children}]'
1811+ LINE = ('driver[{driver}], businfo[{businfo}], '
1812+ 'has_children[{has_children}]')
1813 ctxts = []
1814 for ctxt in self.get_storage_class_info:
1815 if ctxt.get('vendor') and ctxt.get('product'):
1816- tmpl = 'Storage class: vendor[{vendor}], product_name[{product}], ' + LINE
1817+ tmpl = ('Storage class: vendor[{vendor}],'
1818+ 'product_name[{product}], ') + LINE
1819 else:
1820 tmpl = 'Storage class: {}'.format(LINE)
1821 ctxts.append(tmpl.format(**ctxt))
1822@@ -195,9 +212,10 @@ class Hardware(object):
1823 @property
1824 def formatted_disk_class_info(self):
1825 return '\n'.join([
1826- 'Disk class: ld[{logicalname}], dev[{dev}], physid[{physid}], businfo[{businfo}]'
1827- ', product_name[{product}], serial[{serial}], size[{size}]'
1828- ', storage_parent[{storage_parent}]'.format(**ctxt) for ctxt in self.get_disk_class_info
1829+ 'Disk class: ld[{logicalname}], dev[{dev}], physid[{physid}], '
1830+ 'businfo[{businfo}], product_name[{product}], serial[{serial}], '
1831+ 'size[{size}], storage_parent[{storage_parent}]'.format(**ctxt)
1832+ for ctxt in self.get_disk_class_info
1833 ])
1834
1835 @property
1836@@ -206,7 +224,8 @@ class Hardware(object):
1837 'NIC: iface[{logicalname}], businfo[{businfo}], vendor[{vendor}]'
1838 ', product_name[{product}], firmware[{firmware}], driver[{driver}'
1839 ', {driverversion}], serial[{serial}]'
1840- ', speed[{speed}]'.format(**ctxt) for ctxt in self.get_network_class_info
1841+ ', speed[{speed}]'.format(**ctxt)
1842+ for ctxt in self.get_network_class_info
1843 ])
1844
1845
1846diff --git a/src/lib/hwhealth/discovery/supported_vendors.py b/src/lib/hwhealth/discovery/supported_vendors.py
1847index 8dc588e..ef37a5a 100644
1848--- a/src/lib/hwhealth/discovery/supported_vendors.py
1849+++ b/src/lib/hwhealth/discovery/supported_vendors.py
1850@@ -1,12 +1,12 @@
1851 # -*- coding: us-ascii -*-
1852 from hwhealth import tools
1853
1854-SUPPORTED_VENDORS = {
1855+SUPPORTED_STORAGE = {
1856 'LSI Logic / Symbios Logic': {
1857 'SAS2308 PCI-Express Fusion-MPT SAS-2': tools.Sas2Ircu,
1858 'SAS3008 PCI-Express Fusion-MPT SAS-3': tools.Sas3Ircu,
1859 'MegaRAID SAS-3 3108 [Invader]': tools.MegaCLI,
1860- },
1861+ },
1862 # 'Mellanox Technologies': {
1863 # 'MT27710 Family [ConnectX-4 Lx]': lambda: 'mlxconfig',
1864 # 'MT27700 Family [ConnectX-4]': lambda: 'mlxconfig',
1865@@ -18,9 +18,21 @@ SUPPORTED_VENDORS = {
1866 'NVMe SSD Controller SM961/PM961': tools.Nvme,
1867 'NVMe SSD Controller 172Xa/172Xb': tools.Nvme,
1868 },
1869- # 'Hewlett-Packard Company': {
1870- # 'Smart Array Gen9 Controllers': tools.HP,
1871- # }
1872+ 'Hewlett-Packard Company': {
1873+ 'Smart Array Gen9 Controllers': tools.SsaCli,
1874+ 'Smart Storage PQI 12G SAS/PCIe 3': tools.ILOrest,
1875+ },
1876+}
1877+
1878+SUPPORTED_SYSTEMS = {
1879+ 'HPE': {
1880+ # tools.HpLog, # not sure if this works on gen10+
1881+ tools.ILOrest,
1882+ },
1883+ 'HP': {
1884+ tools.HpLog,
1885+ tools.SsaCli,
1886+ }
1887 }
1888
1889 SUPPORTED_DRIVERS = {
1890diff --git a/src/lib/hwhealth/hwdiscovery.py b/src/lib/hwhealth/hwdiscovery.py
1891index f63a025..c23ee13 100644
1892--- a/src/lib/hwhealth/hwdiscovery.py
1893+++ b/src/lib/hwhealth/hwdiscovery.py
1894@@ -1,28 +1,40 @@
1895 # -*- coding: us-ascii -*-
1896+"""Discover hardware and determine tools selection."""
1897 import os
1898 import re
1899 import subprocess
1900+from itertools import chain
1901
1902 from hwhealth import tools
1903 from hwhealth.discovery.lshw import Hardware
1904-from hwhealth.discovery.supported_vendors import SUPPORTED_VENDORS, SUPPORTED_DRIVERS
1905+from hwhealth.discovery.supported_vendors import (
1906+ SUPPORTED_STORAGE,
1907+ SUPPORTED_DRIVERS,
1908+ SUPPORTED_SYSTEMS
1909+)
1910
1911 from charmhelpers.core import hookenv
1912
1913
1914 def get_tools(manufacturer='auto'):
1915+ """Return list of tool classes relevent for the current hardware.
1916+
1917+ In testing, we set manufacturer = test in order to test all tools classes.
1918+ Filtering added for known bad tools that don't work on all series combinations.
1919+ """
1920 if manufacturer == 'test':
1921 # Return all possible tools to aid testing
1922- return [
1923- # tools.HPE(), # NotImplementedYet
1924- # tools.HP(), # NotImplementedYet
1925- tools.Mdadm(),
1926- tools.MegaCLI(),
1927- tools.Nvme(),
1928- tools.Ipmi(),
1929- tools.Sas2Ircu(),
1930- tools.Sas3Ircu(),
1931- ]
1932+ storage_tools = {tool
1933+ for vendor in SUPPORTED_STORAGE.values()
1934+ for tool in vendor.values()}
1935+ # Some system vendors have multiple tools, have to iterate sets
1936+ system_tools = set(chain.from_iterable(SUPPORTED_SYSTEMS.values()))
1937+ driver_tools = set(SUPPORTED_DRIVERS.values())
1938+ all_tools = storage_tools | system_tools | driver_tools
1939+ series_filtered_tools = set(
1940+ tool for tool in all_tools if tool.is_series_supported()
1941+ )
1942+ return series_filtered_tools
1943 elif manufacturer == 'auto':
1944 return _get_tools()
1945 else:
1946@@ -30,22 +42,28 @@ def get_tools(manufacturer='auto'):
1947
1948
1949 def _get_tools():
1950+ """Return list of tool classes relevent for the current hardware."""
1951 hwinfo = Hardware()
1952 toolset = set()
1953 for storage in hwinfo.get_storage_class_info:
1954- if storage.get('vendor'):
1955- if storage.get('product') in SUPPORTED_VENDORS.get(storage['vendor'], []):
1956- toolset.add(SUPPORTED_VENDORS[storage['vendor']][storage['product']])
1957- else:
1958- hookenv.log('Product not supported: [{vendor}][{product}]'.format(**storage),
1959- hookenv.DEBUG)
1960+ vendor = storage.get('vendor')
1961+ product = storage.get('product')
1962+ tool = SUPPORTED_STORAGE.get(vendor, {}).get(product)
1963+ if isinstance(tool, list) or isinstance(tool, set):
1964+ toolset.update(tool)
1965+ elif tool:
1966+ toolset.add(tool)
1967+ else:
1968+ hookenv.log('Product not supported: [{}][{}]'
1969+ ''.format(vendor, product), hookenv.DEBUG)
1970
1971 driver = storage.get('driver')
1972 if driver:
1973 if driver in SUPPORTED_DRIVERS:
1974 toolset.add(SUPPORTED_DRIVERS[driver])
1975 continue
1976- hookenv.log('Driver not supported: {}'.format(driver), hookenv.DEBUG)
1977+ hookenv.log('Driver not supported: {}'.format(driver),
1978+ hookenv.DEBUG)
1979
1980 # SW RAID?
1981 if _supports_mdadm():
1982@@ -54,22 +72,37 @@ def _get_tools():
1983 if hookenv.config('enable_ipmi'):
1984 toolset.add(tools.Ipmi)
1985
1986- executed_toolset = set([tool() for tool in toolset])
1987+ system_vendor = hwinfo.get_system.get('vendor')
1988+ tool = SUPPORTED_SYSTEMS.get(system_vendor)
1989+ if isinstance(tool, list) or isinstance(tool, set):
1990+ toolset.update(tool)
1991+ elif tool:
1992+ toolset.add(tool)
1993+ else:
1994+ hookenv.log('System vendor not supported: {}'.format(system_vendor),
1995+ hookenv.DEBUG)
1996+
1997+ executed_toolset = set([tool() for tool in toolset if tool.is_series_supported])
1998 return executed_toolset
1999
2000
2001 def _supports_mdadm():
2002- """scans for mdadm devices and returns True when the first one is found (otherwise, it returns False)
2003+ """Detect known mdadm supported storage devices.
2004+
2005+ Returns True when the first one is found; otherwise, it returns False)
2006 """
2007 if os.path.exists('/sbin/mdadm'):
2008 try:
2009- devices_raw = subprocess.check_output(['/sbin/mdadm', '--detail', '--scan'])
2010+ devices_raw = subprocess.check_output(
2011+ ['/sbin/mdadm', '--detail', '--scan']
2012+ )
2013 devices_re = re.compile(r'^ARRAY\s+(\S+) ')
2014 for line in devices_raw.splitlines():
2015 line = line.decode().strip()
2016 raid_dev = devices_re.search(line)
2017 if raid_dev:
2018- hookenv.log("Found md raid array {}".format(raid_dev.group(1)))
2019+ hookenv.log("Found md raid array {}"
2020+ "".format(raid_dev.group(1)))
2021 return True
2022 except Exception as e:
2023 hookenv.log("mdadm scan failed with {}".format(e))
2024diff --git a/src/lib/hwhealth/tools.py b/src/lib/hwhealth/tools.py
2025index 970ac7d..e90cb6b 100644
2026--- a/src/lib/hwhealth/tools.py
2027+++ b/src/lib/hwhealth/tools.py
2028@@ -16,7 +16,9 @@ from tempfile import TemporaryDirectory
2029 from charmhelpers import fetch
2030 from charmhelpers.contrib.charmsupport import nrpe
2031 from charmhelpers.core import hookenv
2032+from charmhelpers.core.host import lsb_release
2033 from charmhelpers.core.templating import render
2034+from charms import apt
2035
2036
2037 class JujuResourceNotFound(Exception):
2038@@ -61,6 +63,7 @@ class Tool():
2039 CRONJOB_SCRIPT_MODE = 0o100755
2040 CRONJOB_SCRIPT_UID = 0
2041 CRONJOB_SCRIPT_GID = 0
2042+ CRONJOB_OUTPUT_DIR = '/var/lib/nagios'
2043 NRPE_PLUGINS_DIR = '/usr/local/lib/nagios/plugins'
2044 NRPE_PLUGINS_MODE = 0o100755
2045 NRPE_PLUGINS_UID = 0
2046@@ -69,27 +72,47 @@ class Tool():
2047 SUDOERS_MODE = 0o100440
2048 SUDOERS_UID = 0
2049 SUDOERS_GID = 0
2050+ SUPPORTED_SERIES = ['xenial', 'bionic', 'focal']
2051 TOOLS_DIR = '/usr/local/bin'
2052 TOOLS_MODE = 0o100755
2053 TOOLS_UID = 0
2054 TOOLS_GID = 0
2055
2056- def __init__(self, shortname, nrpe_opts=''):
2057+ def __init__(
2058+ self,
2059+ shortname=None,
2060+ nrpe_opts='',
2061+ nrpe_script=None,
2062+ nrpe_script_dir=None,
2063+ cron_script=None,
2064+ cron_script_dir=None,
2065+ cron_script_args=None,
2066+ ):
2067 self._nagios_hostname = nrpe.get_nagios_hostname()
2068 self._nrpe_opts = nrpe_opts
2069- self._shortname = shortname
2070+ self._shortname = (shortname if shortname
2071+ else self.__class__.__name__.lower())
2072 self._files_dir = os.path.join(hookenv.charm_dir(),
2073 'files',
2074 self._shortname)
2075+ self._nrpe_script = (nrpe_script if nrpe_script
2076+ else 'check_{}.py'.format(self._shortname))
2077+ self._nrpe_script_dir = (nrpe_script_dir if nrpe_script_dir
2078+ else self._files_dir)
2079+ self._cron_script = (cron_script if cron_script
2080+ else 'cron_{}.py'.format(self._shortname))
2081+ self._cron_script_dir = (cron_script_dir if cron_script_dir
2082+ else self._files_dir)
2083 self._templates_dir = os.path.join(hookenv.charm_dir(),
2084 'templates',
2085 self._shortname)
2086- self._nrpe_script = 'check_{}'.format(shortname)
2087- self._cronjob_script = None
2088- self._cronjob_script_args = None
2089+ self._common_libs_dir = os.path.join(hookenv.charm_dir(),
2090+ 'files/common')
2091+ self._common_libs = ['hw_health_lib.py']
2092+ self._cron_script_args = cron_script_args
2093
2094 def _install_nrpe_plugin(self):
2095- src = os.path.join(self._files_dir, self._nrpe_script)
2096+ src = os.path.join(self._nrpe_script_dir, self._nrpe_script)
2097 dst = shutil.copy(src, self.NRPE_PLUGINS_DIR)
2098 os.chmod(dst, self.NRPE_PLUGINS_MODE)
2099 os.chown(dst, uid=self.NRPE_PLUGINS_UID, gid=self.NRPE_PLUGINS_GID)
2100@@ -100,6 +123,21 @@ class Tool():
2101 )
2102 return dst
2103
2104+ def _install_common_libs(self):
2105+ dsts = []
2106+ for lib in self._common_libs:
2107+ src = os.path.join(self._common_libs_dir, lib)
2108+ dst = shutil.copy(src, self.NRPE_PLUGINS_DIR)
2109+ os.chmod(dst, self.NRPE_PLUGINS_MODE)
2110+ os.chown(dst, uid=self.NRPE_PLUGINS_UID, gid=self.NRPE_PLUGINS_GID)
2111+ hookenv.log(
2112+ 'Common Library {} for tool [{}] installed at as {}'
2113+ ''.format(lib, self._shortname, dst),
2114+ hookenv.DEBUG
2115+ )
2116+ dsts.append(dst)
2117+ return dsts
2118+
2119 def _remove_nrpe_plugin(self):
2120 plugin_path = os.path.join(self.NRPE_PLUGINS_DIR, self._nrpe_script)
2121 if not os.path.exists(plugin_path):
2122@@ -111,10 +149,10 @@ class Tool():
2123 )
2124
2125 def configure_nrpe_check(self, nrpe_setup):
2126- cmd = ' '.join([self._nrpe_script, self._nrpe_opts])
2127+ cmd = ' '.join([os.path.basename(self._nrpe_script), self._nrpe_opts])
2128 nrpe_setup.add_check(
2129 shortname=self._shortname,
2130- description='{} Hardware Health',
2131+ description='{} Hardware Health'.format(self._shortname),
2132 check_cmd=cmd,
2133 )
2134 hookenv.log(
2135@@ -136,6 +174,7 @@ class Tool():
2136 )
2137
2138 def install(self):
2139+ self._install_common_libs()
2140 self._install_nrpe_plugin()
2141 hookenv.log('Installed tool [{}]'.format(self._shortname))
2142
2143@@ -144,30 +183,45 @@ class Tool():
2144 self._remove_nrpe_plugin()
2145 hookenv.log('Removed tool [{}]'.format(self._shortname))
2146
2147+ @classmethod
2148+ def is_series_supported(cls):
2149+ series = lsb_release()['DISTRIB_CODENAME']
2150+
2151+ # BUG(lp#1890652) The following works around xenial layer-apt bug during test
2152+ if (
2153+ hookenv.config("manufacturer") == "test"
2154+ and series == "xenial"
2155+ and isinstance(cls, AptVendorTool)
2156+ ):
2157+ return False
2158+
2159+ return series in cls.SUPPORTED_SERIES
2160+
2161 def _install_cronjob(self, cron_user='root'):
2162- assert self._cronjob_script is not None
2163+ assert self._cron_script is not None
2164
2165 # Copy the cronjob script to the nagios plugins directory
2166- src = os.path.join(self._files_dir, self._cronjob_script)
2167+ src = os.path.join(self._cron_script_dir, self._cron_script)
2168 dst = shutil.copy(src, self.NRPE_PLUGINS_DIR)
2169 os.chmod(dst, self.CRONJOB_SCRIPT_MODE)
2170 os.chown(dst, uid=self.CRONJOB_SCRIPT_UID, gid=self.CRONJOB_SCRIPT_GID)
2171 hookenv.log(
2172 'Cronjob script [{}] copied to {}'
2173- ''.format(self._cronjob_script, self.NRPE_PLUGINS_DIR),
2174+ ''.format(self._cron_script, self.NRPE_PLUGINS_DIR),
2175 hookenv.DEBUG
2176 )
2177
2178 cmdline = [dst]
2179- if self._cronjob_script_args \
2180- and isinstance(self._cronjob_script_args, str):
2181+ if self._cron_script_args \
2182+ and isinstance(self._cron_script_args, str):
2183 cmdline.extend([shlex.quote(arg)
2184- for arg in self._cronjob_script_args.split()])
2185- else:
2186- # Run it once to generate the temp file, otherwise the nrpe check
2187- # might fail at first. For security reasons, cronjobs that allow
2188- # parameters shared by the user don't run the script as root
2189- # (check_ipmi allows a missing output file)
2190+ for arg in self._cron_script_args.split()])
2191+ elif hookenv.config('manufacturer') != 'test':
2192+ # Run it once to generate the temp file unless we're on a test
2193+ # container, otherwise the nrpe check # might fail at first.
2194+ # For security reasons, cronjobs that allow parameters shared
2195+ # by the user don't run the script as root
2196+ # e.g. check_ipmi allows a missing output file
2197 subprocess.call(cmdline)
2198
2199 # Generate random cronjob execution (internal in minutes)
2200@@ -193,19 +247,18 @@ class Tool():
2201 return dst
2202
2203 def _remove_cronjob(self):
2204- assert self._cronjob_script is not None
2205+ assert self._cron_script is not None
2206
2207 crond_file = os.path.join(self.CROND_DIR,
2208 'hwhealth_{}'.format(self._shortname))
2209- cronjob_script = os.path.join(self.NRPE_PLUGINS_DIR,
2210- self._cronjob_script)
2211- for filename in (crond_file, cronjob_script):
2212+ cron_script = os.path.join(self.NRPE_PLUGINS_DIR, self._cron_script)
2213+ for filename in (crond_file, cron_script):
2214 if not os.path.exists(filename):
2215 continue
2216 os.remove(filename)
2217 hookenv.log(
2218 'Removed cronjob files [{}, {}]'
2219- ''.format(crond_file, cronjob_script),
2220+ ''.format(crond_file, cron_script),
2221 hookenv.DEBUG
2222 )
2223
2224@@ -214,7 +267,8 @@ class Tool():
2225 if not sudoer_path.exists():
2226 return
2227 sudoer_path.unlink()
2228- hookenv.log('deleted sudoer file: {}'.format(sudoer_path), hookenv.DEBUG)
2229+ hookenv.log('deleted sudoer file: {}'.format(sudoer_path),
2230+ hookenv.DEBUG)
2231
2232
2233 class VendorTool(Tool):
2234@@ -225,10 +279,8 @@ class VendorTool(Tool):
2235 cronjob that runs as root and saves the tool output in a temporary file
2236 that nrpe can read (as nagios user).
2237 """
2238- def __init__(self, shortname):
2239- super().__init__(shortname=shortname)
2240- self._cronjob_script = 'check_{}.sh'.format(shortname)
2241- self._nrpe_script = 'check_{}.py'.format(shortname)
2242+ def __init__(self, *args, **kwargs):
2243+ super().__init__(*args, **kwargs)
2244 self.checksums = []
2245
2246 def install(self):
2247@@ -260,10 +312,12 @@ class VendorTool(Tool):
2248 with open(tmpfile, 'rb') as fd:
2249 checksum.update(fd.read())
2250 if checksum.hexdigest() not in self.checksums:
2251- raise ToolChecksumError({'shortname': self._shortname,
2252- 'checksum': checksum.hexdigest(),
2253- 'expected_checksums': ', '.join(self.checksums),
2254- })
2255+ checksums_string = ', '.join(self.checksums)
2256+ raise ToolChecksumError({
2257+ 'shortname': self._shortname,
2258+ 'checksum': checksum.hexdigest(),
2259+ 'expected_checksums': checksums_string
2260+ })
2261 # We could just use self.TOOLS_DIR as a destination
2262 # here, but shutil.move refuses to overwrite the
2263 # destination file unless it receives a full path
2264@@ -277,7 +331,8 @@ class VendorTool(Tool):
2265
2266 except PermissionError as error:
2267 hookenv.log(
2268- 'Unable to unzip tool {} from the provided resource: {}'
2269+ 'Unable to unzip tool {} '
2270+ 'from the provided resource: {}'
2271 ''.format(self._shortname, error),
2272 hookenv.ERROR
2273 )
2274@@ -296,13 +351,194 @@ class VendorTool(Tool):
2275 )
2276
2277
2278+class AptVendorTool(Tool):
2279+ """A class representing a vendor tool binary package available via apt
2280+
2281+ An AptVendorTool is a statically linked binary or set of binaries,
2282+ config files, and libraries available via apt repository from Ubuntu
2283+ or Hardware Vendor package Archives. Since it requires root privileges
2284+ to run locally, we also deploy a cronjob that runs as root and saves
2285+ the tool output in a temporary file that nrpe can read (as nagios user).
2286+
2287+ """
2288+
2289+ HPE_ILOREST_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
2290+ Version: GnuPG v1.4.12 (GNU/Linux)
2291+
2292+ mQENBFZp0LkBCACXajRw3b4x7G7dulNYj0hUID4BtVFq/MjEb6PHckTxGxZDoQRX
2293+ RK54tiTFA9wq3b4P3yEFnOjbjRoI0d7Ls67FADugFO+cDCtsV9yuDlaYP/U/h2nX
2294+ N0R4AdYbsVd5yr6xr+GAy66Hmx5jFH3kbC+zJpOcI0tU9hcyU7gjbxu6KQ1ypI2Q
2295+ VRKf8sRBJXgmkOlbYx35ZUMFcmVxrLJXvUuxmAVXgT9f5M3Z3rsGt/ab+/+1TFSb
2296+ RsaqHsIPE0QH8ikqW4IeDQAo1T99pCdf7FWr45KFFTo7O4AZdLMWVgqeFHaSoZxJ
2297+ 307VIINsWiwQoPp0tfU5NOOOwB1Sv3x9QgFtABEBAAG0P0hld2xldHQgUGFja2Fy
2298+ ZCBFbnRlcnByaXNlIENvbXBhbnkgUlNBLTIwNDgtMjUgPHNpZ25ocEBocGUuY29t
2299+ PokBPQQTAQIAJwUCVmnQuQIbLwUJEswDAAYLCQgHAwIGFQgCCQoLAxYCAQIeAQIX
2300+ gAAKCRDCCK3eJsK3l9G+B/0ekblsBeN+xHIJ28pvo2aGb2KtWBwbT1ugI+aIS17K
2301+ UQyHZJUQH+ZeRLvosuoiQEdcGIqmOxi2hVhSCQAOV1LAonY16ACveA5DFAEBz1+a
2302+ WQyx6sOLLEAVX1VqGlBXxh3XLEUWOhlAf1gZPNtHsmURTUy2h1Lv/Yoj8KLyuK2n
2303+ DmrLOS3Ro+RqWocaJfvAgXKgt6Fq/ChDUHOnar7lGswzMsbE/yzLJ7He4y89ImK+
2304+ 2ktR5HhDuxqgCe9CWH6Q/1WGhUa0hZ3nbluq7maa+kPe2g7JcRzPH/nJuDCAOZ7U
2305+ 6mHE8j0kMQMYjgaYEx2wc02aQRmPyxhbDLjSbtjomXRr
2306+ =voON
2307+ -----END PGP PUBLIC KEY BLOCK-----"""
2308+ HPE_MCP_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
2309+ Version: GnuPG v1.4.12 (MingW32)
2310+
2311+ mQENBFRtGAgBCADlSku65P14hVdx9E/W0n6MwuB3WGqmsyKNoa3HezFdMjWERldI
2312+ NNUdi8O28cZ6j2+Hi9L1HeQIQ9+7FHpR3JyQePBJtRX8WSEusfRtML98opDhJxKm
2313+ 8Jyxb7aTvCwdNHz3yxADINkMtOj5oRm7VCr8XHkG7YU27ELs8B+BXWvjO21oSosi
2314+ FurnhT+H3hQsYXfYA55aa21q0qX+L5dFJSNdzZVo7m9ybioVv2R5+PfBvdaSxCnm
2315+ OpcGXFaKAsqVHeTW0pd3sdkin1rkbhOBaU5lFBt2ZiMtKpKHpT8TZnqHpFHFbgi8
2316+ j2ARJj4IDct2OGILddUIZSFyue6WE2hpV5c/ABEBAAG0OEhld2xldHQtUGFja2Fy
2317+ ZCBDb21wYW55IFJTQSAoSFAgQ29kZXNpZ25pbmcgU2VydmljZSkgLSAxiQE+BBMB
2318+ AgAoBQJUbRgIAhsDBQkSzAMABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRD6
2319+ 3Y1ksSdeo6BJCADOfIPPLPpIOnFK9jH4t8lLUd+RyMc+alA3uTDPUJa/ZHa6DHfh
2320+ 42iaPYVEV8OG0tnbMlHmwvsZ5c1/MRMw1UbxCvD88P2qM4SUrUjQUlSCms2GLGvF
2321+ ftFXBiOJQ7/yBc9o+yoSvwPrrTxSCk4+Sqm0IfVXVzChDM9dM9YPY2Vzjd+LUaYC
2322+ 3X+eSuggUDO0TmJLJd7tZdF9fVXq3lr63BZ5PY98MTCuOoeSMDa9FIUQf6vn6UUJ
2323+ MDSRZ9OzhpNJOKR+ShVRwDK6My8gtVIW1EAW2w3VQWI2UNF07aLeO8UG6nTNWA23
2324+ +OuZkUdgQovjcq01caSefgOkmiQOx6d74CAk
2325+ =X+eo
2326+ -----END PGP PUBLIC KEY BLOCK-----"""
2327+
2328+ HPE_MCP_REPO_TMPL = (
2329+ 'deb http://downloads.linux.hpe.com/SDR/repo/mcp {series}/current-gen9 non-free'
2330+ )
2331+ HPE_ILOREST_REPO_TMPL = (
2332+ 'deb http://downloads.linux.hpe.com/SDR/repo/ilorest {series}/current non-free'
2333+ )
2334+
2335+ # HP doesn't have focal APT sources as of yet
2336+ APT_SOURCES = {
2337+ 'ssacli': {
2338+ 'xenial': HPE_MCP_REPO_TMPL.format(series='xenial'),
2339+ 'bionic': HPE_MCP_REPO_TMPL.format(series='bionic'),
2340+ },
2341+ 'ilorest': {
2342+ 'xenial': HPE_ILOREST_REPO_TMPL.format(series='xenial'),
2343+ 'bionic': HPE_ILOREST_REPO_TMPL.format(series='bionic'),
2344+ },
2345+ 'hplog': {
2346+ 'xenial': HPE_MCP_REPO_TMPL.format(series='xenial'),
2347+ 'bionic': HPE_MCP_REPO_TMPL.format(series='bionic'),
2348+ },
2349+ }
2350+ APT_KEYS = {
2351+ 'ssacli': {
2352+ 'xenial': HPE_MCP_KEY,
2353+ 'bionic': HPE_MCP_KEY,
2354+ },
2355+ 'ilorest': {
2356+ 'xenial': HPE_ILOREST_KEY,
2357+ 'bionic': HPE_ILOREST_KEY,
2358+ },
2359+ 'hplog': {
2360+ 'xenial': HPE_MCP_KEY,
2361+ 'bionic': HPE_MCP_KEY,
2362+ },
2363+ }
2364+
2365+ def __init__(self, shortname=None, apt_packages=[]):
2366+ super().__init__(
2367+ shortname=shortname,
2368+ nrpe_script='check_hw_health_cron_output.py',
2369+ nrpe_opts='--filename {}/{}.out'.format(self.CRONJOB_OUTPUT_DIR,
2370+ shortname)
2371+ )
2372+ self.apt_packages = (apt_packages if apt_packages
2373+ else [self.__class__.__name__.lower()])
2374+ self._nrpe_script_dir = self._common_libs_dir
2375+
2376+ def install(self):
2377+ self._install_packages()
2378+ self.install_cronjob()
2379+ super().install()
2380+
2381+ def remove(self):
2382+ self._remove_cronjob()
2383+ self._remove_packages()
2384+ super().remove()
2385+
2386+ def _install_packages(self):
2387+ """
2388+ Install the apt packages via layer-apt
2389+
2390+ As deb packages manage their own checksums and each cloud can point to
2391+ trusted repositories via install_sources and install_keys config, no
2392+ checksumming is necessary as in resource-shipped VendorTool.
2393+
2394+ Each instantiation should configure it's apt_packages based on the
2395+ hardware present on the system.
2396+ """
2397+ self._add_apt_source()
2398+ if hookenv.config('manufacturer') == 'test':
2399+ # If we are forcing install on a container for functional tests,
2400+ # we should only download and not install the packages, as some
2401+ # vendor tools depend on hardware to be preset to complete postinst
2402+ # need one option added per package
2403+ apt.queue_install(self.apt_packages, options=["--download-only" for _ in self.apt_packages])
2404+ else:
2405+ apt.queue_install(self.apt_packages)
2406+
2407+ def _add_apt_source(self):
2408+ series = lsb_release()['DISTRIB_CODENAME']
2409+ if self._shortname not in self.APT_SOURCES and self._shortname not in self.APT_KEYS:
2410+ return
2411+ if series in self.APT_SOURCES[self._shortname]:
2412+ apt.add_source(self.APT_SOURCES[self._shortname][series],
2413+ key=self.APT_KEYS[self._shortname][series])
2414+
2415+ def _remove_packages(self):
2416+ apt.purge(self.apt_packages)
2417+
2418+ def install_cronjob(self):
2419+ hookenv.log(
2420+ 'Attempting AptVendorTool cronjob script install [{}]'
2421+ ''.format(self._cron_script),
2422+ hookenv.DEBUG
2423+ )
2424+ # Don't install a cronjob until the tools are installed
2425+ if self.is_apt_installed():
2426+ hookenv.log(
2427+ 'calling _install_cronjob for {}'
2428+ ''.format(self._cron_script),
2429+ hookenv.DEBUG
2430+ )
2431+ self._install_cronjob()
2432+
2433+ def configure_nrpe_check(self, nrpe_setup):
2434+ # Don't drop nrpe checks until the APT package is successfully
2435+ # installed or we'll get red warnings due to Apt errors which
2436+ # should just be reported through juju status
2437+ if self.is_apt_installed():
2438+ super().configure_nrpe_check(nrpe_setup)
2439+
2440+ def is_apt_installed(self):
2441+ """
2442+ Check if packaged queued for install are installed
2443+
2444+ The apt layer will install packages in the background, this method
2445+ allows the reactive layer to know when the apt packages have finished
2446+ installing so that it may go on to configure the nrpe layer of tools
2447+ """
2448+ if hookenv.config('manufacturer') == 'test':
2449+ # it is okay to skip this part in testing as layer-apt will block
2450+ # if the package is not downloadable from sources
2451+ return True
2452+ apt_installed_packages = apt.installed()
2453+ for package in self.apt_packages:
2454+ if package not in apt_installed_packages:
2455+ return False
2456+ return True
2457+
2458+
2459 class Sas3Ircu(VendorTool):
2460 """A class representing the sas3ircu tool
2461
2462 This is a tool supporting the LSI SAS 12Gb/s controllers
2463 """
2464 def __init__(self):
2465- super().__init__(shortname='sas3ircu')
2466+ super().__init__(cron_script='cron_sas3ircu.sh')
2467 self.checksums = [
2468 'f150eb37bb332668949a3eccf9636e0e03f874aecd17a39d586082c6be1386bd',
2469 'd69967057992134df1b136f83bc775a641e32c4efc741def3ef6f6a25a9a14b5',
2470@@ -315,8 +551,8 @@ class Sas2Ircu(VendorTool):
2471 This is a tool supporting the LSI SAS 6Gb/s controllers
2472 """
2473 def __init__(self):
2474- super().__init__(shortname='sas2ircu')
2475- self.checksums = ['37467826d0b22aad47287efe70bb34e47f475d70e9b1b64cbd63f57607701e73']
2476+ super().__init__(cron_script='cron_sas2ircu.sh')
2477+ self.checksums = ['37467826d0b22aad47287efe70bb34e47f475d70e9b1b64cbd63f57607701e73'] # noqa: E501
2478
2479
2480 class MegaCLI(VendorTool):
2481@@ -325,7 +561,7 @@ class MegaCLI(VendorTool):
2482 This is a tool supporting the LSI MegaRAID SAS controllers
2483 """
2484 def __init__(self):
2485- super().__init__(shortname='megacli')
2486+ super().__init__(cron_script='cron_megacli.sh')
2487 self.checksums = [
2488 '34f1a235543662615ee35f458317380b3f89fac0e415dee755e0dbc7c4cf6f92',
2489 '1c4effe33ee5db82227e05925dd629771fd49c7d2be2382d48c48a864452cdec',
2490@@ -333,6 +569,40 @@ class MegaCLI(VendorTool):
2491 ]
2492
2493
2494+class HpLog(AptVendorTool):
2495+ """A class representing the hplog tool
2496+
2497+ This is a tool supporting the LSI MegaRAID SAS controllers
2498+ """
2499+
2500+ SUPPORTED_SERIES = ['xenial', 'bionic']
2501+
2502+ def __init__(self):
2503+ super().__init__(apt_packages=['hp-health'])
2504+
2505+
2506+class SsaCli(AptVendorTool):
2507+ """A class representing the ssacli tool
2508+
2509+ This is a tool supporting the HP Smart Array controllers
2510+ """
2511+
2512+ SUPPORTED_SERIES = ['xenial', 'bionic']
2513+
2514+ def __init__(self):
2515+ super().__init__()
2516+
2517+
2518+class ILOrest(AptVendorTool):
2519+ """A class representing the ILOrest vendor tool (HPE hardware (Gen 10+)
2520+ """
2521+
2522+ SUPPORTED_SERIES = ['xenial', 'bionic']
2523+
2524+ def __init__(self):
2525+ super().__init__()
2526+
2527+
2528 class Mdadm(VendorTool):
2529 """A class representing the mdadm tool (software RAID)
2530
2531@@ -340,10 +610,7 @@ class Mdadm(VendorTool):
2532 installation as it has a cronjob + check script
2533 """
2534 def __init__(self):
2535- shortname = 'mdadm'
2536- super().__init__(shortname=shortname)
2537- self._cronjob_script = 'cron_{}.py'.format(shortname)
2538- self._nrpe_script = 'check_{}.py'.format(shortname)
2539+ super().__init__()
2540
2541 def install(self):
2542 # mdadm should already be installed, but let's check
2543@@ -368,11 +635,11 @@ class Ipmi(Tool):
2544 actual nrpe check, which is imported as a git submodule
2545 """
2546 def __init__(self, nrpe_opts=''):
2547- super().__init__(shortname='ipmi')
2548- self._nrpe_script = 'check_ipmi.py'
2549+ super().__init__(
2550+ cron_script='cron_ipmi_sensors.py',
2551+ cron_script_args=hookenv.config('ipmi_check_options')
2552+ )
2553 self._sudoer_file = '99-check_ipmi_sensor'
2554- self._cronjob_script = 'cron_ipmi_sensors.py'
2555- self._cronjob_script_args = hookenv.config('ipmi_check_options')
2556
2557 def configure_nrpe_check(self, nrpe_setup):
2558 # extra options for check_ipmi_sensors Perl script are configured in
2559@@ -423,37 +690,17 @@ class Ipmi(Tool):
2560 return dst
2561
2562
2563-class HP(VendorTool):
2564- """A class representing tools needed to monitor HP hardware
2565-
2566- This class will need to install tools such as hponcfg, hp-health, and
2567- hpssacli
2568- """
2569- def __init__(self):
2570- raise NotImplementedError
2571-
2572-
2573-class HPE(VendorTool):
2574- """A class representing tools needed to monitor HPE hardware
2575-
2576- This class will need to install tools such as hponcfg and ilorest
2577- """
2578- def __init__(self):
2579- raise NotImplementedError
2580-
2581-
2582 class Nvme(Tool):
2583 """A class representing the nvme tool (userspace tooling to control NVMe)
2584
2585- Our nvme check kind of behaves like a VendorTool for the purpose of
2586- installation as it has a cronjob + check script
2587+ This is a direct subclass of Tool because unlike a VendorTool we are not
2588+ using a cronjob script
2589 """
2590 def __init__(self):
2591- shortname = 'nvme'
2592- super().__init__(shortname=shortname)
2593- self._nrpe_script = 'check_{}.py'.format(shortname)
2594+ super().__init__()
2595 self._sudoer_template = '99-check_nvme.tmpl'
2596 self._sudoer_file = '99-check_nvme'
2597+ self._cron_script = None
2598
2599 def install(self):
2600 # mdadm should already be installed, but let's check
2601diff --git a/src/metadata.yaml b/src/metadata.yaml
2602index 58c709a..a06914e 100644
2603--- a/src/metadata.yaml
2604+++ b/src/metadata.yaml
2605@@ -18,6 +18,7 @@ description: |
2606 nvme
2607 sas2ircu
2608 sas3ircu
2609+ ilorest
2610
2611 tags:
2612 - monitoring
2613diff --git a/src/reactive/hw_health.py b/src/reactive/hw_health.py
2614index 6619dd4..a04f790 100644
2615--- a/src/reactive/hw_health.py
2616+++ b/src/reactive/hw_health.py
2617@@ -10,7 +10,16 @@ from hwhealth.hwdiscovery import get_tools
2618 from hwhealth import tools
2619
2620
2621+def _set_install_status(tool):
2622+ if isinstance(tool, tools.VendorTool) and not isinstance(tool, tools.Mdadm):
2623+ status.maintenance('Installing from attached resource')
2624+ elif isinstance(tool, tools.AptVendorTool):
2625+ status.maintenance('Installing vendor tools via apt')
2626+ set_flag('hw-health.wait-for-vendor-apt')
2627+
2628+
2629 @when_none('hw-health.installed', 'hw-health.unsupported')
2630+@when('nrpe-external-master.available')
2631 @when('general-info.connected')
2632 def install():
2633 manufacturer = hookenv.config('manufacturer')
2634@@ -32,9 +41,10 @@ def install():
2635 else:
2636 try:
2637 tool_list = list()
2638- for tool in toolset:
2639- if isinstance(tool, tools.VendorTool) and not isinstance(tool, tools.Mdadm):
2640- status.maintenance('Installing from attached resource')
2641+ for toolClass in toolset:
2642+ tool = toolClass()
2643+ _set_install_status(tool)
2644+ status.maintenance('Installing tool {}'.format(type(tool).__name__))
2645 tool.install()
2646 # Save the class name in the unit kv db. This will be reused when
2647 # reconfiguring or removing the checks
2648@@ -84,6 +94,24 @@ def remove_tools():
2649 clear_flag('hw-health.configured')
2650
2651
2652+@when('hw-health.wait-for-vendor-apt')
2653+def wait_for_vendor_apt():
2654+ # cycle through any vendor tools that are of type AptVendorTool and
2655+ # check if all packages needed are installed. If not, eject and wait
2656+ unitdb = unitdata.kv()
2657+ for tool_class_name in unitdb.get('toolset', set()):
2658+ # Re-instantiate the tool from the saved class name
2659+ tool_class = getattr(tools, tool_class_name)
2660+ if isinstance(tool_class, tools.AptVendorTool):
2661+ if tool_class.is_apt_installed():
2662+ tool_class.install_cronjob()
2663+ else:
2664+ status.maintenance('Waiting for vendor tools to install via apt')
2665+ return
2666+ clear_flag('hw-health.wait-for-vendor-apt')
2667+ clear_flag('hw-health.configured')
2668+
2669+
2670 @when('config.changed')
2671 @when_not('config.changed.manufacturer')
2672 def config_changed():
2673diff --git a/src/tests/expected/lshw.py b/src/tests/expected/lshw.py
2674new file mode 100644
2675index 0000000..d11909f
2676--- /dev/null
2677+++ b/src/tests/expected/lshw.py
2678@@ -0,0 +1,1917 @@
2679+EXPECTED = {
2680+ "lshw.dell.01.json": {
2681+ "system": {
2682+ "description": "Rack Mount Chassis",
2683+ "hostname": "compute-4",
2684+ "product": "PowerEdge R730 (SKU=NotProvided;ModelName=PowerEdge R730, DBE)", # noqa E501
2685+ "serial": "3YMGWL2",
2686+ "vendor": "Dell Inc.",
2687+ "version": None,
2688+ },
2689+ "motherboard": [
2690+ {
2691+ "description": "Motherboard",
2692+ "product": "04N3DF",
2693+ "serial": ".3YMGWL2.CNFCP0077F0059.",
2694+ "vendor": "Dell Inc.",
2695+ "version": "A10",
2696+ }
2697+ ],
2698+ "storage_class_info": [
2699+ {
2700+ "businfo": "pci@0000:03:00.0",
2701+ "driver": "megaraid_sas",
2702+ "has_children": True,
2703+ "product": "MegaRAID SAS-3 3108 [Invader]",
2704+ "vendor": "LSI Logic / Symbios Logic",
2705+ },
2706+ {
2707+ "businfo": "pci@0000:00:11.4",
2708+ "driver": "ahci",
2709+ "has_children": False,
2710+ "product": "C610/X99 series chipset sSATA Controller [AHCI mode]", # noqa E501
2711+ "vendor": "Intel Corporation",
2712+ },
2713+ {
2714+ "businfo": "pci@0000:00:1f.2",
2715+ "driver": "ahci",
2716+ "has_children": False,
2717+ "product": "C610/X99 series chipset 6-Port SATA Controller [AHCI mode]", # noqa E501
2718+ "vendor": "Intel Corporation",
2719+ },
2720+ {
2721+ "businfo": "pci@0000:82:00.0",
2722+ "driver": "nvme",
2723+ "has_children": False,
2724+ "product": "PCIe Data Center SSD",
2725+ "vendor": "Intel Corporation",
2726+ },
2727+ ],
2728+ "disk_class_info": [
2729+ {
2730+ "businfo": "scsi@0:2.0.0",
2731+ "dev": "8:0",
2732+ "logicalname": "/dev/sda",
2733+ "physid": "2.0.0",
2734+ "product": "PERC H730P Mini",
2735+ "serial": "00174c7006cbabeb2100aa7515604609",
2736+ "size": 4000225165312,
2737+ "storage_parent": "MegaRAID SAS-3 3108 [Invader]",
2738+ },
2739+ {
2740+ "businfo": "scsi@0:2.1.0",
2741+ "dev": "8:16",
2742+ "logicalname": "/dev/sdb",
2743+ "physid": "2.1.0",
2744+ "product": "PERC H730P Mini",
2745+ "serial": "00f7e28406ccabeb2100aa7515604609",
2746+ "size": 4000225165312,
2747+ "storage_parent": "MegaRAID SAS-3 3108 [Invader]",
2748+ },
2749+ {
2750+ "businfo": "scsi@0:2.2.0",
2751+ "dev": "8:32",
2752+ "logicalname": "/dev/sdc",
2753+ "physid": "2.2.0",
2754+ "product": "PERC H730P Mini",
2755+ "serial": "0024b29406cdabeb2100aa7515604609",
2756+ "size": 4000225165312,
2757+ "storage_parent": "MegaRAID SAS-3 3108 [Invader]",
2758+ },
2759+ {
2760+ "businfo": "scsi@0:2.3.0",
2761+ "dev": "8:48",
2762+ "logicalname": "/dev/sdd",
2763+ "physid": "2.3.0",
2764+ "product": "PERC H730P Mini",
2765+ "serial": "007a79a506ceabeb2100aa7515604609",
2766+ "size": 4000225165312,
2767+ "storage_parent": "MegaRAID SAS-3 3108 [Invader]",
2768+ },
2769+ {
2770+ "businfo": "scsi@0:2.4.0",
2771+ "dev": "8:64",
2772+ "logicalname": ["/dev/sde", "/srv/node/sde"],
2773+ "physid": "2.4.0",
2774+ "product": "PERC H730P Mini",
2775+ "serial": "0005ebba06cfabeb2100aa7515604609",
2776+ "size": 4000225165312,
2777+ "storage_parent": "MegaRAID SAS-3 3108 [Invader]",
2778+ },
2779+ {
2780+ "businfo": "scsi@0:2.5.0",
2781+ "dev": "8:80",
2782+ "logicalname": ["/dev/sdf", "/srv/node/sdf"],
2783+ "physid": "2.5.0",
2784+ "product": "PERC H730P Mini",
2785+ "serial": "005c35cc06d1abeb2100aa7515604609",
2786+ "size": 4000225165312,
2787+ "storage_parent": "MegaRAID SAS-3 3108 [Invader]",
2788+ },
2789+ {
2790+ "businfo": "scsi@0:2.6.0",
2791+ "dev": "8:96",
2792+ "logicalname": ["/dev/sdg", "/srv/node/sdg"],
2793+ "physid": "2.6.0",
2794+ "product": "PERC H730P Mini",
2795+ "serial": "00e046dd06d2abeb2100aa7515604609",
2796+ "size": 4000225165312,
2797+ "storage_parent": "MegaRAID SAS-3 3108 [Invader]",
2798+ },
2799+ ],
2800+ "network_class_info": [
2801+ {
2802+ "businfo": "pci@0000:01:00.0",
2803+ "driver": "mlx5_core",
2804+ "driverversion": "5.0-0",
2805+ "firmware": "14.21.3012 (DEL2810000034)",
2806+ "logicalname": "eno1",
2807+ "product": "MT27710 Family [ConnectX-4 Lx]",
2808+ "serial": "de:ad:be:ef:00:ef",
2809+ "speed": None,
2810+ "vendor": "Mellanox Technologies",
2811+ },
2812+ {
2813+ "businfo": "pci@0000:01:00.1",
2814+ "driver": "mlx5_core",
2815+ "driverversion": "5.0-0",
2816+ "firmware": "14.21.3012 (DEL2810000034)",
2817+ "logicalname": "eno2",
2818+ "product": "MT27710 Family [ConnectX-4 Lx]",
2819+ "serial": "de:ad:be:ef:00:ef",
2820+ "speed": None,
2821+ "vendor": "Mellanox Technologies",
2822+ },
2823+ {
2824+ "businfo": "pci@0000:01:02.6",
2825+ "driver": "mlx5_core",
2826+ "driverversion": "5.0-0",
2827+ "firmware": "14.21.3012 (DEL2810000034)",
2828+ "logicalname": "enp1s2f6",
2829+ "product": "Illegal Vendor ID",
2830+ "serial": "de:ad:be:ef:00:ef",
2831+ "speed": None,
2832+ "vendor": "Illegal Vendor ID",
2833+ },
2834+ {
2835+ "businfo": "pci@0000:01:02.7",
2836+ "driver": "mlx5_core",
2837+ "driverversion": "5.0-0",
2838+ "firmware": "14.21.3012 (DEL2810000034)",
2839+ "logicalname": "enp1s2f7",
2840+ "product": "Illegal Vendor ID",
2841+ "serial": "de:ad:be:ef:00:ef",
2842+ "speed": None,
2843+ "vendor": "Illegal Vendor ID",
2844+ },
2845+ {
2846+ "businfo": "pci@0000:01:03.0",
2847+ "driver": "mlx5_core",
2848+ "driverversion": "5.0-0",
2849+ "firmware": "14.21.3012 (DEL2810000034)",
2850+ "logicalname": "enp1s3",
2851+ "product": "Illegal Vendor ID",
2852+ "serial": "de:ad:be:ef:00:ef",
2853+ "speed": None,
2854+ "vendor": "Illegal Vendor ID",
2855+ },
2856+ {
2857+ "businfo": "pci@0000:01:03.1",
2858+ "driver": "mlx5_core",
2859+ "driverversion": "5.0-0",
2860+ "firmware": "14.21.3012 (DEL2810000034)",
2861+ "logicalname": "enp1s3f1",
2862+ "product": "Illegal Vendor ID",
2863+ "serial": "de:ad:be:ef:00:ef",
2864+ "speed": None,
2865+ "vendor": "Illegal Vendor ID",
2866+ },
2867+ {
2868+ "businfo": "pci@0000:01:03.2",
2869+ "driver": "mlx5_core",
2870+ "driverversion": "5.0-0",
2871+ "firmware": "14.21.3012 (DEL2810000034)",
2872+ "logicalname": "enp1s3f2",
2873+ "product": "Illegal Vendor ID",
2874+ "serial": "de:ad:be:ef:00:ef",
2875+ "speed": None,
2876+ "vendor": "Illegal Vendor ID",
2877+ },
2878+ {
2879+ "businfo": "pci@0000:01:03.3",
2880+ "driver": "mlx5_core",
2881+ "driverversion": "5.0-0",
2882+ "firmware": "14.21.3012 (DEL2810000034)",
2883+ "logicalname": "enp1s3f3",
2884+ "product": "Illegal Vendor ID",
2885+ "serial": "de:ad:be:ef:00:ef",
2886+ "speed": None,
2887+ "vendor": "Illegal Vendor ID",
2888+ },
2889+ {
2890+ "businfo": "pci@0000:01:03.4",
2891+ "driver": "mlx5_core",
2892+ "driverversion": "5.0-0",
2893+ "firmware": "14.21.3012 (DEL2810000034)",
2894+ "logicalname": "enp1s3f4",
2895+ "product": "Illegal Vendor ID",
2896+ "serial": "de:ad:be:ef:00:ef",
2897+ "speed": None,
2898+ "vendor": "Illegal Vendor ID",
2899+ },
2900+ {
2901+ "businfo": "pci@0000:01:03.5",
2902+ "driver": "mlx5_core",
2903+ "driverversion": "5.0-0",
2904+ "firmware": "14.21.3012 (DEL2810000034)",
2905+ "logicalname": "enp1s3f5",
2906+ "product": "Illegal Vendor ID",
2907+ "serial": "de:ad:be:ef:00:ef",
2908+ "speed": None,
2909+ "vendor": "Illegal Vendor ID",
2910+ },
2911+ {
2912+ "businfo": "pci@0000:01:03.6",
2913+ "driver": "mlx5_core",
2914+ "driverversion": "5.0-0",
2915+ "firmware": "14.21.3012 (DEL2810000034)",
2916+ "logicalname": "enp1s3f6",
2917+ "product": "Illegal Vendor ID",
2918+ "serial": "de:ad:be:ef:00:ef",
2919+ "speed": None,
2920+ "vendor": "Illegal Vendor ID",
2921+ },
2922+ {
2923+ "businfo": "pci@0000:01:03.7",
2924+ "driver": "mlx5_core",
2925+ "driverversion": "5.0-0",
2926+ "firmware": "14.21.3012 (DEL2810000034)",
2927+ "logicalname": "enp1s3f7",
2928+ "product": "Illegal Vendor ID",
2929+ "serial": "de:ad:be:ef:00:ef",
2930+ "speed": None,
2931+ "vendor": "Illegal Vendor ID",
2932+ },
2933+ {
2934+ "businfo": "pci@0000:01:04.0",
2935+ "driver": "mlx5_core",
2936+ "driverversion": "5.0-0",
2937+ "firmware": "14.21.3012 (DEL2810000034)",
2938+ "logicalname": "enp1s4",
2939+ "product": "Illegal Vendor ID",
2940+ "serial": "de:ad:be:ef:00:ef",
2941+ "speed": None,
2942+ "vendor": "Illegal Vendor ID",
2943+ },
2944+ {
2945+ "businfo": "pci@0000:01:04.1",
2946+ "driver": "mlx5_core",
2947+ "driverversion": "5.0-0",
2948+ "firmware": "14.21.3012 (DEL2810000034)",
2949+ "logicalname": "enp1s4f1",
2950+ "product": "Illegal Vendor ID",
2951+ "serial": "de:ad:be:ef:00:ef",
2952+ "speed": None,
2953+ "vendor": "Illegal Vendor ID",
2954+ },
2955+ {
2956+ "businfo": "pci@0000:01:04.2",
2957+ "driver": "mlx5_core",
2958+ "driverversion": "5.0-0",
2959+ "firmware": "14.21.3012 (DEL2810000034)",
2960+ "logicalname": "enp1s4f2",
2961+ "product": "Illegal Vendor ID",
2962+ "serial": "de:ad:be:ef:00:ef",
2963+ "speed": None,
2964+ "vendor": "Illegal Vendor ID",
2965+ },
2966+ {
2967+ "businfo": "pci@0000:01:04.3",
2968+ "driver": "mlx5_core",
2969+ "driverversion": "5.0-0",
2970+ "firmware": "14.21.3012 (DEL2810000034)",
2971+ "logicalname": "enp1s4f3",
2972+ "product": "Illegal Vendor ID",
2973+ "serial": "de:ad:be:ef:00:ef",
2974+ "speed": None,
2975+ "vendor": "Illegal Vendor ID",
2976+ },
2977+ {
2978+ "businfo": "pci@0000:01:04.4",
2979+ "driver": "mlx5_core",
2980+ "driverversion": "5.0-0",
2981+ "firmware": "14.21.3012 (DEL2810000034)",
2982+ "logicalname": "enp1s4f4",
2983+ "product": "Illegal Vendor ID",
2984+ "serial": "de:ad:be:ef:00:ef",
2985+ "speed": None,
2986+ "vendor": "Illegal Vendor ID",
2987+ },
2988+ {
2989+ "businfo": "pci@0000:01:04.5",
2990+ "driver": "mlx5_core",
2991+ "driverversion": "5.0-0",
2992+ "firmware": "14.21.3012 (DEL2810000034)",
2993+ "logicalname": "enp1s4f5",
2994+ "product": "Illegal Vendor ID",
2995+ "serial": "de:ad:be:ef:00:ef",
2996+ "speed": None,
2997+ "vendor": "Illegal Vendor ID",
2998+ },
2999+ {
3000+ "businfo": "pci@0000:01:04.6",
3001+ "driver": "mlx5_core",
3002+ "driverversion": "5.0-0",
3003+ "firmware": "14.21.3012 (DEL2810000034)",
3004+ "logicalname": "enp1s4f6",
3005+ "product": "Illegal Vendor ID",
3006+ "serial": "de:ad:be:ef:00:ef",
3007+ "speed": None,
3008+ "vendor": "Illegal Vendor ID",
3009+ },
3010+ {
3011+ "businfo": "pci@0000:01:04.7",
3012+ "driver": "mlx5_core",
3013+ "driverversion": "5.0-0",
3014+ "firmware": "14.21.3012 (DEL2810000034)",
3015+ "logicalname": "enp1s4f7",
3016+ "product": "Illegal Vendor ID",
3017+ "serial": "de:ad:be:ef:00:ef",
3018+ "speed": None,
3019+ "vendor": "Illegal Vendor ID",
3020+ },
3021+ {
3022+ "businfo": "pci@0000:01:05.0",
3023+ "driver": "mlx5_core",
3024+ "driverversion": "5.0-0",
3025+ "firmware": "14.21.3012 (DEL2810000034)",
3026+ "logicalname": "enp1s5",
3027+ "product": "Illegal Vendor ID",
3028+ "serial": "de:ad:be:ef:00:ef",
3029+ "speed": None,
3030+ "vendor": "Illegal Vendor ID",
3031+ },
3032+ {
3033+ "businfo": "pci@0000:01:05.1",
3034+ "driver": "mlx5_core",
3035+ "driverversion": "5.0-0",
3036+ "firmware": "14.21.3012 (DEL2810000034)",
3037+ "logicalname": "enp1s5f1",
3038+ "product": "Illegal Vendor ID",
3039+ "serial": "de:ad:be:ef:00:ef",
3040+ "speed": None,
3041+ "vendor": "Illegal Vendor ID",
3042+ },
3043+ {
3044+ "businfo": "pci@0000:83:00.0",
3045+ "driver": "mlx5_core",
3046+ "driverversion": "5.0-0",
3047+ "firmware": "14.21.3012 (DEL2420110034)",
3048+ "logicalname": "enp131s0f0",
3049+ "product": "MT27710 Family [ConnectX-4 Lx]",
3050+ "serial": "de:ad:be:ef:00:ef",
3051+ "speed": None,
3052+ "vendor": "Mellanox Technologies",
3053+ },
3054+ {
3055+ "businfo": "pci@0000:83:00.1",
3056+ "driver": "mlx5_core",
3057+ "driverversion": "5.0-0",
3058+ "firmware": "14.21.3012 (DEL2420110034)",
3059+ "logicalname": "enp131s0f1",
3060+ "product": "MT27710 Family [ConnectX-4 Lx]",
3061+ "serial": "de:ad:be:ef:00:ef",
3062+ "speed": None,
3063+ "vendor": "Mellanox Technologies",
3064+ },
3065+ {
3066+ "businfo": "pci@0000:83:02.6",
3067+ "driver": "mlx5_core",
3068+ "driverversion": "5.0-0",
3069+ "firmware": "14.21.3012 (DEL2420110034)",
3070+ "logicalname": "enp131s2f6",
3071+ "product": "Illegal Vendor ID",
3072+ "serial": "de:ad:be:ef:00:ef",
3073+ "speed": None,
3074+ "vendor": "Illegal Vendor ID",
3075+ },
3076+ {
3077+ "businfo": "pci@0000:83:02.7",
3078+ "driver": "mlx5_core",
3079+ "driverversion": "5.0-0",
3080+ "firmware": "14.21.3012 (DEL2420110034)",
3081+ "logicalname": "enp131s2f7",
3082+ "product": "Illegal Vendor ID",
3083+ "serial": "de:ad:be:ef:00:ef",
3084+ "speed": None,
3085+ "vendor": "Illegal Vendor ID",
3086+ },
3087+ {
3088+ "businfo": "pci@0000:83:03.0",
3089+ "driver": "mlx5_core",
3090+ "driverversion": "5.0-0",
3091+ "firmware": "14.21.3012 (DEL2420110034)",
3092+ "logicalname": "enp131s3",
3093+ "product": "Illegal Vendor ID",
3094+ "serial": "de:ad:be:ef:00:ef",
3095+ "speed": None,
3096+ "vendor": "Illegal Vendor ID",
3097+ },
3098+ {
3099+ "businfo": "pci@0000:83:03.1",
3100+ "driver": "mlx5_core",
3101+ "driverversion": "5.0-0",
3102+ "firmware": "14.21.3012 (DEL2420110034)",
3103+ "logicalname": "enp131s3f1",
3104+ "product": "Illegal Vendor ID",
3105+ "serial": "de:ad:be:ef:00:ef",
3106+ "speed": None,
3107+ "vendor": "Illegal Vendor ID",
3108+ },
3109+ {
3110+ "businfo": "pci@0000:83:03.2",
3111+ "driver": "mlx5_core",
3112+ "driverversion": "5.0-0",
3113+ "firmware": "14.21.3012 (DEL2420110034)",
3114+ "logicalname": "enp131s3f2",
3115+ "product": "Illegal Vendor ID",
3116+ "serial": "de:ad:be:ef:00:ef",
3117+ "speed": None,
3118+ "vendor": "Illegal Vendor ID",
3119+ },
3120+ {
3121+ "businfo": "pci@0000:83:03.3",
3122+ "driver": "mlx5_core",
3123+ "driverversion": "5.0-0",
3124+ "firmware": "14.21.3012 (DEL2420110034)",
3125+ "logicalname": "enp131s3f3",
3126+ "product": "Illegal Vendor ID",
3127+ "serial": "de:ad:be:ef:00:ef",
3128+ "speed": None,
3129+ "vendor": "Illegal Vendor ID",
3130+ },
3131+ {
3132+ "businfo": "pci@0000:83:03.4",
3133+ "driver": "mlx5_core",
3134+ "driverversion": "5.0-0",
3135+ "firmware": "14.21.3012 (DEL2420110034)",
3136+ "logicalname": "enp131s3f4",
3137+ "product": "Illegal Vendor ID",
3138+ "serial": "de:ad:be:ef:00:ef",
3139+ "speed": None,
3140+ "vendor": "Illegal Vendor ID",
3141+ },
3142+ {
3143+ "businfo": "pci@0000:83:03.5",
3144+ "driver": "mlx5_core",
3145+ "driverversion": "5.0-0",
3146+ "firmware": "14.21.3012 (DEL2420110034)",
3147+ "logicalname": "enp131s3f5",
3148+ "product": "Illegal Vendor ID",
3149+ "serial": "de:ad:be:ef:00:ef",
3150+ "speed": None,
3151+ "vendor": "Illegal Vendor ID",
3152+ },
3153+ {
3154+ "businfo": "pci@0000:83:03.6",
3155+ "driver": "mlx5_core",
3156+ "driverversion": "5.0-0",
3157+ "firmware": "14.21.3012 (DEL2420110034)",
3158+ "logicalname": "enp131s3f6",
3159+ "product": "Illegal Vendor ID",
3160+ "serial": "de:ad:be:ef:00:ef",
3161+ "speed": None,
3162+ "vendor": "Illegal Vendor ID",
3163+ },
3164+ {
3165+ "businfo": "pci@0000:83:03.7",
3166+ "driver": "mlx5_core",
3167+ "driverversion": "5.0-0",
3168+ "firmware": "14.21.3012 (DEL2420110034)",
3169+ "logicalname": "enp131s3f7",
3170+ "product": "Illegal Vendor ID",
3171+ "serial": "de:ad:be:ef:00:ef",
3172+ "speed": None,
3173+ "vendor": "Illegal Vendor ID",
3174+ },
3175+ {
3176+ "businfo": "pci@0000:83:04.0",
3177+ "driver": "mlx5_core",
3178+ "driverversion": "5.0-0",
3179+ "firmware": "14.21.3012 (DEL2420110034)",
3180+ "logicalname": "enp131s4",
3181+ "product": "Illegal Vendor ID",
3182+ "serial": "de:ad:be:ef:00:ef",
3183+ "speed": None,
3184+ "vendor": "Illegal Vendor ID",
3185+ },
3186+ {
3187+ "businfo": "pci@0000:83:04.1",
3188+ "driver": "mlx5_core",
3189+ "driverversion": "5.0-0",
3190+ "firmware": "14.21.3012 (DEL2420110034)",
3191+ "logicalname": "enp131s4f1",
3192+ "product": "Illegal Vendor ID",
3193+ "serial": "de:ad:be:ef:00:ef",
3194+ "speed": None,
3195+ "vendor": "Illegal Vendor ID",
3196+ },
3197+ {
3198+ "businfo": "pci@0000:83:04.2",
3199+ "driver": "mlx5_core",
3200+ "driverversion": "5.0-0",
3201+ "firmware": "14.21.3012 (DEL2420110034)",
3202+ "logicalname": "enp131s4f2",
3203+ "product": "Illegal Vendor ID",
3204+ "serial": "de:ad:be:ef:00:ef",
3205+ "speed": None,
3206+ "vendor": "Illegal Vendor ID",
3207+ },
3208+ {
3209+ "businfo": "pci@0000:83:04.3",
3210+ "driver": "mlx5_core",
3211+ "driverversion": "5.0-0",
3212+ "firmware": "14.21.3012 (DEL2420110034)",
3213+ "logicalname": "enp131s4f3",
3214+ "product": "Illegal Vendor ID",
3215+ "serial": "de:ad:be:ef:00:ef",
3216+ "speed": None,
3217+ "vendor": "Illegal Vendor ID",
3218+ },
3219+ {
3220+ "businfo": "pci@0000:83:04.4",
3221+ "driver": "mlx5_core",
3222+ "driverversion": "5.0-0",
3223+ "firmware": "14.21.3012 (DEL2420110034)",
3224+ "logicalname": "enp131s4f4",
3225+ "product": "Illegal Vendor ID",
3226+ "serial": "de:ad:be:ef:00:ef",
3227+ "speed": None,
3228+ "vendor": "Illegal Vendor ID",
3229+ },
3230+ {
3231+ "businfo": "pci@0000:83:04.5",
3232+ "driver": "mlx5_core",
3233+ "driverversion": "5.0-0",
3234+ "firmware": "14.21.3012 (DEL2420110034)",
3235+ "logicalname": "enp131s4f5",
3236+ "product": "Illegal Vendor ID",
3237+ "serial": "de:ad:be:ef:00:ef",
3238+ "speed": None,
3239+ "vendor": "Illegal Vendor ID",
3240+ },
3241+ {
3242+ "businfo": "pci@0000:83:04.6",
3243+ "driver": "mlx5_core",
3244+ "driverversion": "5.0-0",
3245+ "firmware": "14.21.3012 (DEL2420110034)",
3246+ "logicalname": "enp131s4f6",
3247+ "product": "Illegal Vendor ID",
3248+ "serial": "de:ad:be:ef:00:ef",
3249+ "speed": None,
3250+ "vendor": "Illegal Vendor ID",
3251+ },
3252+ {
3253+ "businfo": "pci@0000:83:04.7",
3254+ "driver": "mlx5_core",
3255+ "driverversion": "5.0-0",
3256+ "firmware": "14.21.3012 (DEL2420110034)",
3257+ "logicalname": "enp131s4f7",
3258+ "product": "Illegal Vendor ID",
3259+ "serial": "de:ad:be:ef:00:ef",
3260+ "speed": None,
3261+ "vendor": "Illegal Vendor ID",
3262+ },
3263+ {
3264+ "businfo": "pci@0000:83:05.0",
3265+ "driver": "mlx5_core",
3266+ "driverversion": "5.0-0",
3267+ "firmware": "14.21.3012 (DEL2420110034)",
3268+ "logicalname": "enp131s5",
3269+ "product": "Illegal Vendor ID",
3270+ "serial": "de:ad:be:ef:00:ef",
3271+ "speed": None,
3272+ "vendor": "Illegal Vendor ID",
3273+ },
3274+ {
3275+ "businfo": "pci@0000:83:05.1",
3276+ "driver": "mlx5_core",
3277+ "driverversion": "5.0-0",
3278+ "firmware": "14.21.3012 (DEL2420110034)",
3279+ "logicalname": "enp131s5f1",
3280+ "product": "Illegal Vendor ID",
3281+ "serial": "de:ad:be:ef:00:ef",
3282+ "speed": None,
3283+ "vendor": "Illegal Vendor ID",
3284+ },
3285+ ],
3286+ },
3287+ "lshw.dell.02.json": {
3288+ "system": {
3289+ "description": "Notebook",
3290+ "hostname": "pluto",
3291+ "product": "Precision 7520 (07B0)",
3292+ "serial": "1N79PH2",
3293+ "vendor": "Dell Inc.",
3294+ "version": None,
3295+ },
3296+ "motherboard": [
3297+ {
3298+ "description": "Motherboard",
3299+ "product": "04FCWT",
3300+ "serial": "/1N79PH2/CN129637BG002D/",
3301+ "vendor": "Dell Inc.",
3302+ "version": "A00",
3303+ }
3304+ ],
3305+ "storage_class_info": [
3306+ {
3307+ "businfo": "pci@0000:00:17.0",
3308+ "driver": "ahci",
3309+ "has_children": False,
3310+ "product": "Q170/Q150/B150/H170/H110/Z170/CM236 Chipset SATA Controller [AHCI Mode]", # noqa E501
3311+ "vendor": "Intel Corporation",
3312+ },
3313+ {
3314+ "businfo": "pci@0000:3c:00.0",
3315+ "driver": "nvme",
3316+ "has_children": False,
3317+ "product": "NVMe SSD Controller SM961/PM961",
3318+ "vendor": "Samsung Electronics Co Ltd",
3319+ },
3320+ ],
3321+ "disk_class_info": [],
3322+ "network_class_info": [
3323+ {
3324+ "businfo": "pci@0000:01:00.0",
3325+ "driver": "iwlwifi",
3326+ "driverversion": "4.15.0-46-generic",
3327+ "firmware": "34.0.1",
3328+ "logicalname": "wlp1s0",
3329+ "product": "Wireless 8265 / 8275",
3330+ "serial": "de:ad:be:ef:00:ef",
3331+ "speed": None,
3332+ "vendor": "Intel Corporation",
3333+ },
3334+ {
3335+ "businfo": "pci@0000:00:1f.6",
3336+ "driver": "e1000e",
3337+ "driverversion": "3.2.6-k",
3338+ "firmware": "0.1-3",
3339+ "logicalname": "enp0s31f6",
3340+ "product": "Ethernet Connection (5) I219-LM",
3341+ "serial": "de:ad:be:ef:00:ef",
3342+ "speed": "1Gbit/s",
3343+ "vendor": "Intel Corporation",
3344+ },
3345+ ],
3346+ },
3347+ "lshw.hp.json": {
3348+ "system": {
3349+ "description": "Rack Mount Chassis",
3350+ "hostname": "hpmachine",
3351+ "product": "ProLiant DL380 Gen9 (719061-B21)",
3352+ "serial": "MXQ53804X4",
3353+ "vendor": "HP",
3354+ "version": None,
3355+ },
3356+ "motherboard": [
3357+ {
3358+ "description": "Motherboard",
3359+ "product": None,
3360+ "serial": None,
3361+ "vendor": None,
3362+ "version": None,
3363+ }
3364+ ],
3365+ "storage_class_info": [
3366+ {
3367+ "businfo": "pci@0000:08:00.0",
3368+ "driver": "hpsa",
3369+ "has_children": True,
3370+ "product": "Smart Array Gen9 Controllers",
3371+ "vendor": "Hewlett-Packard Company",
3372+ },
3373+ {
3374+ "businfo": "pci@0000:0b:00.0",
3375+ "driver": "nvme",
3376+ "has_children": False,
3377+ "product": "PCIe Data Center SSD",
3378+ "vendor": "Intel Corporation",
3379+ },
3380+ ],
3381+ "disk_class_info": [],
3382+ "network_class_info": [
3383+ {
3384+ "businfo": "pci@0000:05:00.0",
3385+ "driver": "ixgbe",
3386+ "driverversion": "4.2.1-k",
3387+ "firmware": "0x800005ab",
3388+ "logicalname": "ens1f0",
3389+ "product": "Ethernet Controller 10-Gigabit X540-AT2",
3390+ "serial": "de:ad:be:ef:00:ef",
3391+ "speed": "10Gbit/s",
3392+ "vendor": "Intel Corporation",
3393+ },
3394+ {
3395+ "businfo": "pci@0000:05:00.1",
3396+ "driver": "ixgbe",
3397+ "driverversion": "4.2.1-k",
3398+ "firmware": "0x800005ab",
3399+ "logicalname": "ens1f1",
3400+ "product": "Ethernet Controller 10-Gigabit X540-AT2",
3401+ "serial": "de:ad:be:ef:00:ef",
3402+ "speed": "10Gbit/s",
3403+ "vendor": "Intel Corporation",
3404+ },
3405+ {
3406+ "businfo": "pci@0000:04:00.0",
3407+ "driver": "ixgbe",
3408+ "driverversion": "4.2.1-k",
3409+ "firmware": "0x800005ac",
3410+ "logicalname": "eno49",
3411+ "product": "Ethernet Controller 10-Gigabit X540-AT2",
3412+ "serial": "de:ad:be:ef:00:ef",
3413+ "speed": "10Gbit/s",
3414+ "vendor": "Intel Corporation",
3415+ },
3416+ {
3417+ "businfo": "pci@0000:04:00.1",
3418+ "driver": "ixgbe",
3419+ "driverversion": "4.2.1-k",
3420+ "firmware": "0x800005ac",
3421+ "logicalname": "eno50",
3422+ "product": "Ethernet Controller 10-Gigabit X540-AT2",
3423+ "serial": "de:ad:be:ef:00:ef",
3424+ "speed": "10Gbit/s",
3425+ "vendor": "Intel Corporation",
3426+ },
3427+ {
3428+ "businfo": "pci@0000:02:00.0",
3429+ "driver": "tg3",
3430+ "driverversion": "3.137",
3431+ "firmware": "5719-v1.41 NCSI v1.3.7.0",
3432+ "logicalname": "eno1",
3433+ "product": "NetXtreme BCM5719 Gigabit Ethernet PCIe",
3434+ "serial": "de:ad:be:ef:00:ef",
3435+ "speed": None,
3436+ "vendor": "Broadcom Corporation",
3437+ },
3438+ {
3439+ "businfo": "pci@0000:02:00.1",
3440+ "driver": "tg3",
3441+ "driverversion": "3.137",
3442+ "firmware": "5719-v1.41 NCSI v1.3.7.0",
3443+ "logicalname": "eno2",
3444+ "product": "NetXtreme BCM5719 Gigabit Ethernet PCIe",
3445+ "serial": "de:ad:be:ef:00:ef",
3446+ "speed": None,
3447+ "vendor": "Broadcom Corporation",
3448+ },
3449+ {
3450+ "businfo": "pci@0000:02:00.2",
3451+ "driver": "tg3",
3452+ "driverversion": "3.137",
3453+ "firmware": "5719-v1.41 NCSI v1.3.7.0",
3454+ "logicalname": "eno3",
3455+ "product": "NetXtreme BCM5719 Gigabit Ethernet PCIe",
3456+ "serial": "de:ad:be:ef:00:ef",
3457+ "speed": None,
3458+ "vendor": "Broadcom Corporation",
3459+ },
3460+ {
3461+ "businfo": "pci@0000:02:00.3",
3462+ "driver": "tg3",
3463+ "driverversion": "3.137",
3464+ "firmware": "5719-v1.41 NCSI v1.3.7.0",
3465+ "logicalname": "eno4",
3466+ "product": "NetXtreme BCM5719 Gigabit Ethernet PCIe",
3467+ "serial": "de:ad:be:ef:00:ef",
3468+ "speed": None,
3469+ "vendor": "Broadcom Corporation",
3470+ },
3471+ ],
3472+ },
3473+ "lshw.hpe.nonvme.json": {
3474+ "system": {
3475+ "description": "Rack Mount Chassis",
3476+ "hostname": "lcpip-hcosmg01",
3477+ "product": "ProLiant DL380 Gen10 (868706-B21)",
3478+ "serial": "2M291303M6",
3479+ "vendor": "HPE",
3480+ "version": None,
3481+ },
3482+ "motherboard": [
3483+ {
3484+ "description": "Motherboard",
3485+ "product": "ProLiant DL380 Gen10",
3486+ "serial": "PFARA%%LMBR4ZT",
3487+ "vendor": "HPE",
3488+ "version": None,
3489+ }
3490+ ],
3491+ "storage_class_info": [
3492+ {
3493+ "businfo": "pci@0000:5c:00.0",
3494+ "driver": "smartpqi",
3495+ "has_children": True,
3496+ "product": "Smart Storage PQI 12G SAS/PCIe 3",
3497+ "vendor": "Adaptec",
3498+ }
3499+ ],
3500+ "disk_class_info": [
3501+ {
3502+ "businfo": "scsi@1:1.0.0",
3503+ "dev": "8:0",
3504+ "logicalname": "/dev/sda",
3505+ "physid": "1.0.0",
3506+ "product": "LOGICAL VOLUME",
3507+ "serial": "PEYHC0DRHBU40M",
3508+ "size": 8001529315328,
3509+ "storage_parent": "Smart Storage PQI 12G SAS/PCIe 3",
3510+ },
3511+ {
3512+ "businfo": "scsi@1:1.0.1",
3513+ "dev": "8:16",
3514+ "logicalname": "/dev/sdb",
3515+ "physid": "1.0.1",
3516+ "product": "LOGICAL VOLUME",
3517+ "serial": "PEYHC0DRHBU40M",
3518+ "size": 800131645440,
3519+ "storage_parent": "Smart Storage PQI 12G SAS/PCIe 3",
3520+ },
3521+ ],
3522+ "network_class_info": [
3523+ {
3524+ "businfo": "pci@0000:02:00.0",
3525+ "driver": "tg3",
3526+ "driverversion": "3.137",
3527+ "firmware": "5719-v1.46 NCSI v1.5.1.0",
3528+ "logicalname": "eno1",
3529+ "product": "NetXtreme BCM5719 Gigabit Ethernet PCIe",
3530+ "serial": "2e:77:1a:56:c8:fb",
3531+ "speed": "1Gbit/s",
3532+ "vendor": "Broadcom Inc. and subsidiaries",
3533+ },
3534+ {
3535+ "businfo": "pci@0000:02:00.1",
3536+ "driver": "tg3",
3537+ "driverversion": "3.137",
3538+ "firmware": "5719-v1.46 NCSI v1.5.1.0",
3539+ "logicalname": "eno2",
3540+ "product": "NetXtreme BCM5719 Gigabit Ethernet PCIe",
3541+ "serial": "2e:77:1a:56:c8:fb",
3542+ "speed": "1Gbit/s",
3543+ "vendor": "Broadcom Inc. and subsidiaries",
3544+ },
3545+ {
3546+ "businfo": "pci@0000:02:00.2",
3547+ "driver": "tg3",
3548+ "driverversion": "3.137",
3549+ "firmware": "5719-v1.46 NCSI v1.5.1.0",
3550+ "logicalname": "eno3",
3551+ "product": "NetXtreme BCM5719 Gigabit Ethernet PCIe",
3552+ "serial": "b8:83:03:52:42:26",
3553+ "speed": None,
3554+ "vendor": "Broadcom Inc. and subsidiaries",
3555+ },
3556+ {
3557+ "businfo": "pci@0000:02:00.3",
3558+ "driver": "tg3",
3559+ "driverversion": "3.137",
3560+ "firmware": "5719-v1.46 NCSI v1.5.1.0",
3561+ "logicalname": "eno4",
3562+ "product": "NetXtreme BCM5719 Gigabit Ethernet PCIe",
3563+ "serial": "b8:83:03:52:42:27",
3564+ "speed": None,
3565+ "vendor": "Broadcom Inc. and subsidiaries",
3566+ },
3567+ {
3568+ "businfo": "pci@0000:5d:00.0",
3569+ "driver": "mlx5_core",
3570+ "driverversion": "5.0-0",
3571+ "firmware": "14.23.8052 (HP_2690110034)",
3572+ "logicalname": "eno5",
3573+ "product": "MT27710 Family [ConnectX-4 Lx]",
3574+ "serial": "a2:ce:45:45:2b:ea",
3575+ "speed": None,
3576+ "vendor": "Mellanox Technologies",
3577+ },
3578+ {
3579+ "businfo": "pci@0000:5d:00.1",
3580+ "driver": "mlx5_core",
3581+ "driverversion": "5.0-0",
3582+ "firmware": "14.23.8052 (HP_2690110034)",
3583+ "logicalname": "eno6",
3584+ "product": "MT27710 Family [ConnectX-4 Lx]",
3585+ "serial": "a2:ce:45:45:2b:ea",
3586+ "speed": None,
3587+ "vendor": "Mellanox Technologies",
3588+ },
3589+ ],
3590+ },
3591+ "lshw.huawei.json": {
3592+ "system": {
3593+ "description": "System",
3594+ "hostname": "huaweimachine",
3595+ "product": "RH2288H V3 (Type1Sku0)",
3596+ "serial": "2102311GHG10G2000031",
3597+ "vendor": "Huawei",
3598+ "version": "V100R003",
3599+ },
3600+ "motherboard": [
3601+ {
3602+ "description": "Motherboard",
3603+ "product": "BC11HGSA0",
3604+ "serial": "022HLVCNFB004121",
3605+ "vendor": "Huawei",
3606+ "version": "V100R003",
3607+ }
3608+ ],
3609+ "storage_class_info": [
3610+ {
3611+ "businfo": "pci@0000:01:00.0",
3612+ "driver": "mpt3sas",
3613+ "has_children": True,
3614+ "product": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3615+ "vendor": "LSI Logic / Symbios Logic",
3616+ },
3617+ {
3618+ "businfo": "pci@0000:03:00.0",
3619+ "driver": "hio",
3620+ "has_children": False,
3621+ "product": None,
3622+ "vendor": None,
3623+ },
3624+ {
3625+ "businfo": "pci@0000:00:11.4",
3626+ "driver": "ahci",
3627+ "has_children": False,
3628+ "product": "C610/X99 series chipset sSATA Controller [AHCI mode]", # noqa E501
3629+ "vendor": "Intel Corporation",
3630+ },
3631+ {
3632+ "businfo": "pci@0000:00:1f.2",
3633+ "driver": "ahci",
3634+ "has_children": False,
3635+ "product": "C610/X99 series chipset 6-Port SATA Controller [AHCI mode]", # noqa E501
3636+ "vendor": "Intel Corporation",
3637+ },
3638+ ],
3639+ "disk_class_info": [
3640+ {
3641+ "businfo": "scsi@0:0.0.0",
3642+ "dev": "8:0",
3643+ "logicalname": "/dev/sda",
3644+ "physid": "0.0.0",
3645+ "product": "HGST HUS724040AL",
3646+ "serial": "PN1334PEJ6T0XS",
3647+ "size": 4000787030016,
3648+ "storage_parent": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3649+ },
3650+ {
3651+ "businfo": "scsi@0:0.1.0",
3652+ "dev": "8:16",
3653+ "logicalname": "/dev/sdb",
3654+ "physid": "0.1.0",
3655+ "product": "HGST HUS724040AL",
3656+ "serial": "PN1334PEJ8L6XS",
3657+ "size": 4000787030016,
3658+ "storage_parent": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3659+ },
3660+ {
3661+ "businfo": "scsi@0:0.2.0",
3662+ "dev": "8:32",
3663+ "logicalname": "/dev/sdc",
3664+ "physid": "0.2.0",
3665+ "product": "HGST HUS724040AL",
3666+ "serial": "PN1334PEJ93NES",
3667+ "size": 4000787030016,
3668+ "storage_parent": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3669+ },
3670+ {
3671+ "businfo": "scsi@0:0.3.0",
3672+ "dev": "8:48",
3673+ "logicalname": "/dev/sdd",
3674+ "physid": "0.3.0",
3675+ "product": "HGST HUS724040AL",
3676+ "serial": "PN1334PEJ93J3S",
3677+ "size": 4000787030016,
3678+ "storage_parent": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3679+ },
3680+ {
3681+ "businfo": "scsi@0:0.4.0",
3682+ "dev": "8:64",
3683+ "logicalname": "/dev/sde",
3684+ "physid": "0.4.0",
3685+ "product": "HGST HUS724040AL",
3686+ "serial": "PN1334PEJ6SZYS",
3687+ "size": 4000787030016,
3688+ "storage_parent": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3689+ },
3690+ {
3691+ "businfo": "scsi@0:0.5.0",
3692+ "dev": "8:80",
3693+ "logicalname": "/dev/sdf",
3694+ "physid": "0.5.0",
3695+ "product": "HGST HUS724040AL",
3696+ "serial": "PN1334PEJ93MKS",
3697+ "size": 4000787030016,
3698+ "storage_parent": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3699+ },
3700+ {
3701+ "businfo": "scsi@0:0.6.0",
3702+ "dev": "8:96",
3703+ "logicalname": "/dev/sdg",
3704+ "physid": "0.6.0",
3705+ "product": "HGST HUS724040AL",
3706+ "serial": "PN1334PEJ8L7TS",
3707+ "size": 4000787030016,
3708+ "storage_parent": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3709+ },
3710+ {
3711+ "businfo": "scsi@0:0.7.0",
3712+ "dev": "8:112",
3713+ "logicalname": "/dev/sdh",
3714+ "physid": "0.7.0",
3715+ "product": "HGST HUS724040AL",
3716+ "serial": "PN1334PEJ8L85S",
3717+ "size": 4000787030016,
3718+ "storage_parent": "SAS2308 PCI-Express Fusion-MPT SAS-2",
3719+ },
3720+ ],
3721+ "network_class_info": [
3722+ {
3723+ "businfo": "pci@0000:02:00.0",
3724+ "driver": "igb",
3725+ "driverversion": "5.4.0-k",
3726+ "firmware": "1.63, 0x800009fa",
3727+ "logicalname": "enp2s0f0",
3728+ "product": "I350 Gigabit Network Connection",
3729+ "serial": "de:ad:be:ef:00:ef",
3730+ "speed": "1Gbit/s",
3731+ "vendor": "Intel Corporation",
3732+ },
3733+ {
3734+ "businfo": "pci@0000:02:00.1",
3735+ "driver": "igb",
3736+ "driverversion": "5.4.0-k",
3737+ "firmware": "1.63, 0x800009fa",
3738+ "logicalname": "enp2s0f1",
3739+ "product": "I350 Gigabit Network Connection",
3740+ "serial": "de:ad:be:ef:00:ef",
3741+ "speed": "1Gbit/s",
3742+ "vendor": "Intel Corporation",
3743+ },
3744+ {
3745+ "businfo": "pci@0000:82:00.0",
3746+ "driver": "mlx5_core",
3747+ "driverversion": "5.0-0",
3748+ "firmware": "14.18.2000 (MT_2420110034)",
3749+ "logicalname": "enp130s0f0",
3750+ "product": "MT27710 Family [ConnectX-4 Lx]",
3751+ "serial": "de:ad:be:ef:00:ef",
3752+ "speed": "10Gbit/s",
3753+ "vendor": "Mellanox Technologies",
3754+ },
3755+ {
3756+ "businfo": "pci@0000:82:00.1",
3757+ "driver": "mlx5_core",
3758+ "driverversion": "5.0-0",
3759+ "firmware": "14.18.2000 (MT_2420110034)",
3760+ "logicalname": "enp130s0f1",
3761+ "product": "MT27710 Family [ConnectX-4 Lx]",
3762+ "serial": "de:ad:be:ef:00:ef",
3763+ "speed": "10Gbit/s",
3764+ "vendor": "Mellanox Technologies",
3765+ },
3766+ {
3767+ "businfo": "pci@0000:81:00.0",
3768+ "driver": "mlx5_core",
3769+ "driverversion": "5.0-0",
3770+ "firmware": "14.18.2000 (MT_2420110034)",
3771+ "logicalname": "enp129s0f0",
3772+ "product": "MT27710 Family [ConnectX-4 Lx]",
3773+ "serial": "de:ad:be:ef:00:ef",
3774+ "speed": "10Gbit/s",
3775+ "vendor": "Mellanox Technologies",
3776+ },
3777+ {
3778+ "businfo": "pci@0000:81:00.1",
3779+ "driver": "mlx5_core",
3780+ "driverversion": "5.0-0",
3781+ "firmware": "14.18.2000 (MT_2420110034)",
3782+ "logicalname": "enp129s0f1",
3783+ "product": "MT27710 Family [ConnectX-4 Lx]",
3784+ "serial": "de:ad:be:ef:00:ef",
3785+ "speed": "10Gbit/s",
3786+ "vendor": "Mellanox Technologies",
3787+ },
3788+ ],
3789+ },
3790+ "lshw.supermicro.notools.json": {
3791+ "system": {
3792+ "description": "Computer",
3793+ "hostname": "supermicro01",
3794+ "product": "SYS-6029U-TR4T (To be filled by O.E.M.)",
3795+ "serial": "E263684X8302310",
3796+ "vendor": "Supermicro",
3797+ "version": "0123456789",
3798+ },
3799+ "motherboard": [
3800+ {
3801+ "description": "Motherboard",
3802+ "product": "X11DPU",
3803+ "serial": "OM181S021896",
3804+ "vendor": "Supermicro",
3805+ "version": "1.10",
3806+ }
3807+ ],
3808+ "storage_class_info": [
3809+ {
3810+ "businfo": "pci@0000:00:11.5",
3811+ "driver": "ahci",
3812+ "has_children": False,
3813+ "product": "Lewisburg SSATA Controller [AHCI mode]",
3814+ "vendor": "Intel Corporation",
3815+ },
3816+ {
3817+ "businfo": "pci@0000:00:17.0",
3818+ "driver": "ahci",
3819+ "has_children": False,
3820+ "product": "Lewisburg SATA Controller [AHCI mode]",
3821+ "vendor": "Intel Corporation",
3822+ },
3823+ ],
3824+ "disk_class_info": [],
3825+ "network_class_info": [
3826+ {
3827+ "businfo": "pci@0000:18:00.0",
3828+ "driver": "i40e",
3829+ "driverversion": "2.1.14-k",
3830+ "firmware": "6.01 0x800036f0 1.1861.0",
3831+ "logicalname": "eno1",
3832+ "product": "Ethernet Controller X710/X557-AT 10GBASE-T",
3833+ "serial": "de:ad:be:ef:00:ef",
3834+ "speed": "10Gbit/s",
3835+ "vendor": "Intel Corporation",
3836+ },
3837+ {
3838+ "businfo": "pci@0000:18:00.1",
3839+ "driver": "i40e",
3840+ "driverversion": "2.1.14-k",
3841+ "firmware": "6.01 0x800036f0 1.1861.0",
3842+ "logicalname": "eno2",
3843+ "product": "Ethernet Controller X710/X557-AT 10GBASE-T",
3844+ "serial": "de:ad:be:ef:00:ef",
3845+ "speed": "10Gbit/s",
3846+ "vendor": "Intel Corporation",
3847+ },
3848+ {
3849+ "businfo": "pci@0000:18:00.2",
3850+ "driver": "i40e",
3851+ "driverversion": "2.1.14-k",
3852+ "firmware": "6.01 0x800036f0 1.1861.0",
3853+ "logicalname": "eno3",
3854+ "product": "Ethernet Controller X710/X557-AT 10GBASE-T",
3855+ "serial": "de:ad:be:ef:00:ef",
3856+ "speed": None,
3857+ "vendor": "Intel Corporation",
3858+ },
3859+ {
3860+ "businfo": "pci@0000:18:00.3",
3861+ "driver": "i40e",
3862+ "driverversion": "2.1.14-k",
3863+ "firmware": "6.01 0x800036f0 1.1861.0",
3864+ "logicalname": "eno4",
3865+ "product": "Ethernet Controller X710/X557-AT 10GBASE-T",
3866+ "serial": "de:ad:be:ef:00:ef",
3867+ "speed": None,
3868+ "vendor": "Intel Corporation",
3869+ },
3870+ {
3871+ "businfo": "pci@0000:af:00.0",
3872+ "driver": "mlx5_core",
3873+ "driverversion": "5.0-0",
3874+ "firmware": "14.21.1000 (SM_2001000001034)",
3875+ "logicalname": "enp175s0f0",
3876+ "product": "MT27710 Family [ConnectX-4 Lx]",
3877+ "serial": "de:ad:be:ef:00:ef",
3878+ "speed": None,
3879+ "vendor": "Mellanox Technologies",
3880+ },
3881+ {
3882+ "businfo": "pci@0000:af:00.1",
3883+ "driver": "mlx5_core",
3884+ "driverversion": "5.0-0",
3885+ "firmware": "14.21.1000 (SM_2001000001034)",
3886+ "logicalname": "enp175s0f1",
3887+ "product": "MT27710 Family [ConnectX-4 Lx]",
3888+ "serial": "de:ad:be:ef:00:ef",
3889+ "speed": None,
3890+ "vendor": "Mellanox Technologies",
3891+ },
3892+ {
3893+ "businfo": "pci@0000:d8:00.0",
3894+ "driver": "mlx5_core",
3895+ "driverversion": "5.0-0",
3896+ "firmware": "14.21.1000 (SM_2001000001034)",
3897+ "logicalname": "enp216s0f0",
3898+ "product": "MT27710 Family [ConnectX-4 Lx]",
3899+ "serial": "de:ad:be:ef:00:ef",
3900+ "speed": None,
3901+ "vendor": "Mellanox Technologies",
3902+ },
3903+ {
3904+ "businfo": "pci@0000:d8:00.1",
3905+ "driver": "mlx5_core",
3906+ "driverversion": "5.0-0",
3907+ "firmware": "14.21.1000 (SM_2001000001034)",
3908+ "logicalname": "enp216s0f1",
3909+ "product": "MT27710 Family [ConnectX-4 Lx]",
3910+ "serial": "de:ad:be:ef:00:ef",
3911+ "speed": None,
3912+ "vendor": "Mellanox Technologies",
3913+ },
3914+ ],
3915+ },
3916+ "lshw.supermicro.nvme.json": {
3917+ "system": {
3918+ "description": "Computer",
3919+ "hostname": "supermicro-nvme",
3920+ "product": "SYS-2029U-TN24R4T (To be filled by O.E.M.)",
3921+ "serial": "E264025X8400712",
3922+ "vendor": "Supermicro",
3923+ "version": "0123456789",
3924+ },
3925+ "motherboard": [
3926+ {
3927+ "description": "Motherboard",
3928+ "product": "X11DPU",
3929+ "serial": "OM183S019050",
3930+ "vendor": "Supermicro",
3931+ "version": "1.10",
3932+ }
3933+ ],
3934+ "storage_class_info": [
3935+ {
3936+ "businfo": "pci@0000:00:11.5",
3937+ "driver": "ahci",
3938+ "has_children": False,
3939+ "product": "Lewisburg SSATA Controller [AHCI mode]",
3940+ "vendor": "Intel Corporation",
3941+ },
3942+ {
3943+ "businfo": "pci@0000:00:17.0",
3944+ "driver": "ahci",
3945+ "has_children": False,
3946+ "product": "Lewisburg SATA Controller [AHCI mode]",
3947+ "vendor": "Intel Corporation",
3948+ },
3949+ {
3950+ "businfo": "pci@0000:60:00.0",
3951+ "driver": "nvme",
3952+ "has_children": False,
3953+ "product": "Intel Corporation",
3954+ "vendor": "Intel Corporation",
3955+ },
3956+ {
3957+ "businfo": "pci@0000:61:00.0",
3958+ "driver": "nvme",
3959+ "has_children": False,
3960+ "product": "Intel Corporation",
3961+ "vendor": "Intel Corporation",
3962+ },
3963+ {
3964+ "businfo": "pci@0000:62:00.0",
3965+ "driver": "nvme",
3966+ "has_children": False,
3967+ "product": "Intel Corporation",
3968+ "vendor": "Intel Corporation",
3969+ },
3970+ {
3971+ "businfo": "pci@0000:63:00.0",
3972+ "driver": "nvme",
3973+ "has_children": False,
3974+ "product": "Intel Corporation",
3975+ "vendor": "Intel Corporation",
3976+ },
3977+ {
3978+ "businfo": "pci@0000:64:00.0",
3979+ "driver": "nvme",
3980+ "has_children": False,
3981+ "product": "Intel Corporation",
3982+ "vendor": "Intel Corporation",
3983+ },
3984+ {
3985+ "businfo": "pci@0000:65:00.0",
3986+ "driver": "nvme",
3987+ "has_children": False,
3988+ "product": "Intel Corporation",
3989+ "vendor": "Intel Corporation",
3990+ },
3991+ {
3992+ "businfo": "pci@0000:66:00.0",
3993+ "driver": "nvme",
3994+ "has_children": False,
3995+ "product": "Intel Corporation",
3996+ "vendor": "Intel Corporation",
3997+ },
3998+ {
3999+ "businfo": "pci@0000:67:00.0",
4000+ "driver": "nvme",
4001+ "has_children": False,
4002+ "product": "Intel Corporation",
4003+ "vendor": "Intel Corporation",
4004+ },
4005+ {
4006+ "businfo": "pci@0000:68:00.0",
4007+ "driver": "nvme",
4008+ "has_children": False,
4009+ "product": "Intel Corporation",
4010+ "vendor": "Intel Corporation",
4011+ },
4012+ {
4013+ "businfo": "pci@0000:69:00.0",
4014+ "driver": "nvme",
4015+ "has_children": False,
4016+ "product": "Intel Corporation",
4017+ "vendor": "Intel Corporation",
4018+ },
4019+ {
4020+ "businfo": "pci@0000:6a:00.0",
4021+ "driver": "nvme",
4022+ "has_children": False,
4023+ "product": "Intel Corporation",
4024+ "vendor": "Intel Corporation",
4025+ },
4026+ ],
4027+ "disk_class_info": [],
4028+ "network_class_info": [
4029+ {
4030+ "businfo": "pci@0000:18:00.0",
4031+ "driver": "ixgbe",
4032+ "driverversion": "5.1.0-k",
4033+ "firmware": "0x800005f8",
4034+ "logicalname": "eno1",
4035+ "product": "Ethernet Controller 10G X550T",
4036+ "serial": "de:ad:be:ef:00:ef",
4037+ "speed": "10Gbit/s",
4038+ "vendor": "Intel Corporation",
4039+ },
4040+ {
4041+ "businfo": "pci@0000:18:00.1",
4042+ "driver": "ixgbe",
4043+ "driverversion": "5.1.0-k",
4044+ "firmware": "0x800005f8",
4045+ "logicalname": "eno2",
4046+ "product": "Ethernet Controller 10G X550T",
4047+ "serial": "de:ad:be:ef:00:ef",
4048+ "speed": "10Gbit/s",
4049+ "vendor": "Intel Corporation",
4050+ },
4051+ {
4052+ "businfo": "pci@0000:19:00.0",
4053+ "driver": "ixgbe",
4054+ "driverversion": "5.1.0-k",
4055+ "firmware": "0x800005f9",
4056+ "logicalname": "eno3",
4057+ "product": "Ethernet Controller 10G X550T",
4058+ "serial": "de:ad:be:ef:00:ef",
4059+ "speed": None,
4060+ "vendor": "Intel Corporation",
4061+ },
4062+ {
4063+ "businfo": "pci@0000:19:00.1",
4064+ "driver": "ixgbe",
4065+ "driverversion": "5.1.0-k",
4066+ "firmware": "0x800005f9",
4067+ "logicalname": "eno4",
4068+ "product": "Ethernet Controller 10G X550T",
4069+ "serial": "de:ad:be:ef:00:ef",
4070+ "speed": None,
4071+ "vendor": "Intel Corporation",
4072+ },
4073+ {
4074+ "businfo": "pci@0000:3b:00.0",
4075+ "driver": "mlx5_core",
4076+ "driverversion": "5.0-0",
4077+ "firmware": "12.17.2020 (SM_2001000001033)",
4078+ "logicalname": "enp59s0f0",
4079+ "product": "MT27700 Family [ConnectX-4]",
4080+ "serial": "de:ad:be:ef:00:ef",
4081+ "speed": None,
4082+ "vendor": "Mellanox Technologies",
4083+ },
4084+ {
4085+ "businfo": "pci@0000:3b:00.1",
4086+ "driver": "mlx5_core",
4087+ "driverversion": "5.0-0",
4088+ "firmware": "12.17.2020 (SM_2001000001033)",
4089+ "logicalname": "enp59s0f1",
4090+ "product": "MT27700 Family [ConnectX-4]",
4091+ "serial": "de:ad:be:ef:00:ef",
4092+ "speed": None,
4093+ "vendor": "Mellanox Technologies",
4094+ },
4095+ {
4096+ "businfo": "pci@0000:d8:00.0",
4097+ "driver": "mlx5_core",
4098+ "driverversion": "5.0-0",
4099+ "firmware": "14.21.1000 (SM_2001000001034)",
4100+ "logicalname": "enp216s0f0",
4101+ "product": "MT27710 Family [ConnectX-4 Lx]",
4102+ "serial": "de:ad:be:ef:00:ef",
4103+ "speed": None,
4104+ "vendor": "Mellanox Technologies",
4105+ },
4106+ {
4107+ "businfo": "pci@0000:d8:00.1",
4108+ "driver": "mlx5_core",
4109+ "driverversion": "5.0-0",
4110+ "firmware": "14.21.1000 (SM_2001000001034)",
4111+ "logicalname": "enp216s0f1",
4112+ "product": "MT27710 Family [ConnectX-4 Lx]",
4113+ "serial": "de:ad:be:ef:00:ef",
4114+ "speed": None,
4115+ "vendor": "Mellanox Technologies",
4116+ },
4117+ ],
4118+ },
4119+ "lshw.supermicro.sas.01.json": {
4120+ "system": {
4121+ "description": "Computer",
4122+ "hostname": "supermicro-sas01",
4123+ "product": "SYS-6029U-TR4T (To be filled by O.E.M.)",
4124+ "serial": "E263684X8302334",
4125+ "vendor": "Supermicro",
4126+ "version": "0123456789",
4127+ },
4128+ "motherboard": [
4129+ {
4130+ "description": "Motherboard",
4131+ "product": "X11DPU",
4132+ "serial": "OM183S008126",
4133+ "vendor": "Supermicro",
4134+ "version": "1.10",
4135+ }
4136+ ],
4137+ "storage_class_info": [
4138+ {
4139+ "businfo": "pci@0000:00:11.5",
4140+ "driver": "ahci",
4141+ "has_children": False,
4142+ "product": "Lewisburg SSATA Controller [AHCI mode]",
4143+ "vendor": "Intel Corporation",
4144+ },
4145+ {
4146+ "businfo": "pci@0000:00:17.0",
4147+ "driver": "ahci",
4148+ "has_children": False,
4149+ "product": "Lewisburg SATA Controller [AHCI mode]",
4150+ "vendor": "Intel Corporation",
4151+ },
4152+ {
4153+ "businfo": "pci@0000:1a:00.0",
4154+ "driver": "nvme",
4155+ "has_children": False,
4156+ "product": "Intel Corporation",
4157+ "vendor": "Intel Corporation",
4158+ },
4159+ {
4160+ "businfo": "pci@0000:86:00.0",
4161+ "driver": "mpt3sas",
4162+ "has_children": True,
4163+ "product": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4164+ "vendor": "LSI Logic / Symbios Logic",
4165+ },
4166+ {
4167+ "businfo": "pci@0000:af:00.0",
4168+ "driver": "mpt3sas",
4169+ "has_children": True,
4170+ "product": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4171+ "vendor": "LSI Logic / Symbios Logic",
4172+ },
4173+ ],
4174+ "disk_class_info": [
4175+ {
4176+ "businfo": "scsi@0:0.0.0",
4177+ "dev": "8:0",
4178+ "logicalname": "/dev/sda",
4179+ "physid": "0.0.0",
4180+ "product": "HUS726060AL5210",
4181+ "serial": "K1HX9AJD",
4182+ "size": 6001175126016,
4183+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4184+ },
4185+ {
4186+ "businfo": "scsi@0:0.1.0",
4187+ "dev": "8:16",
4188+ "logicalname": "/dev/sdb",
4189+ "physid": "0.1.0",
4190+ "product": "HUS726060AL5210",
4191+ "serial": "K1HXJ44F",
4192+ "size": 6001175126016,
4193+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4194+ },
4195+ {
4196+ "businfo": "scsi@0:0.2.0",
4197+ "dev": "8:32",
4198+ "logicalname": "/dev/sdc",
4199+ "physid": "0.2.0",
4200+ "product": "HUS726060AL5210",
4201+ "serial": "K1HWXPJD",
4202+ "size": 6001175126016,
4203+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4204+ },
4205+ {
4206+ "businfo": "scsi@0:0.3.0",
4207+ "dev": "8:48",
4208+ "logicalname": "/dev/sdd",
4209+ "physid": "0.3.0",
4210+ "product": "HUS726060AL5210",
4211+ "serial": "K1HXJ6YF",
4212+ "size": 6001175126016,
4213+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4214+ },
4215+ {
4216+ "businfo": "scsi@0:0.4.0",
4217+ "dev": "8:64",
4218+ "logicalname": "/dev/sde",
4219+ "physid": "0.4.0",
4220+ "product": "HUS726060AL5210",
4221+ "serial": "K1HWXPDD",
4222+ "size": 6001175126016,
4223+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4224+ },
4225+ {
4226+ "businfo": "scsi@0:0.5.0",
4227+ "dev": "8:80",
4228+ "logicalname": "/dev/sdf",
4229+ "physid": "0.5.0",
4230+ "product": "HUS726060AL5210",
4231+ "serial": "K1HXJLUF",
4232+ "size": 6001175126016,
4233+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4234+ },
4235+ {
4236+ "businfo": "scsi@0:0.6.0",
4237+ "dev": "8:96",
4238+ "logicalname": "/dev/sdg",
4239+ "physid": "0.6.0",
4240+ "product": "HUS726060AL5210",
4241+ "serial": "K1HWXPYD",
4242+ "size": 6001175126016,
4243+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4244+ },
4245+ {
4246+ "businfo": "scsi@0:0.7.0",
4247+ "dev": "8:112",
4248+ "logicalname": "/dev/sdh",
4249+ "physid": "0.7.0",
4250+ "product": "HUS726060AL5210",
4251+ "serial": "K1HWXP4D",
4252+ "size": 6001175126016,
4253+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4254+ },
4255+ {
4256+ "businfo": "scsi@1:0.2.0",
4257+ "dev": "8:160",
4258+ "logicalname": "/dev/sdk",
4259+ "physid": "0.2.0",
4260+ "product": "ST6000NM0095",
4261+ "serial": "ZAD4S0DW0000C843M2UC",
4262+ "size": 6001175126016,
4263+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4264+ },
4265+ {
4266+ "businfo": "scsi@1:0.0.0",
4267+ "dev": "8:128",
4268+ "logicalname": "/dev/sdi",
4269+ "physid": "0.0.0",
4270+ "product": "HUS726060AL5210",
4271+ "serial": "K1HWGU9D",
4272+ "size": 6001175126016,
4273+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4274+ },
4275+ {
4276+ "businfo": "scsi@1:0.1.0",
4277+ "dev": "8:144",
4278+ "logicalname": "/dev/sdj",
4279+ "physid": "0.1.0",
4280+ "product": "ST6000NM0095",
4281+ "serial": "ZAD4S0A50000C8432082",
4282+ "size": 6001175126016,
4283+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4284+ },
4285+ ],
4286+ "network_class_info": [
4287+ {
4288+ "businfo": "pci@0000:18:00.0",
4289+ "driver": "i40e",
4290+ "driverversion": "2.1.14-k",
4291+ "firmware": "6.01 0x800036f0 1.1861.0",
4292+ "logicalname": "eno1",
4293+ "product": "Ethernet Controller X710/X557-AT 10GBASE-T",
4294+ "serial": "de:ad:be:ef:00:ef",
4295+ "speed": "10Gbit/s",
4296+ "vendor": "Intel Corporation",
4297+ },
4298+ {
4299+ "businfo": "pci@0000:18:00.1",
4300+ "driver": "i40e",
4301+ "driverversion": "2.1.14-k",
4302+ "firmware": "6.01 0x800036f0 1.1861.0",
4303+ "logicalname": "eno2",
4304+ "product": "Ethernet Controller X710/X557-AT 10GBASE-T",
4305+ "serial": "de:ad:be:ef:00:ef",
4306+ "speed": "10Gbit/s",
4307+ "vendor": "Intel Corporation",
4308+ },
4309+ {
4310+ "businfo": "pci@0000:18:00.2",
4311+ "driver": "i40e",
4312+ "driverversion": "2.1.14-k",
4313+ "firmware": "6.01 0x800036f0 1.1861.0",
4314+ "logicalname": "eno3",
4315+ "product": "Ethernet Controller X710/X557-AT 10GBASE-T",
4316+ "serial": "de:ad:be:ef:00:ef",
4317+ "speed": None,
4318+ "vendor": "Intel Corporation",
4319+ },
4320+ {
4321+ "businfo": "pci@0000:18:00.3",
4322+ "driver": "i40e",
4323+ "driverversion": "2.1.14-k",
4324+ "firmware": "6.01 0x800036f0 1.1861.0",
4325+ "logicalname": "eno4",
4326+ "product": "Ethernet Controller X710/X557-AT 10GBASE-T",
4327+ "serial": "de:ad:be:ef:00:ef",
4328+ "speed": None,
4329+ "vendor": "Intel Corporation",
4330+ },
4331+ {
4332+ "businfo": "pci@0000:3b:00.0",
4333+ "driver": "mlx5_core",
4334+ "driverversion": "5.0-0",
4335+ "firmware": "12.17.2020 (SM_2001000001033)",
4336+ "logicalname": "enp59s0f0",
4337+ "product": "MT27700 Family [ConnectX-4]",
4338+ "serial": "de:ad:be:ef:00:ef",
4339+ "speed": None,
4340+ "vendor": "Mellanox Technologies",
4341+ },
4342+ {
4343+ "businfo": "pci@0000:3b:00.1",
4344+ "driver": "mlx5_core",
4345+ "driverversion": "5.0-0",
4346+ "firmware": "12.17.2020 (SM_2001000001033)",
4347+ "logicalname": "enp59s0f1",
4348+ "product": "MT27700 Family [ConnectX-4]",
4349+ "serial": "de:ad:be:ef:00:ef",
4350+ "speed": None,
4351+ "vendor": "Mellanox Technologies",
4352+ },
4353+ {
4354+ "businfo": "pci@0000:d8:00.0",
4355+ "driver": "mlx5_core",
4356+ "driverversion": "5.0-0",
4357+ "firmware": "14.21.1000 (SM_2001000001034)",
4358+ "logicalname": "enp216s0f0",
4359+ "product": "MT27710 Family [ConnectX-4 Lx]",
4360+ "serial": "de:ad:be:ef:00:ef",
4361+ "speed": None,
4362+ "vendor": "Mellanox Technologies",
4363+ },
4364+ {
4365+ "businfo": "pci@0000:d8:00.1",
4366+ "driver": "mlx5_core",
4367+ "driverversion": "5.0-0",
4368+ "firmware": "14.21.1000 (SM_2001000001034)",
4369+ "logicalname": "enp216s0f1",
4370+ "product": "MT27710 Family [ConnectX-4 Lx]",
4371+ "serial": "de:ad:be:ef:00:ef",
4372+ "speed": None,
4373+ "vendor": "Mellanox Technologies",
4374+ },
4375+ ],
4376+ },
4377+ "lshw.supermicro.sas.02.json": {
4378+ "system": {
4379+ "description": "Computer",
4380+ "hostname": "supermicro-sas02",
4381+ "product": "SYS-6028U-TR4+ (Default string)",
4382+ "serial": "S16512428221502",
4383+ "vendor": "Supermicro",
4384+ "version": "0123456789",
4385+ },
4386+ "motherboard": [
4387+ {
4388+ "description": "Motherboard",
4389+ "product": "X10DRU-i+",
4390+ "serial": "OM17CS000182",
4391+ "vendor": "Supermicro",
4392+ "version": "1.02B",
4393+ }
4394+ ],
4395+ "storage_class_info": [
4396+ {
4397+ "businfo": "pci@0000:03:00.0",
4398+ "driver": "mpt3sas",
4399+ "has_children": True,
4400+ "product": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4401+ "vendor": "LSI Logic / Symbios Logic",
4402+ },
4403+ {
4404+ "businfo": "pci@0000:00:11.4",
4405+ "driver": "ahci",
4406+ "has_children": False,
4407+ "product": "C610/X99 series chipset sSATA Controller [AHCI mode]", # noqa E501
4408+ "vendor": "Intel Corporation",
4409+ },
4410+ {
4411+ "businfo": "pci@0000:00:1f.2",
4412+ "driver": "ahci",
4413+ "has_children": False,
4414+ "product": "C610/X99 series chipset 6-Port SATA Controller [AHCI mode]", # noqa E501
4415+ "vendor": "Intel Corporation",
4416+ },
4417+ {
4418+ "businfo": "pci@0000:81:00.0",
4419+ "driver": "nvme",
4420+ "has_children": False,
4421+ "product": "NVMe SSD Controller SM961/PM961",
4422+ "vendor": "Samsung Electronics Co Ltd",
4423+ },
4424+ {
4425+ "businfo": "pci@0000:82:00.0",
4426+ "driver": "nvme",
4427+ "has_children": False,
4428+ "product": "NVMe SSD Controller SM961/PM961",
4429+ "vendor": "Samsung Electronics Co Ltd",
4430+ },
4431+ ],
4432+ "disk_class_info": [
4433+ {
4434+ "businfo": "scsi@10:0.0.0",
4435+ "dev": None,
4436+ "logicalname": None,
4437+ "physid": "0.0.0",
4438+ "product": "ST4000NM115-1YZ1",
4439+ "serial": "ZC10Z0XQ",
4440+ "size": None,
4441+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4442+ },
4443+ {
4444+ "businfo": "scsi@10:1.0.0",
4445+ "dev": "8:0",
4446+ "logicalname": "/dev/sda",
4447+ "physid": "1.0.0",
4448+ "product": "Logical Volume",
4449+ "serial": "3589195262245447776",
4450+ "size": 3999999721472,
4451+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4452+ },
4453+ {
4454+ "businfo": "scsi@10:0.1.0",
4455+ "dev": None,
4456+ "logicalname": None,
4457+ "physid": "0.1.0",
4458+ "product": "ST4000NM115-1YZ1",
4459+ "serial": "ZC10Z0ZL",
4460+ "size": None,
4461+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4462+ },
4463+ {
4464+ "businfo": "scsi@10:0.2.0",
4465+ "dev": "8:16",
4466+ "logicalname": "/dev/sdb",
4467+ "physid": "0.2.0",
4468+ "product": "ST4000NM115-1YZ1",
4469+ "serial": "ZC10YVHJ",
4470+ "size": 4000787030016,
4471+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4472+ },
4473+ {
4474+ "businfo": "scsi@10:0.3.0",
4475+ "dev": "8:32",
4476+ "logicalname": "/dev/sdc",
4477+ "physid": "0.3.0",
4478+ "product": "ST4000NM115-1YZ1",
4479+ "serial": "ZC10XX97",
4480+ "size": 4000787030016,
4481+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4482+ },
4483+ {
4484+ "businfo": "scsi@10:0.4.0",
4485+ "dev": "8:48",
4486+ "logicalname": "/dev/sdd",
4487+ "physid": "0.4.0",
4488+ "product": "ST4000NM115-1YZ1",
4489+ "serial": "ZC10SFX1",
4490+ "size": 4000787030016,
4491+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4492+ },
4493+ {
4494+ "businfo": "scsi@10:0.5.0",
4495+ "dev": "8:64",
4496+ "logicalname": "/dev/sde",
4497+ "physid": "0.5.0",
4498+ "product": "ST4000NM115-1YZ1",
4499+ "serial": "ZC10YVK1",
4500+ "size": 4000787030016,
4501+ "storage_parent": "SAS3008 PCI-Express Fusion-MPT SAS-3",
4502+ },
4503+ ],
4504+ "network_class_info": [
4505+ {
4506+ "businfo": "pci@0000:01:00.0",
4507+ "driver": "igb",
4508+ "driverversion": "5.3.0-k",
4509+ "firmware": "1.63, 0x800009fa",
4510+ "logicalname": "enp1s0f0",
4511+ "product": "I350 Gigabit Network Connection",
4512+ "serial": "de:ad:be:ef:00:ef",
4513+ "speed": "1Gbit/s",
4514+ "vendor": "Intel Corporation",
4515+ },
4516+ {
4517+ "businfo": "pci@0000:01:00.1",
4518+ "driver": "igb",
4519+ "driverversion": "5.3.0-k",
4520+ "firmware": "1.63, 0x800009fa",
4521+ "logicalname": "enp1s0f1",
4522+ "product": "I350 Gigabit Network Connection",
4523+ "serial": "de:ad:be:ef:00:ef",
4524+ "speed": None,
4525+ "vendor": "Intel Corporation",
4526+ },
4527+ {
4528+ "businfo": "pci@0000:01:00.2",
4529+ "driver": "igb",
4530+ "driverversion": "5.3.0-k",
4531+ "firmware": "1.63, 0x800009fa",
4532+ "logicalname": "enp1s0f2",
4533+ "product": "I350 Gigabit Network Connection",
4534+ "serial": "de:ad:be:ef:00:ef",
4535+ "speed": "1Gbit/s",
4536+ "vendor": "Intel Corporation",
4537+ },
4538+ {
4539+ "businfo": "pci@0000:01:00.3",
4540+ "driver": "igb",
4541+ "driverversion": "5.3.0-k",
4542+ "firmware": "1.63, 0x800009fa",
4543+ "logicalname": "enp1s0f3",
4544+ "product": "I350 Gigabit Network Connection",
4545+ "serial": "de:ad:be:ef:00:ef",
4546+ "speed": None,
4547+ "vendor": "Intel Corporation",
4548+ },
4549+ {
4550+ "businfo": "pci@0000:83:00.0",
4551+ "driver": "ixgbe",
4552+ "driverversion": "4.2.1-k",
4553+ "firmware": "0x800006da",
4554+ "logicalname": "enp131s0f0",
4555+ "product": "82599ES 10-Gigabit SFI/SFP+ Network Connection",
4556+ "serial": "de:ad:be:ef:00:ef",
4557+ "speed": "10Gbit/s",
4558+ "vendor": "Intel Corporation",
4559+ },
4560+ {
4561+ "businfo": "pci@0000:83:00.1",
4562+ "driver": "ixgbe",
4563+ "driverversion": "4.2.1-k",
4564+ "firmware": "0x800006da",
4565+ "logicalname": "enp131s0f1",
4566+ "product": "82599ES 10-Gigabit SFI/SFP+ Network Connection",
4567+ "serial": "de:ad:be:ef:00:ef",
4568+ "speed": "10Gbit/s",
4569+ "vendor": "Intel Corporation",
4570+ },
4571+ {
4572+ "businfo": "pci@0000:84:00.0",
4573+ "driver": "ixgbe",
4574+ "driverversion": "4.2.1-k",
4575+ "firmware": "0x800006da",
4576+ "logicalname": "ens4f0",
4577+ "product": "82599ES 10-Gigabit SFI/SFP+ Network Connection",
4578+ "serial": "de:ad:be:ef:00:ef",
4579+ "speed": "10Gbit/s",
4580+ "vendor": "Intel Corporation",
4581+ },
4582+ {
4583+ "businfo": "pci@0000:84:00.1",
4584+ "driver": "ixgbe",
4585+ "driverversion": "4.2.1-k",
4586+ "firmware": "0x800006da",
4587+ "logicalname": "ens4f1",
4588+ "product": "82599ES 10-Gigabit SFI/SFP+ Network Connection",
4589+ "serial": "de:ad:be:ef:00:ef",
4590+ "speed": "10Gbit/s",
4591+ "vendor": "Intel Corporation",
4592+ },
4593+ ],
4594+ },
4595+}
4596diff --git a/src/tests/functional/requirements.txt b/src/tests/functional/requirements.txt
4597index 193a938..88925a4 100644
4598--- a/src/tests/functional/requirements.txt
4599+++ b/src/tests/functional/requirements.txt
4600@@ -5,3 +5,4 @@ pytest
4601 pytest-asyncio
4602 requests
4603 charmhelpers
4604+charms.reactive
4605diff --git a/src/tests/functional/test_hwhealth.py b/src/tests/functional/test_hwhealth.py
4606index 51d407a..3e544ea 100644
4607--- a/src/tests/functional/test_hwhealth.py
4608+++ b/src/tests/functional/test_hwhealth.py
4609@@ -19,16 +19,18 @@ SERIES = [
4610 ]
4611 JUJU_REPOSITORY = os.getenv('JUJU_REPOSITORY', '.')
4612 NRPECFG_DIR = '/etc/nagios/nrpe.d'
4613+DEF_TIMEOUT = 600
4614+# These go along with the hpe repos for the hp* tools
4615
4616 ################
4617 # Helper funcs #
4618 ################
4619
4620
4621-async def deploy_hwhealth_res(model, app_name, tool_name):
4622+async def deploy_hwhealth_res(model, app_name, res_filename):
4623 # Attaching resources is not implemented yet in libjuju
4624 # see https://github.com/juju/python-libjuju/issues/294
4625- tools_res_path = os.path.join(JUJU_REPOSITORY, tool_name)
4626+ tools_res_path = os.path.join(JUJU_REPOSITORY, 'builds', res_filename)
4627 subprocess.check_call([
4628 'juju',
4629 'deploy',
4630@@ -41,8 +43,8 @@ async def deploy_hwhealth_res(model, app_name, tool_name):
4631 ])
4632
4633
4634-async def update_hwhealth_res(model, app_name, tool_name):
4635- tools_res_path = os.path.join(JUJU_REPOSITORY, tool_name)
4636+async def update_hwhealth_res(model, app_name, res_filename):
4637+ tools_res_path = os.path.join(JUJU_REPOSITORY, 'builds', res_filename)
4638 subprocess.check_call([
4639 'juju',
4640 'attach-resource',
4641@@ -82,8 +84,12 @@ async def deploy_app(request, model):
4642 series=series,
4643 channel=channel,
4644 )
4645- await model.deploy('ubuntu', application_name='ubuntu-checksum-{}'.format(release),
4646- series=release, channel=channel)
4647+ await model.deploy(
4648+ 'ubuntu',
4649+ application_name='ubuntu-checksum-{}'.format(release),
4650+ series=release,
4651+ channel=channel
4652+ )
4653 nrpe_app = await model.deploy(
4654 'nrpe',
4655 application_name='nrpe-{}'.format(release),
4656@@ -104,33 +110,43 @@ async def deploy_app(request, model):
4657 # Attaching resources is not implemented yet in libjuju
4658 # see https://github.com/juju/python-libjuju/issues/294
4659 await deploy_hwhealth_res(model, hw_health_app_name, 'tools.zip')
4660- await deploy_hwhealth_res(model, hw_health_checksum_app_name, 'tools-checksum.zip')
4661+ await deploy_hwhealth_res(model, hw_health_checksum_app_name,
4662+ 'tools-checksum.zip')
4663
4664 # This is pretty horrible, but we can't deploy via libjuju
4665 while True:
4666 try:
4667 hw_health_app = model.applications[hw_health_app_name]
4668- hw_health_checksum_app = model.applications[hw_health_checksum_app_name]
4669+ hw_health_checksum_app = \
4670+ model.applications[hw_health_checksum_app_name]
4671 break
4672 except KeyError:
4673 await asyncio.sleep(5)
4674
4675- await hw_health_app.add_relation('general-info',
4676- 'ubuntu-{}:juju-info'.format(release))
4677- await hw_health_app.add_relation('nrpe-external-master',
4678- '{}:nrpe-external-master'.format(nrpe_app.name))
4679+ await hw_health_app.add_relation(
4680+ 'general-info',
4681+ 'ubuntu-{}:juju-info'.format(release)
4682+ )
4683+ await hw_health_app.add_relation(
4684+ 'nrpe-external-master',
4685+ '{}:nrpe-external-master'.format(nrpe_app.name)
4686+ )
4687
4688- await hw_health_checksum_app.add_relation('general-info',
4689- 'ubuntu-checksum-{}:juju-info'.format(release))
4690- await hw_health_checksum_app.add_relation('nrpe-external-master',
4691- '{}:nrpe-external-master'.format(nrpe_app.name))
4692+ await hw_health_checksum_app.add_relation(
4693+ 'general-info',
4694+ 'ubuntu-checksum-{}:juju-info'.format(release)
4695+ )
4696+ await hw_health_checksum_app.add_relation(
4697+ 'nrpe-external-master',
4698+ '{}:nrpe-external-master'.format(nrpe_app.name)
4699+ )
4700
4701 # The app will initially be in blocked state because it's running in a
4702 # container
4703 await model.block_until(
4704- lambda: (hw_health_app.status == 'blocked' and
4705+ lambda: (hw_health_app.status == 'blocked' and # noqa:W504
4706 hw_health_checksum_app.status == 'blocked'),
4707- timeout=600
4708+ timeout=DEF_TIMEOUT
4709 )
4710 yield hw_health_app
4711
4712@@ -151,10 +167,10 @@ async def toolset(monkeypatch):
4713 m.setattr('charmhelpers.core.hookenv.charm_dir',
4714 lambda: JUJU_REPOSITORY)
4715 m.setattr('charmhelpers.core.hookenv.config',
4716- lambda x=None: dict())
4717+ lambda x=None: dict({'manufacturer': 'test'}))
4718 m.setattr('charmhelpers.contrib.charmsupport.nrpe.get_nagios_hostname',
4719 lambda: 'pytest')
4720- return hwdiscovery.get_tools('test')
4721+ return [tool() for tool in hwdiscovery.get_tools('test')]
4722
4723 #########
4724 # Tests #
4725@@ -172,14 +188,17 @@ async def test_forced_deploy(deploy_app, model, run_command):
4726 for unit in model.units.values():
4727 if unit.entity_id.startswith('ubuntu-{}'.format(series)):
4728 ubuntu_unit = unit
4729- await model.block_until(lambda: ubuntu_unit.workload_status == 'active', timeout=180)
4730+ await model.block_until(
4731+ lambda: ubuntu_unit.workload_status == 'active',
4732+ timeout=DEF_TIMEOUT
4733+ )
4734 await run_command(CREATE_FAKE_NVME, ubuntu_unit)
4735 break
4736
4737 await deploy_app.set_config({'manufacturer': 'test'})
4738 await model.block_until(
4739 lambda: deploy_app.status == 'active',
4740- timeout=600
4741+ timeout=DEF_TIMEOUT
4742 )
4743 assert deploy_app.status == 'active'
4744
4745@@ -188,64 +207,84 @@ async def test_checksum_forced_deploy(deploy_app, model, run_command):
4746 # Create a fake NVMe device for the cronjob to be configured
4747 CREATE_FAKE_NVME = "/bin/bash -c 'touch /dev/nvme0'"
4748 series = deploy_app.name.split('-')[-1]
4749- hw_health_checksum_app_name = 'hw-health-checksum-{}'.format(series)
4750- hw_health_checksum_app = model.applications[hw_health_checksum_app_name]
4751+ checksum_app_name = 'hw-health-checksum-{}'.format(series)
4752+ checksum_app = model.applications[checksum_app_name]
4753 for unit in model.units.values():
4754 if unit.entity_id.startswith('ubuntu-checksum-{}'.format(series)):
4755 ubuntu_unit = unit
4756- await model.block_until(lambda: ubuntu_unit.workload_status == 'active',
4757- timeout=180)
4758+ await model.block_until(
4759+ lambda: ubuntu_unit.workload_status == 'active',
4760+ timeout=DEF_TIMEOUT
4761+ )
4762 await run_command(CREATE_FAKE_NVME, ubuntu_unit)
4763- elif unit.entity_id.startswith(hw_health_checksum_app_name):
4764- hw_health_checksum_unit = unit
4765-
4766- await hw_health_checksum_app.set_config({'manufacturer': 'test'})
4767- await model.block_until(
4768- lambda: (hw_health_checksum_app.status == 'blocked' and
4769- hw_health_checksum_unit.workload_status_message == 'Tool megacli - checksum error'),
4770- timeout=600)
4771+ elif unit.entity_id.startswith(checksum_app_name):
4772+ checksum_unit = unit
4773+
4774+ await checksum_app.set_config({'manufacturer': 'test'})
4775+ try:
4776+ await model.block_until(
4777+ lambda: (
4778+ checksum_app.status == 'blocked' and checksum_unit.workload_status_message == 'Tool megacli - checksum error' # noqa E501
4779+ ),
4780+ timeout=DEF_TIMEOUT)
4781+ except asyncio.exceptions.TimeoutError:
4782+ print(
4783+ "failed to get expected state 'blocked:Tool megacli - checksum error', "
4784+ "witnessed '{}:{}'".format(checksum_app.status, checksum_unit.workload_status_message)
4785+ )
4786+ assert checksum_app.status == 'blocked'
4787+ assert checksum_unit.workload_status_message == 'Tool megacli - checksum error'
4788
4789
4790 async def test_checksum_updated_resource_missing(deploy_app, model):
4791 series = deploy_app.name.split('-')[-1]
4792- hw_health_checksum_app_name = 'hw-health-checksum-{}'.format(series)
4793- hw_health_checksum_app = model.applications[hw_health_checksum_app_name]
4794- await update_hwhealth_res(model, hw_health_checksum_app_name, 'tools-missing.zip')
4795+ checksum_app_name = 'hw-health-checksum-{}'.format(series)
4796+ checksum_app = model.applications[checksum_app_name]
4797+ await update_hwhealth_res(model, checksum_app_name, 'tools-missing.zip')
4798 for unit in model.units.values():
4799- if unit.entity_id.startswith(hw_health_checksum_app_name):
4800- hw_health_checksum_unit = unit
4801+ if unit.entity_id.startswith(checksum_app_name):
4802+ checksum_unit = unit
4803 break
4804
4805 await model.block_until(
4806- lambda: (hw_health_checksum_app.status == 'blocked' and
4807- hw_health_checksum_unit.workload_status_message == 'Tool megacli not found'),
4808- timeout=600
4809+ lambda: (
4810+ checksum_app.status == 'blocked' and checksum_unit.workload_status_message == 'Tool megacli not found' # noqa E501
4811+ ),
4812+ timeout=DEF_TIMEOUT
4813 )
4814
4815
4816 async def test_checksum_updated_resource_ok(deploy_app, model):
4817 series = deploy_app.name.split('-')[-1]
4818- hw_health_checksum_app_name = 'hw-health-checksum-{}'.format(series)
4819- hw_health_checksum_app = model.applications[hw_health_checksum_app_name]
4820- await update_hwhealth_res(model, hw_health_checksum_app_name, 'tools.zip')
4821+ checksum_app_name = 'hw-health-checksum-{}'.format(series)
4822+ checksum_app = model.applications[checksum_app_name]
4823+ await update_hwhealth_res(model, checksum_app_name, 'tools.zip')
4824 for unit in model.units.values():
4825- if unit.entity_id.startswith(hw_health_checksum_app_name):
4826- hw_health_checksum_unit = unit
4827+ if unit.entity_id.startswith(checksum_app_name):
4828+ checksum_unit = unit
4829 break
4830
4831 await model.block_until(
4832- lambda: (hw_health_checksum_app.status == 'active' and
4833- hw_health_checksum_unit.workload_status_message == 'ready'),
4834- timeout=600
4835+ lambda: (checksum_app.status == 'active' and # noqa:W504
4836+ checksum_unit.workload_status_message == 'ready'),
4837+ timeout=DEF_TIMEOUT
4838 )
4839
4840
4841-async def test_stats(toolset, deployed_unit, file_stat):
4842+async def test_deployed_file_stats(monkeypatch, toolset, deploy_app, deployed_unit, file_stat):
4843 # This should really be a parametrized test, but fixtures cannot be used as
4844 # params value as if they were iterators
4845 # It should also check for other installed files and differentiate between
4846 # tool types (e.g. tools.Ipmi does not use a vendor binary)
4847+ series = deploy_app.name.split('-')[-1]
4848 for tool in toolset:
4849+ # Skip tools that are out of series for the currently deployed application
4850+ with monkeypatch.context() as m:
4851+ m.setattr('hwhealth.tools.lsb_release',
4852+ lambda x=None: {'DISTRIB_CODENAME': series})
4853+ if not tool.is_series_supported():
4854+ print('Skipping tool {}. Distribution {} not supported.'.format(tool, series))
4855+ continue
4856 # Have we rendered the nrpe check cfg?
4857 nrpecfg_path = os.path.join(NRPECFG_DIR,
4858 'check_{}.cfg'.format(tool._shortname))
4859@@ -255,7 +294,7 @@ async def test_stats(toolset, deployed_unit, file_stat):
4860
4861 # Have we installed the nrpe check script?
4862 nrpescript_path = os.path.join(tool.NRPE_PLUGINS_DIR,
4863- tool._nrpe_script)
4864+ os.path.basename(tool._nrpe_script))
4865 print('Checking {}'.format(nrpescript_path))
4866 test_stat = await file_stat(nrpescript_path, deployed_unit)
4867 assert test_stat['size'] > 0
4868@@ -263,6 +302,17 @@ async def test_stats(toolset, deployed_unit, file_stat):
4869 assert test_stat['uid'] == tool.NRPE_PLUGINS_UID
4870 assert test_stat['mode'] == oct(tool.NRPE_PLUGINS_MODE)
4871
4872+ # Have we installed any common libs?
4873+ for lib in tool._common_libs:
4874+ lib_path = os.path.join(tool.NRPE_PLUGINS_DIR,
4875+ os.path.basename(lib))
4876+ print('Checking {}'.format(nrpescript_path))
4877+ test_stat = await file_stat(lib_path, deployed_unit)
4878+ assert test_stat['size'] > 0
4879+ assert test_stat['gid'] == tool.NRPE_PLUGINS_GID
4880+ assert test_stat['uid'] == tool.NRPE_PLUGINS_UID
4881+ assert test_stat['mode'] == oct(tool.NRPE_PLUGINS_MODE)
4882+
4883 if isinstance(tool, tools.Ipmi) or isinstance(tool, tools.Nvme):
4884 # Have we added sudo rights for running freeipmi commands?
4885 sudoer_path = os.path.join(tool.SUDOERS_DIR, tool._sudoer_file)
4886@@ -275,10 +325,10 @@ async def test_stats(toolset, deployed_unit, file_stat):
4887
4888 if isinstance(tool, tools.Ipmi) or isinstance(tool, tools.VendorTool):
4889 # Have we installed the cronjob script helper?
4890- cronjob_script_path = os.path.join(tool.NRPE_PLUGINS_DIR,
4891- tool._cronjob_script)
4892- print('Checking {}'.format(cronjob_script_path))
4893- test_stat = await file_stat(cronjob_script_path, deployed_unit)
4894+ cron_script_path = os.path.join(tool.NRPE_PLUGINS_DIR,
4895+ tool._cron_script)
4896+ print('Checking {}'.format(cron_script_path))
4897+ test_stat = await file_stat(cron_script_path, deployed_unit)
4898 assert test_stat['size'] > 0
4899 assert test_stat['gid'] == tool.CRONJOB_SCRIPT_GID
4900 assert test_stat['uid'] == tool.CRONJOB_SCRIPT_UID
4901@@ -305,14 +355,42 @@ async def test_stats(toolset, deployed_unit, file_stat):
4902 assert test_stat['mode'] == oct(tool.TOOLS_MODE)
4903
4904
4905-async def test_removal(toolset, model, deploy_app, file_stat):
4906+@pytest.mark.parametrize('script_type', ['_nrpe_script', '_cron_script'])
4907+async def test_imports(script_type, monkeypatch, toolset, deploy_app, deployed_unit, run_command):
4908+ '''Dry run all auxiliary files to ensure we have all needed dependecies'''
4909+ series = deploy_app.name.split('-')[-1]
4910+ for tool in toolset:
4911+ # Skip tools that are out of series for the currently deployed application
4912+ with monkeypatch.context() as m:
4913+ m.setattr('hwhealth.tools.lsb_release',
4914+ lambda x=None: {'DISTRIB_CODENAME': series})
4915+ if not tool.is_series_supported():
4916+ print('Skipping tool {}. Distribution {} not supported.'.format(tool, series))
4917+ continue
4918+ script_name = getattr(tool, script_type)
4919+ tool_name = tool.__class__.__name__
4920+ if not script_name:
4921+ # Cannot pytest.skip because it would break out of the loop
4922+ print('Skipping test as {} does not have a {}'
4923+ ''.format(tool_name, script_type))
4924+ else:
4925+ print('Checking {}: {}'.format(tool_name, script_name))
4926+ path = os.path.join(tool.NRPE_PLUGINS_DIR, script_name)
4927+ cmd = path + " --help"
4928+ results = await run_command(cmd, deployed_unit)
4929+ rc = results['Code']
4930+ assert rc == '0', ('{}, {}. RC is non-zero. results={}'
4931+ ''.format(tool_name, script_name, results))
4932+
4933+
4934+async def test_removal(monkeypatch, toolset, model, deploy_app, file_stat):
4935 '''Remove the unit, test that all files have been cleaned up'''
4936 hw_health_app_name = deploy_app.name
4937 series = deploy_app.name.split('-')[-1]
4938 await deploy_app.remove()
4939 await model.block_until(
4940 lambda: hw_health_app_name not in model.applications,
4941- timeout=600
4942+ timeout=DEF_TIMEOUT
4943 )
4944 # Since we've removed the hw-health app, we can't target it anymore, we
4945 # need to find the principal unit
4946@@ -320,6 +398,13 @@ async def test_removal(toolset, model, deploy_app, file_stat):
4947 if unit.entity_id.startswith('ubuntu-{}'.format(series)):
4948 ubuntu_unit = unit
4949 for tool in toolset:
4950+ # Skip tools that are out of series for the currently deployed application
4951+ with monkeypatch.context() as m:
4952+ m.setattr('hwhealth.tools.lsb_release',
4953+ lambda x=None: {'DISTRIB_CODENAME': series})
4954+ if not tool.is_series_supported():
4955+ print('Skipping tool {}. Distribution {} not supported.'.format(tool, series))
4956+ continue
4957 # Have we removed the nrpe check cfg?
4958 nrpecfg_path = os.path.join(NRPECFG_DIR,
4959 'check_{}.cfg'.format(tool._shortname))
4960@@ -344,7 +429,7 @@ async def test_removal(toolset, model, deploy_app, file_stat):
4961 if isinstance(tool, tools.Ipmi) or isinstance(tool, tools.VendorTool):
4962 # Have we removed the cronjob script helper?
4963 cronjob_path = os.path.join(tool.NRPE_PLUGINS_DIR,
4964- tool._cronjob_script)
4965+ tool._cron_script)
4966 print('Checking {}'.format(cronjob_path))
4967 with pytest.raises(RuntimeError):
4968 await file_stat(cronjob_path, ubuntu_unit)
4969@@ -356,7 +441,7 @@ async def test_removal(toolset, model, deploy_app, file_stat):
4970 with pytest.raises(RuntimeError):
4971 await file_stat(cronjob_path, ubuntu_unit)
4972
4973- if isinstance(tool, tools.VendorTool) and not isinstance(tool, tools.Mdadm):
4974+ if isinstance(tool, tools.VendorTool) and not isinstance(tool, tools.Mdadm): # noqa E501
4975 # /sbin/mdadm will not be removed, but the vendor binaries
4976 # should have been
4977 bin_path = os.path.join(tool.TOOLS_DIR, tool._shortname)
4978diff --git a/src/tests/hw-health-samples/cron_outputs/hplog.out.ok b/src/tests/hw-health-samples/cron_outputs/hplog.out.ok
4979new file mode 100644
4980index 0000000..3ff5bcb
4981--- /dev/null
4982+++ b/src/tests/hw-health-samples/cron_outputs/hplog.out.ok
4983@@ -0,0 +1 @@
4984+OK No errors found
4985\ No newline at end of file
4986diff --git a/src/tests/hw-health-samples/cron_outputs/ipmi_sensors.out.critical b/src/tests/hw-health-samples/cron_outputs/ipmi_sensors.out.critical
4987new file mode 100644
4988index 0000000..4359fa2
4989--- /dev/null
4990+++ b/src/tests/hw-health-samples/cron_outputs/ipmi_sensors.out.critical
4991@@ -0,0 +1 @@
4992+IPMI Status: Critical [16 system event log (SEL) entries present] | 'Current Power'=116 'Temp 1'=28.00;;~:42.00 'Temp 2 (CPU 1)'=40.00;;~:81.00 'Temp 3 (CPU 2)'=40.00;;~:81.00 'Temp 4 (MemD1)'=44.00;;~:87.00 'Temp 5 (MemD2)'=37.00;;~:87.00 'Temp 7 (IIOH)'=60.00;;~:105.00 'Temp 8 (PCIR)'=37.00;;~:85.00 'Temp 9 (PCIR)'=43.00;;~:85.00 'Temp 10 (PCIR)'=32.00;;~:70.00 'Temp 11 (PCIR)'=31.00;;~:65.00 'Temp 12 (PCIR)'=40.00;;~:75.00 'Temp 13 (PCIR)'=38.00;;~:87.00 'Temp 14 (PCIR)'=32.00;;~:65.00 'Temp 15 (IOH2)'=0.00;;~:105.00
4993diff --git a/src/tests/hw-health-samples/cron_outputs/ssacli.out.ok b/src/tests/hw-health-samples/cron_outputs/ssacli.out.ok
4994new file mode 100644
4995index 0000000..3ff5bcb
4996--- /dev/null
4997+++ b/src/tests/hw-health-samples/cron_outputs/ssacli.out.ok
4998@@ -0,0 +1 @@
4999+OK No errors found
5000\ No newline at end of file
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: