Created
April 19, 2020 21:19
-
-
Save davecoutts/f4bcfe500976440160a74f9216445b3b to your computer and use it in GitHub Desktop.
Python Folding@home FAHClient info data extractor. Useful for Folding job progress data collection into influxdb via the telegraf inputs.exec collector.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
DESCRIPTION = \ | |
''' | |
#-------------------------------------------------------------------------------------------------- | |
Python Folding@home FAHClient info data extractor. | |
#-------------------------------------------------------------------------------------------------- | |
''' | |
EPILOG = \ | |
''' | |
#-------------------------------------------------------------------------------------------------- | |
This script executes the 'FAHClient --send-command XYZ' command and converts the resulting PyON | |
output to a json-like string that is then converted into a python object with json.loads. | |
The script requires Python 3.5+ due the use of the subprocess 'run' function. | |
# USAGE EXAMPLES | |
Compare the outputs of, | |
FAHClient --send-command queue-info | |
python3 fahclientinfo.py --send_command queue-info --pretty_print | |
python3 fahclientinfo.py -pp -sc queue-info | |
FAHClient --send-command slot-info | |
"C:\Program Files (x86)\FAHClient\FAHClient.exe" --send-command slot-info | |
python3 fahclientinfo.py --send_command slot-info --pretty_print | |
python3 fahclientinfo.py --queue_info_subset --pretty_print | |
python3 fahclientinfo.py --queue_info_subset --queue_info_fields percentdone slot state -pp | |
python3 fahclientinfo.py --queue_info_subset --fahclient_path="/usr/local/bin/FAHClient" | |
# EXECUTABLE PATH | |
The path to the FAHClient executable is set to the default install location, per OS. | |
If the FAHClient executable is in a different location, use the --fahclient_path option. | |
e.g | |
fahclientinfo.py -qs -pp --fahclient_path="C:\Program Files (x86)\FAHClient\FAHClient.exe" | |
fahclientinfo.py -qs -pp --fahclient_path="C:\Program Files\FAHClient\FAHClient.exe" | |
fahclientinfo.py -qs -pp --fahclient_path /usr/local/bin/FAHClient | |
fahclientinfo.py -qs -pp --fahclient_path /usr/bin/FAHClient | |
# TELEGRAF | |
The script was originally written to be run by the influxdb telegraf inputs.exec collector. | |
That is why it writes json to standard out. | |
https://github.com/influxdata/telegraf/tree/master/plugins/inputs/exec | |
See example telegraf collector configuration snippet below. | |
# telegraf.conf | |
[[inputs.exec]] | |
commands = ["/usr/bin/python3 /home/auser/fahclientinfo.py -qs -qf percentdone ppd slot state"] | |
interval = "60s" | |
timeout = "10s" | |
data_format = "json" | |
tag_keys = ["slot", "state"] | |
name_override = "fah_queue" | |
#-------------------------------------------------------------------------------------------------- | |
''' | |
#-------------------------------------------------------------------------------------------------- | |
__author__ = 'Dave Coutts' | |
__license__ = 'Apache' | |
__version__ = '1.0.0' | |
__maintainer__ = 'https://github.com/davecoutts' | |
__status__ = 'Production' | |
#-------------------------------------------------------------------------------------------------- | |
import re | |
import sys | |
import json | |
import platform | |
from pathlib import Path | |
from operator import itemgetter | |
from subprocess import PIPE, run | |
#-------------------------------------------------------------------------------------------------- | |
# Default fields used by the 'queue_info_subset' function. | |
QUEUE_INFO_FIELDS = ( | |
'framesdone', | |
'percentdone', | |
'ppd', | |
'slot', | |
'state', | |
'totalframes' | |
) | |
#-------------------------------------------------------------------------------------------------- | |
# Set the path for the FAHClient executable to the common install path per operating system. | |
OS = platform.system() | |
if OS == 'Linux': | |
FAHCLIENT_PATH = Path(r'/usr/bin/FAHClient') | |
elif OS == 'Windows': | |
FAHCLIENT_PATH = Path(r'C:\Program Files (x86)\FAHClient\FAHClient.exe') | |
elif OS == 'Darwin': | |
FAHCLIENT_PATH = Path(r'/usr/local/bin/FAHClient') | |
else: | |
FAHCLIENT_PATH = Path('FAHClient') | |
#-------------------------------------------------------------------------------------------------- | |
def json_stdout(data, pretty=False): | |
'''Write json.dumps converted object to stdout.''' | |
if pretty: | |
sys.stdout.write(json.dumps(data, sort_keys=True, indent=4)) | |
else: | |
sys.stdout.write(json.dumps(data)) | |
#-------------------------------------------------------------------------------------------------- | |
def send_command(command, fahclient=FAHCLIENT_PATH): | |
''' | |
- Execute FAHClient --send-command XYZ command. | |
- Extract json-like string from FAHClient PyON output using regex. | |
- Return python object from json.loads conversion of json-like string. | |
''' | |
result = run([str(fahclient), '--send-command', command], stdout=PIPE, stderr=PIPE) | |
command_output = result.stdout.decode(encoding="utf-8") | |
# Regex assumes all FAHClient PyON output is a list or dict at the top level. | |
# Run 'FAHClient --send-command queue-info' to see example PyON output. | |
extract = re.search(r'.*PyON\s\d+\s\w+\r?\n([\[|\{].*[\]|\}])\r?\n---', command_output, re.DOTALL) | |
if extract is not None: | |
# Horrible hack to turn FAHClient's PyON text output into json-like text. | |
# At least for boolean and None types. | |
# See https://pypi.org/project/pon/ for info on PyON. | |
pyon_jsonified_text = extract.group(1)\ | |
.replace(': True', ': true')\ | |
.replace(': False', ': false')\ | |
.replace(': None', ': null') | |
return json.loads(pyon_jsonified_text) | |
else: | |
return [] | |
#-------------------------------------------------------------------------------------------------- | |
def queue_info_subset(queue_fields=QUEUE_INFO_FIELDS, fahclient=FAHCLIENT_PATH): | |
''' | |
- Collect data as dict of lists from 'FAHClient --send-command queue-info' command output. | |
- Exclude unwanted fields. | |
- Convert percentdone to float and ppd to int. | |
- Return python dict of lists ordered by slot field. | |
''' | |
queue_fields = set(queue_fields) | |
queue_fields.add('slot') # Ensure slot key is always included | |
queue_info_all = send_command('queue-info', fahclient=fahclient) | |
info_subset = [] | |
for slot in queue_info_all: | |
subset = {k:v for k,v in slot.items() if k in queue_fields} # Exclude unwanted fields. | |
if 'percentdone' in subset: | |
subset['percentdone'] = float(subset['percentdone'].replace('%', '')) | |
if 'ppd' in subset: | |
subset['ppd'] = int(subset['ppd']) | |
info_subset.append(subset) | |
return sorted(info_subset, key=itemgetter('slot')) | |
#-------------------------------------------------------------------------------------------------- | |
def main(): | |
import argparse | |
parser = argparse.ArgumentParser( | |
epilog=EPILOG, | |
description=DESCRIPTION, | |
formatter_class=argparse.RawTextHelpFormatter | |
) | |
parser.add_argument('-qs', '--queue_info_subset', | |
dest='queue_info_subset', | |
action="store_true", | |
default=False, | |
help='Write to stdout a subset of queue-info data in json format' | |
) | |
parser.add_argument('-qf', '--queue_info_fields', | |
dest='queue_info_fields', | |
action="store", | |
type=str, | |
nargs='*', | |
default=QUEUE_INFO_FIELDS, | |
help='Specify list of fields to be included in the queue-info subset output.' | |
) | |
parser.add_argument('-sc', '--send_command', | |
dest='send_command', | |
type=str, | |
default=None, | |
help='Execute user supplied command and write result in json format to stdout.' | |
) | |
parser.add_argument('-fp', '--fahclient_path', | |
dest='fahclient_path', | |
type=Path, | |
default=FAHCLIENT_PATH, | |
help='File system location of the FAHClient executable.' | |
) | |
parser.add_argument('-pp', '--pretty_print', | |
dest='pretty_print', | |
action="store_true", | |
default=False, | |
help='Print json to stdout in intended and key ordered format' | |
) | |
args = parser.parse_args() | |
if args.queue_info_subset: | |
queue_info = queue_info_subset(queue_fields=args.queue_info_fields, fahclient=args.fahclient_path) | |
json_stdout(queue_info, args.pretty_print) | |
elif args.send_command is not None: | |
sent_command_info = send_command(args.send_command, fahclient=args.fahclient_path) | |
json_stdout(sent_command_info, args.pretty_print) | |
#-------------------------------------------------------------------------------------------------- | |
if __name__ == '__main__': | |
main() | |
#-------------------------------------------------------------------------------------------------- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment