From fc3eb6f324d9cb5e8ebfe429c95d4f9857c2839b Mon Sep 17 00:00:00 2001 From: Jannik Schönartz Date: Tue, 22 Jun 2021 21:04:44 +0200 Subject: Split hardware collector and python in seperate modules and remove pip for now (maybe own module later) --- modules.d/bas-hw-collect/module-setup.sh | 109 +++++++++ .../bas-hw-collect/scripts/collect_hw_info_json.py | 260 +++++++++++++++++++++ modules.d/bas-hw-collect/scripts/dmiparser.py | 201 ++++++++++++++++ modules.d/bas-python/module-setup.sh | 45 ---- .../bas-python/scripts/collect_hw_info_json.py | 260 --------------------- modules.d/bas-registration-hooks/module-setup.sh | 3 +- modules.d/python/module-setup.sh | 18 ++ 7 files changed, 590 insertions(+), 306 deletions(-) create mode 100755 modules.d/bas-hw-collect/module-setup.sh create mode 100755 modules.d/bas-hw-collect/scripts/collect_hw_info_json.py create mode 100644 modules.d/bas-hw-collect/scripts/dmiparser.py delete mode 100755 modules.d/bas-python/module-setup.sh delete mode 100755 modules.d/bas-python/scripts/collect_hw_info_json.py create mode 100755 modules.d/python/module-setup.sh diff --git a/modules.d/bas-hw-collect/module-setup.sh b/modules.d/bas-hw-collect/module-setup.sh new file mode 100755 index 00000000..0ebccc07 --- /dev/null +++ b/modules.d/bas-hw-collect/module-setup.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash + +check() { + for bin in lshw lspci ip dmidecode; do + if ! hash "$bin" 2>&1 /dev/null; then + echo "Missing '$bin' please install it..." + return 1 + fi + done + return 255 +} +depends() { + # drm is needed for getting the edid data + echo slx-drm python +} +install() { + # Copy the python hardware collection script to /opt/bas/ + mkdir -p "$initdir/opt/bas" + cp -r "$moddir/scripts/collect_hw_info_json.py" "$initdir/opt/bas/" + cp -r "$moddir/scripts/dmiparser.py" "$initdir/opt/bas/" + + # Libs needed for python3 and dmiparser + mkdir -p "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/__future__.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/_collections_abc.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/_sitebuiltins.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/_weakrefset.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/abc.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/argparse.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/base64.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/bisect.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/calendar.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/codecs.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/collections" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/contextlib.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/copy.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/copyreg.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/datetime.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/email" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/encodings" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/enum.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/fnmatch.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/functools.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/genericpath.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/hashlib.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/heapq.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/hmac.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/http" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/importlib" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/io.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/json" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/keyword.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/linecache.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/locale.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/logging" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/mimetypes.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/operator.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/os.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/posixpath.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/queue.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/quopri.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/random.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/re.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/reprlib.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/selectors.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/shlex.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/shutil.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/signal.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/site.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/socket.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/sre_compile.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/sre_constants.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/sre_parse.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/stat.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/string.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/stringprep.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/struct.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/subprocess.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/threading.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/tempfile.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/token.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/tokenize.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/traceback.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/types.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/urllib" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/uu.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/warnings.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/weakref.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3.8/zipfile.py" "$initdir/usr/lib/python3.8" + + cp -r "/usr/lib/python3/dist-packages/certifi" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3/dist-packages/chardet" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3/dist-packages/idna" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3/dist-packages/requests" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3/dist-packages/six.py" "$initdir/usr/lib/python3.8" + cp -r "/usr/lib/python3/dist-packages/urllib3" "$initdir/usr/lib/python3.8" + + # Certificates + mkdir -p "$initdir/etc/ssl/certs/" + cp "/etc/ssl/certs/ca-certificates.crt" "$initdir/etc/ssl/certs/" + + inst_multiple lshw lspci smartctl ip dmidecode + + # Script get called via bash registration hook configured in the bas + # Exec the python hook and reboot instead of Switch Root + # inst_hook pre-mount 00 "$moddir/scripts/python_hook.sh" + + return 0 +} diff --git a/modules.d/bas-hw-collect/scripts/collect_hw_info_json.py b/modules.d/bas-hw-collect/scripts/collect_hw_info_json.py new file mode 100755 index 00000000..14e3877a --- /dev/null +++ b/modules.d/bas-hw-collect/scripts/collect_hw_info_json.py @@ -0,0 +1,260 @@ +from dmiparser import DmiParser +from subprocess import PIPE +from os import listdir +import argparse +import json +import requests +import shlex +import subprocess +import sys + +__debug = False + +# Run dmi command as subprocess and get the stdout +def run_subprocess(_cmd): + global __debug + proc = subprocess.run(_cmd, shell=True, stdout=PIPE, stderr=PIPE) + stdout = proc.stdout + stderr = proc.stderr + + if __debug: + eprint(_cmd + ':') + eprint() + eprint('Errors:') + eprint(stderr.decode()) + eprint() + # stderr len instead of proc.returncode > 0 is used because some have returncode 2 but are still valid + if len(stderr.decode()) > 0: + eprint(_cmd + ' Errors:') + eprint(stderr.decode()) + if proc.returncode != 0: + eprint('Error Return Code: ' + str(proc.returncode)) + eprint() + return False + else: + return stdout.decode() + else: + return stdout.decode() + +# Get and parse dmidecode using dmiparser +def get_dmidecode(): + _dmiraw = run_subprocess('dmidecode') + _dmiparsed = '' + if _dmiraw: + # Parse dmidecode + _dmiparsed = DmiParser(_dmiraw) + return json.loads(str(_dmiparsed)) + else: + return [] + +# Get the readlink -f output +def get_readlink(link): + _readlink = run_subprocess('readlink -f ' + link) + return _readlink + +# Get smartctl output in json format +def get_smartctl(): + diskdir = '/dev/disk/by-path/' + # Get and filter all disks + disks = listdir(diskdir) + filteredDisks = [i for i in disks if (not "-part" in i) and (not "-usb-" in i)] + smartctl = {} + for d in filteredDisks: + output = run_subprocess('smartctl -x --json ' + diskdir + d) + if isinstance(output, str): + try: + disk = json.loads(output) + disk['readlink'] = get_readlink(diskdir + d).rstrip() + smartctl[d] = disk + except ValueError as e: + eprint('smartctl failed with error:') + eprint(e) + eprint() + return [] + return smartctl + +# Get and process "lspci -mn" output +def get_lspci(): + lspci = [] + lspci_raw = run_subprocess('lspci -mmn').split('\n') + for line in lspci_raw: + if len(line) <= 0: continue + + # Parsing shell like command parameters + parse = shlex.split(line) + lspci_parsed = {} + arguments = [] + values = [] + for parameter in parse: + # Split values from arguments + if parameter.startswith('-'): + arguments.append(parameter) + else: + values.append(parameter) + + # Prepare values positions are in order + if len(values) >= 6: + lspci_parsed['slot'] = values[0] + + # The busybox version of lspci has "Class " instead of "" + lspci_parsed['class'] = values[1].replace("Class ", "") + lspci_parsed['vendor'] = values[2] + lspci_parsed['device'] = values[3] + lspci_parsed['subsystem_vendor'] = values[4] + lspci_parsed['subsystem'] = values[5] + + # Prepare arguments + if len(arguments) > 0: + for arg in arguments: + if arg.startswith('-p'): + lspci_parsed['progif'] = arg[2:] + elif arg.startswith('-r'): + lspci_parsed['rev'] = arg[2:] + else: continue + + lspci.append(lspci_parsed) + return lspci + +# Get ip data in json format +def get_ip(): + result = [] + ip_raw = run_subprocess('ip --json addr show') + if isinstance(ip_raw, str): + result = json.loads(ip_raw) + return result + +def get_net_fallback(): + result = {} + + # Get MAC address + interfaces = run_subprocess('ls /sys/class/net').split('\n') + for interface in interfaces: + # Skip local stuff + if interface == 'lo' or interface == '': + continue + net = {} + net['mac'] = run_subprocess('cat /sys/class/net/' + interface + '/address') + if net['mac'].endswith('\n'): + net['mac'] = net['mac'][:-1] + result[interface] = net + + # Get IP address + interfaces = run_subprocess('ip -o addr show | awk \'/inet/ {print $2, $3, $4}\'').split('\n') + for interface in interfaces: + if interface == '': + continue + interf = interface.split(' ') + if interf[0] == 'lo': + continue + else: + if interf[1] == 'inet': + result[interf[0]]['ipv4'] = interf[2] + elif interf[1] == 'inet6': + result[interf[0]]['ipv6'] = interf[2] + return result + +# Get and convert EDID data to hex +def get_edid(): + edid = [] + display_paths = run_subprocess('ls /sys/class/drm/*/edid') + if display_paths: + display_paths = display_paths.split('\n') + else: + return edid + for dp in display_paths: + if dp == '': continue + edid_hex = open(dp, 'rb').read().hex() + if len(edid_hex) > 0: + edid.append({ 'path': dp, 'edid': edid_hex }) + return edid + +def get_lshw(): + result = [] + lshw_raw = run_subprocess('lshw -json') + if isinstance(lshw_raw, str): + result = json.loads(lshw_raw) + return result + +def prepare_location(parent, bay, slot): + location = {} + if parent: + location['parent'] = parent + if bay: + location['bay'] = int(bay) + if slot: + location['slot'] = int(slot) + return location + +def prepare_contacts(contact_list): + contacts = [] + if contact_list == None: + return contacts + print(str(contact_list)) + for contact in contact_list: + contacts.append({ + 'firstname': contact[0], + 'lastname': contact[1], + }) + return contacts + +def send_post(url, payload): + # headers = { 'Content-type': 'application/json', 'Accept': 'text/plain' } + # req = requests.post(url, json=payload, headers=headers) + req = requests.post(url, json=payload) + # Print the response + print("POST-Request Response: \n") + print(req.text) + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + +def main(): + global __debug + + # Create and parse arguments + parser = argparse.ArgumentParser(description='Collects hardware data from different tools and returns it as json.') + parser.add_argument('-d', '--debug', action='store_true', help='Prints all STDERR messages. (Non critical included)') + parser.add_argument('-u', '--url', action='append', help='[multiple] If given, a post request with the generated JSON is sent to the given URLs') + parser.add_argument('-p', '--print', action='store_true', help='Prints the generated JSON') + parser.add_argument('-l', '--location', action='store', help=': Room-/Rackname where the client is located') + parser.add_argument('-s', '--slot', action='store', help=' The slot number (int) where the client is located in the rack') + parser.add_argument('-b', '--bay', action='store', help=' The bay number (int) where the client is located in the slot (segment)') + parser.add_argument('-c', '--contact', action='append', help='[multiple] : of the person responsible for this machine', nargs=2) + parser.add_argument('-n', '--name', action='store', help=': Name of the client') + parser.add_argument('-S', '--SERVER', action='store_true', help='Defines the type of the client to be a server') + args = parser.parse_args() + + if args.debug: + __debug = True + + # Run the tools + _collecthw = {} + _collecthw['version'] = 2.0 + _collecthw['dmidecode'] = get_dmidecode() + _collecthw['smartctl'] = get_smartctl() + _collecthw['lspci'] = get_lspci() + _collecthw['ip'] = get_ip() + _collecthw['edid'] = get_edid() + _collecthw['lshw'] = get_lshw() + _collecthw['net'] = get_net_fallback() + _collecthw['location'] = prepare_location(args.location, args.bay, args.slot) + _collecthw['contacts'] = prepare_contacts(args.contact) + + if args.name: + _collecthw['name'] = args.name + + if args.SERVER: + _collecthw['type'] = 'SERVER' + + collecthw_json = json.dumps(_collecthw) + if args.url: + for url in args.url: + send_post(url, _collecthw) + + # Print out the final json + if args.print: + print(json.dumps(_collecthw)) + +if __name__ == "__main__": + main() + diff --git a/modules.d/bas-hw-collect/scripts/dmiparser.py b/modules.d/bas-hw-collect/scripts/dmiparser.py new file mode 100644 index 00000000..9e3a0bfa --- /dev/null +++ b/modules.d/bas-hw-collect/scripts/dmiparser.py @@ -0,0 +1,201 @@ +import re +import json +from itertools import takewhile +from enum import Enum + +__all__ = ['DmiParser'] + +DmiParserState = Enum ( + 'DmiParserState', + ( + 'GET_SECT', + 'GET_PROP', + 'GET_PROP_ITEM', + ) +) + +class DmiParserSectionHandle(object): + '''A handle looks like this + + Handle 0x0066, DMI type 148, 48 bytes + ''' + def __init__(self): + self.id= '' + self.type = '' + self.bytes = 0 + + def __str__(self): + return json.dumps(self.__dict__) + +class DmiParserSectionProp(object): + '''A property looks like this + + Characteristics: + 3.3 V is provided + PME signal is supported + SMBus signal is supported + ''' + def __init__(self, value:str): + self.values = [] + + if value: + self.append(value) + + def __str__(self): + return json.dumps(self.__dict__) + + def append(self, item:str): + self.values.append(item) + +class DmiParserSection(object): + '''A section looks like this + + On Board Device 1 Information + Type: Video + Status: Enabled + Description: ServerEngines Pilot III + ''' + def __init__(self): + self.handle = None + self.name = '' + self.props = {} + + def __str__(self): + return json.dumps(self.__dict__) + + def append(self, key:str, prop:str): + self.props[key] = prop + +class DmiParser(object): + '''This parse dmidecode output to JSON + ''' + + def __init__(self, text:str, **kwargs): + ''' + text: output of command dmidecode + kwargs: these will pass to json.dumps + ''' + self._text = text + self._kwargs = kwargs + self._indentLv = lambda l: len(list(takewhile(lambda c: "\t" == c, l))) + self._sections = [] + + if type(text) is not str: + raise TypeError("%s want a %s but got %s" %( + self.__class__, type(__name__), type(text))) + + self._parse(text) + + def __str__(self): + return json.dumps(self._sections, **self._kwargs) + + def _parse(self, text:str): + lines = self._text.splitlines() + rhandle = r'^Handle\s(.+?),\sDMI\stype\s(\d+?),\s(\d+?)\sbytes$' + section = None + prop = None + state = None + k, v = None, None + + for i, l in enumerate(lines): + if i == len(lines) - 1 or DmiParserState.GET_SECT == state: + # Add previous section if exist + if section: + # Add previous prop if exist + if prop: + section.append(k, json.loads(str(prop))) + prop = None + + self._sections.append(json.loads(str(section))) + section = None + + if not l: + continue + + if l.startswith('Handle'): + state = DmiParserState.GET_SECT + handle = DmiParserSectionHandle() + match = re.match(rhandle, l) + handle.id, handle.type, handle.bytes = match.groups() + continue + + if DmiParserState.GET_SECT == state: + section = DmiParserSection() + section.handle = json.loads(str(handle)) + section.name = l + state = DmiParserState.GET_PROP + continue + + if DmiParserState.GET_PROP == state: + k, v = [x.strip() for x in l.split(':', 1)] + prop = DmiParserSectionProp(v) + lv = self._indentLv(l) - self._indentLv(lines[i+1]) + + if v: + if not lv: + section.append(k, json.loads(str(prop))) + prop = None + elif -1 == lv: + state = DmiParserState.GET_PROP_ITEM + continue + else: + if -1 == lv: + state = DmiParserState.GET_PROP_ITEM + continue + + # Next section for this handle + if not self._indentLv(lines[i+1]): + state = DmiParserState.GET_SECT + + if DmiParserState.GET_PROP_ITEM == state: + prop.append(l.strip()) + + lv = self._indentLv(l) - self._indentLv(lines[i+1]) + + if lv: + section.append(k, json.loads(str(prop))) + prop = None + + if lv > 1: + state = DmiParserState.GET_SECT + else: + state = DmiParserState.GET_PROP + +if '__main__' == __name__: + text='''# dmidecode 3.0 +Getting SMBIOS data from sysfs. +SMBIOS 2.7 present. + +Handle 0x0003, DMI type 2, 17 bytes +Base Board Information + Manufacturer: Intel Corporation + Product Name: S2600WT2R + Version: H21573-372 + Serial Number: BQWL81150522 + Asset Tag: Base Board Asset Tag + Features: + Board is a hosting board + Board is replaceable + Location In Chassis: Part Component + Chassis Handle: 0x0000 + Type: Motherboard + Contained Object Handles: 0 + + ''' + + # just print + parser = DmiParser(text) + #parser = DmiParser(text, sort_keys=True, indent=2) + print("parser is %s" %(type(parser))) + print(parser) + + # if you want a string + dmistr = str(parser) + print("dmistr is %s" %(type(dmistr))) + print(dmistr) + + # if you want a data structure + dmidata = json.loads(str(parser)) + print("dmidata is %s" %(type(dmidata))) + print(dmidata) + diff --git a/modules.d/bas-python/module-setup.sh b/modules.d/bas-python/module-setup.sh deleted file mode 100755 index 83f32df9..00000000 --- a/modules.d/bas-python/module-setup.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -check() { - for bin in python3 pip3 lshw lspci ip dmidecode; do - if ! hash "$bin" 2>&1 /dev/null; then - echo "Missing '$bin' please install it..." - return 1 - fi - done - return 255 -} -depends() { - # drm is needed for getting the edid data - echo slx-drm -} -install() { - # Copy the python hardware collection script to /opt/bas/ - mkdir -p "$initdir/opt/bas" - cp -r "$moddir/scripts/collect_hw_info_json.py" "$initdir/opt/bas/" - - # For testing the scripts manual, copy em all - # cp -r "$moddir/scripts" "$initdir/opt/bas/" - - # Libs needed for python3 and dmiparser - mkdir -p "$initdir/usr/lib" - cp -r "/usr/lib/python3" "$initdir/usr/lib/" - cp -r "/usr/lib/python3.8" "$initdir/usr/lib/" - - mkdir -p "$initdir/usr/local/lib" - cp -r "/usr/local/lib/python3.8" "$initdir/usr/local/lib/" - - mkdir -p "$initdir/usr/share" - cp -r "/usr/share/python-wheels" "$initdir/usr/share/python-wheels/" - - # Certificates - mkdir -p "$initdir/etc/ssl/certs/" - cp "/etc/ssl/certs/ca-certificates.crt" "$initdir/etc/ssl/certs/" - - inst_multiple python3 pip3 lshw lspci smartctl ip dmidecode - - # Exec the python hook and reboot instead of Switch Root - # inst_hook pre-mount 00 "$moddir/scripts/python_hook.sh" - - return 0 -} diff --git a/modules.d/bas-python/scripts/collect_hw_info_json.py b/modules.d/bas-python/scripts/collect_hw_info_json.py deleted file mode 100755 index 14e3877a..00000000 --- a/modules.d/bas-python/scripts/collect_hw_info_json.py +++ /dev/null @@ -1,260 +0,0 @@ -from dmiparser import DmiParser -from subprocess import PIPE -from os import listdir -import argparse -import json -import requests -import shlex -import subprocess -import sys - -__debug = False - -# Run dmi command as subprocess and get the stdout -def run_subprocess(_cmd): - global __debug - proc = subprocess.run(_cmd, shell=True, stdout=PIPE, stderr=PIPE) - stdout = proc.stdout - stderr = proc.stderr - - if __debug: - eprint(_cmd + ':') - eprint() - eprint('Errors:') - eprint(stderr.decode()) - eprint() - # stderr len instead of proc.returncode > 0 is used because some have returncode 2 but are still valid - if len(stderr.decode()) > 0: - eprint(_cmd + ' Errors:') - eprint(stderr.decode()) - if proc.returncode != 0: - eprint('Error Return Code: ' + str(proc.returncode)) - eprint() - return False - else: - return stdout.decode() - else: - return stdout.decode() - -# Get and parse dmidecode using dmiparser -def get_dmidecode(): - _dmiraw = run_subprocess('dmidecode') - _dmiparsed = '' - if _dmiraw: - # Parse dmidecode - _dmiparsed = DmiParser(_dmiraw) - return json.loads(str(_dmiparsed)) - else: - return [] - -# Get the readlink -f output -def get_readlink(link): - _readlink = run_subprocess('readlink -f ' + link) - return _readlink - -# Get smartctl output in json format -def get_smartctl(): - diskdir = '/dev/disk/by-path/' - # Get and filter all disks - disks = listdir(diskdir) - filteredDisks = [i for i in disks if (not "-part" in i) and (not "-usb-" in i)] - smartctl = {} - for d in filteredDisks: - output = run_subprocess('smartctl -x --json ' + diskdir + d) - if isinstance(output, str): - try: - disk = json.loads(output) - disk['readlink'] = get_readlink(diskdir + d).rstrip() - smartctl[d] = disk - except ValueError as e: - eprint('smartctl failed with error:') - eprint(e) - eprint() - return [] - return smartctl - -# Get and process "lspci -mn" output -def get_lspci(): - lspci = [] - lspci_raw = run_subprocess('lspci -mmn').split('\n') - for line in lspci_raw: - if len(line) <= 0: continue - - # Parsing shell like command parameters - parse = shlex.split(line) - lspci_parsed = {} - arguments = [] - values = [] - for parameter in parse: - # Split values from arguments - if parameter.startswith('-'): - arguments.append(parameter) - else: - values.append(parameter) - - # Prepare values positions are in order - if len(values) >= 6: - lspci_parsed['slot'] = values[0] - - # The busybox version of lspci has "Class " instead of "" - lspci_parsed['class'] = values[1].replace("Class ", "") - lspci_parsed['vendor'] = values[2] - lspci_parsed['device'] = values[3] - lspci_parsed['subsystem_vendor'] = values[4] - lspci_parsed['subsystem'] = values[5] - - # Prepare arguments - if len(arguments) > 0: - for arg in arguments: - if arg.startswith('-p'): - lspci_parsed['progif'] = arg[2:] - elif arg.startswith('-r'): - lspci_parsed['rev'] = arg[2:] - else: continue - - lspci.append(lspci_parsed) - return lspci - -# Get ip data in json format -def get_ip(): - result = [] - ip_raw = run_subprocess('ip --json addr show') - if isinstance(ip_raw, str): - result = json.loads(ip_raw) - return result - -def get_net_fallback(): - result = {} - - # Get MAC address - interfaces = run_subprocess('ls /sys/class/net').split('\n') - for interface in interfaces: - # Skip local stuff - if interface == 'lo' or interface == '': - continue - net = {} - net['mac'] = run_subprocess('cat /sys/class/net/' + interface + '/address') - if net['mac'].endswith('\n'): - net['mac'] = net['mac'][:-1] - result[interface] = net - - # Get IP address - interfaces = run_subprocess('ip -o addr show | awk \'/inet/ {print $2, $3, $4}\'').split('\n') - for interface in interfaces: - if interface == '': - continue - interf = interface.split(' ') - if interf[0] == 'lo': - continue - else: - if interf[1] == 'inet': - result[interf[0]]['ipv4'] = interf[2] - elif interf[1] == 'inet6': - result[interf[0]]['ipv6'] = interf[2] - return result - -# Get and convert EDID data to hex -def get_edid(): - edid = [] - display_paths = run_subprocess('ls /sys/class/drm/*/edid') - if display_paths: - display_paths = display_paths.split('\n') - else: - return edid - for dp in display_paths: - if dp == '': continue - edid_hex = open(dp, 'rb').read().hex() - if len(edid_hex) > 0: - edid.append({ 'path': dp, 'edid': edid_hex }) - return edid - -def get_lshw(): - result = [] - lshw_raw = run_subprocess('lshw -json') - if isinstance(lshw_raw, str): - result = json.loads(lshw_raw) - return result - -def prepare_location(parent, bay, slot): - location = {} - if parent: - location['parent'] = parent - if bay: - location['bay'] = int(bay) - if slot: - location['slot'] = int(slot) - return location - -def prepare_contacts(contact_list): - contacts = [] - if contact_list == None: - return contacts - print(str(contact_list)) - for contact in contact_list: - contacts.append({ - 'firstname': contact[0], - 'lastname': contact[1], - }) - return contacts - -def send_post(url, payload): - # headers = { 'Content-type': 'application/json', 'Accept': 'text/plain' } - # req = requests.post(url, json=payload, headers=headers) - req = requests.post(url, json=payload) - # Print the response - print("POST-Request Response: \n") - print(req.text) - -def eprint(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) - -def main(): - global __debug - - # Create and parse arguments - parser = argparse.ArgumentParser(description='Collects hardware data from different tools and returns it as json.') - parser.add_argument('-d', '--debug', action='store_true', help='Prints all STDERR messages. (Non critical included)') - parser.add_argument('-u', '--url', action='append', help='[multiple] If given, a post request with the generated JSON is sent to the given URLs') - parser.add_argument('-p', '--print', action='store_true', help='Prints the generated JSON') - parser.add_argument('-l', '--location', action='store', help=': Room-/Rackname where the client is located') - parser.add_argument('-s', '--slot', action='store', help=' The slot number (int) where the client is located in the rack') - parser.add_argument('-b', '--bay', action='store', help=' The bay number (int) where the client is located in the slot (segment)') - parser.add_argument('-c', '--contact', action='append', help='[multiple] : of the person responsible for this machine', nargs=2) - parser.add_argument('-n', '--name', action='store', help=': Name of the client') - parser.add_argument('-S', '--SERVER', action='store_true', help='Defines the type of the client to be a server') - args = parser.parse_args() - - if args.debug: - __debug = True - - # Run the tools - _collecthw = {} - _collecthw['version'] = 2.0 - _collecthw['dmidecode'] = get_dmidecode() - _collecthw['smartctl'] = get_smartctl() - _collecthw['lspci'] = get_lspci() - _collecthw['ip'] = get_ip() - _collecthw['edid'] = get_edid() - _collecthw['lshw'] = get_lshw() - _collecthw['net'] = get_net_fallback() - _collecthw['location'] = prepare_location(args.location, args.bay, args.slot) - _collecthw['contacts'] = prepare_contacts(args.contact) - - if args.name: - _collecthw['name'] = args.name - - if args.SERVER: - _collecthw['type'] = 'SERVER' - - collecthw_json = json.dumps(_collecthw) - if args.url: - for url in args.url: - send_post(url, _collecthw) - - # Print out the final json - if args.print: - print(json.dumps(_collecthw)) - -if __name__ == "__main__": - main() - diff --git a/modules.d/bas-registration-hooks/module-setup.sh b/modules.d/bas-registration-hooks/module-setup.sh index a916a4b6..e5d41424 100755 --- a/modules.d/bas-registration-hooks/module-setup.sh +++ b/modules.d/bas-registration-hooks/module-setup.sh @@ -10,7 +10,8 @@ check() { return 255 } depends() { - echo bas-python + # echo bas-python + echo bas-hw-collect } install() { # pre-mount is the first hook with guaranteed network access diff --git a/modules.d/python/module-setup.sh b/modules.d/python/module-setup.sh new file mode 100755 index 00000000..05a811a7 --- /dev/null +++ b/modules.d/python/module-setup.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +check() { + for bin in python3; do + if ! hash "$bin" 2>&1 /dev/null; then + echo "Missing '$bin' please install it..." + return 1 + fi + done + return 255 +} +depends() { + echo "" +} +install() { + inst_multiple python3 + return 0 +} -- cgit v1.2.3-55-g7522