Files
DetectionLab/Vagrant/resources/malcolm/shared/bin/sensorcommon.py
2021-08-06 10:35:01 +02:00

244 lines
8.3 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved.
import argparse
import ipaddress
import json
import os
import socket
import ssl
import subprocess
import sys
import urllib.request
from base64 import b64encode
from bs4 import BeautifulSoup
from bs4.element import Comment
from contextlib import closing
from http.client import HTTPSConnection, HTTPConnection
from multiprocessing import RawValue
from threading import Lock
NIC_BLINK_SECONDS = 10
###################################################################################################
class CancelledError(Exception):
"""Raised when user cancels the operation"""
pass
###################################################################################################
class Iface:
def __init__(self, name, description):
self.name = name
self.description = description
###################################################################################################
# clear the terminal window and exit the script
def clearquit():
os.system('clear')
sys.exit(0)
###################################################################################################
# print to stderr
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
###################################################################################################
# urlencode each character of a string
def aggressive_url_encode(string):
return "".join("%{0:0>2}".format(format(ord(char), "x")) for char in string)
###################################################################################################
# strip a prefix from the beginning of a string if needed
def remove_prefix(text, prefix):
if (len(prefix) > 0) and text.startswith(prefix):
return text[len(prefix):]
else:
return text
###################################################################################################
# nice human-readable file sizes
def sizeof_fmt(num, suffix='B'):
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
###################################################################################################
# convenient boolean argument parsing
def str2bool(v):
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
###################################################################################################
# will it float?
def isfloat(value):
try:
float(value)
return True
except ValueError:
return False
###################################################################################################
# check a string or list to see if something is a valid IP address
def isipaddress(value):
result = True
try:
if isinstance(value, list) or isinstance(value, tuple) or isinstance(value, set):
for v in value:
ip = ipaddress.ip_address(v)
else:
ip = ipaddress.ip_address(value)
except:
result = False
return result
###################################################################################################
# execute a shell process returning its exit code and output
def run_process(command, stdout=True, stderr=False, stdin=None, timeout=60):
retcode = -1
output = []
p = subprocess.run([command], input=stdin, universal_newlines=True, capture_output=True, shell=True, timeout=timeout)
if p:
retcode = p.returncode
if stderr and p.stderr:
output.extend(p.stderr.splitlines())
if stdout and p.stdout:
output.extend(p.stdout.splitlines())
return retcode, output
def tag_visible(element):
if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
return False
if isinstance(element, Comment):
return False
return True
def text_from_html(body):
soup = BeautifulSoup(body, 'html.parser')
texts = soup.findAll(text=True)
visible_texts = filter(tag_visible, texts)
return u" ".join(t.strip() for t in visible_texts).splitlines()
###################################################################################################
# test a connection to an HTTP/HTTPS server
def test_connection(protocol="http", host="127.0.0.1", port=80, uri="", username=None, password=None, ssl_verify="full", user_agent="hedgehog"):
status = 400
message = "Connection error"
output = []
if protocol.lower() == "https":
if ssl_verify.lower() == "full":
c = HTTPSConnection(host, port=port)
else:
c = HTTPSConnection(host, port=port, context=ssl._create_unverified_context())
elif protocol.lower() == "http":
c = HTTPConnection(host)
else:
c = None
if c:
try:
if username and password:
c.request('GET', f'/{uri}', headers={ 'User-agent': user_agent, 'Authorization' : 'Basic %s' % b64encode(f"{username}:{password}".encode()).decode("ascii") })
else:
c.request('GET', f'/{uri}', headers={ 'User-agent': user_agent })
res = c.getresponse()
status = res.status
message = res.reason
output = text_from_html(res.read())
except Exception as e:
if len(output) == 0:
output = ["Error: {}".format(e)]
return status, message, output
###################################################################################################
# test if a remote port is open
def check_socket(host, port):
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
sock.settimeout(10)
if sock.connect_ex((host, port)) == 0:
return True
else:
return False
###################################################################################################
# determine a list of available (non-virtual) adapters (Iface's)
def get_available_adapters():
available_adapters = []
_, all_iface_list = run_process("find /sys/class/net/ -mindepth 1 -maxdepth 1 -type l -printf '%P %l\\n'")
available_iface_list = [x.split(" ", 1)[0] for x in all_iface_list if 'virtual' not in x]
# for each adapter, determine its MAC address and link speed
for adapter in available_iface_list:
mac_address = '??:??:??:??:??:??'
speed = '?'
try:
with open(f"/sys/class/net/{adapter}/address", 'r') as f:
mac_address = f.readline().strip()
except:
pass
try:
with open(f"/sys/class/net/{adapter}/speed", 'r') as f:
speed = f.readline().strip()
except:
pass
description = f"{mac_address} ({speed} Mbits/sec)"
iface = Iface(adapter, description)
available_adapters.append(iface)
return available_adapters
###################################################################################################
# identify the specified adapter using ethtool --identify
def identify_adapter(adapter, duration=NIC_BLINK_SECONDS, background=False):
if background:
subprocess.Popen(["/sbin/ethtool", "--identify", adapter, str(duration)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
retCode, _ = run_process(f"/sbin/ethtool --identify {adapter} {duration}", stdout=False, stderr=False, timeout=duration*2)
return (retCode == 0)
###################################################################################################
# client that writes to the local instance of protologbeat listening on the configured host/port/protocol
class HeatBeatLogger:
def __init__(self, host='127.0.0.1', port=9515, proto='udp', format='plain', debug=False):
self.host = host
self.port = port
if proto == 'udp':
self.proto = 'udp'
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
else:
self.proto = 'tcp'
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.format = format
if self.format not in ['plain','json']:
self.format = 'plain'
self.debug = debug
if self.debug:
print("Creating instance of logger via {} on {}:{}".format(self.proto, self.host, self.port))
def enable_debug(self):
self.debug = True
def send_message(self, msg):
if self.format == 'json':
payload = json.dumps(msg)
else:
payload = msg
if self.debug:
print("Sending message: {}".format(payload.encode('utf-8')))
self.socket.sendto(payload.encode('utf-8'), (self.host, self.port))