Skip to content

Instantly share code, notes, and snippets.

@davecoutts
Created April 19, 2020 21:19
Show Gist options
  • Save davecoutts/f4bcfe500976440160a74f9216445b3b to your computer and use it in GitHub Desktop.
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.
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