You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
At present the cylc get-host-metrics command used to pick one host from a group based on system activity uses Linux only commands to gather metrics which are fragile and not portable (e.g. to Darwin).
The amazing psutil library provides portable abstractions which provide what we need and much more.
Upgrade the cylc get-host-metrics command to allow users to specify "thresholds" as Python expressions using psutil functions e.g:
As a start point here is an approach which could be used:
importastfromioimportBytesIOimportpicklefromsubprocessimportPopen, PIPEfromtokenizeimporttokenizeimporttokenTHRESHOLD_STRING=''' virtual_memory().available > 123456789 getloadavg()[0] < 5 cpu_count() > 1 disk_usage('/').free > 123'''classSimpleVisitor(ast.NodeVisitor):
"""Abstract syntax tree node visitor for simple safe operations."""defvisit(self, node):
ifnotisinstance(node, self.whitelist):
# permit only whitelisted operationsraiseValueError(type(node))
returnsuper().visit(node)
whitelist= (
ast.Expression,
# variablesast.Name, ast.Load, ast.Attribute, ast.Subscript, ast.Index,
# opersast.BinOp, ast.operator,
# typesast.Num, ast.Str,
# comparisonsast.Compare, ast.cmpop, ast.List, ast.Tuple
)
defsimple_eval(expr, **variables):
"""Safely evaluates simple python expressions. Supports a minimal subset of Python operators: * Binary operations * Simple comparisons Supports a minimal subset of Python data types: * Numbers * Strings * Tuples * Lists Examples: >>> simple_eval('1 + 1') 2 >>> simple_eval('1 < a', a=2) True >>> simple_eval('1 in (1, 2, 3)') True >>> import psutil >>> simple_eval('a.available > 0', a=psutil.virtual_memory()) True If you try to get it to do something it's not supposed to: >>> simple_eval('open("foo")') Traceback (most recent call last): ValueError: open("foo") """try:
node=ast.parse(expr.strip(), mode='eval')
SimpleVisitor().visit(node)
returneval(
compile(node, '<string>', 'eval'),
{'__builtins__': None},
variables
)
exceptException:
raiseValueError(expr)
defget_thresholds(string):
"""Yield parsed threshold expressions. Examples: The first ``token.NAME`` encountered is returned as the query: >>> get_thresholds('foo() == 123').__next__() (('foo',), 'RESULT == 123') If multiple are present they will not get parsed: >>> get_thresholds('foo() in bar()').__next__() (('foo',), 'RESULT in bar()') Positional arguments are added to the query tuple: >>> get_thresholds('1 in foo("a")').__next__() (('foo', 'a'), '1 in RESULT') Yields: tuple - (query, expression) query (tuple): The method to call followed by any positional arguments. expression (str): The expression with the method call replaced by `RESULT` """forlineinstring.splitlines():
# parse the string one line at a time# purposfully don't support multi-line expressionsline=line.strip()
ifnotline:
# skip blank linescontinuequery= []
start=Nonein_args=Falseline_feed=BytesIO(line.encode())
foritemintokenize(line_feed.readline):
ifitem.type==token.ENCODING:
# encoding tag, not of interestpasselifnotquery:
# the first token.NAME has not yet been encounteredifitem.type==token.NAMEanditem.string!='in':
# this is the first token.NAME, assume it it the methodstart=item.start[1]
query.append(item.string)
elifitem.string=='(':
# positional arguments follow thisin_args=Trueelifitem.string==')':
# end of positional argumentsin_args=Falsebreakelifitem.string==',':
passelifin_args:
# literal eval each argumentquery.append(ast.literal_eval(item.string))
end=item.end[1]
yield (
tuple(query),
line[:start] +'RESULT'+line[end:]
)
defget_script(keys):
"""Return a Python script for obtaining the requested keys."""return'; '.join([
'import pickle',
'import psutil',
'print(pickle.dumps([%s]))'% (
', '.join((
f'getattr(psutil, "{key[0]}"){key[1:]}'forkeyinkeys
))
)
])
defrun(script):
"""Run the pprovided script un-pickling the result."""cmd= ['python', '-']
stdout, stderr=Popen(
cmd, stdout=PIPE, stdin=PIPE
).communicate(script.encode())
returnpickle.loads(ast.literal_eval(stdout.decode()))
defmain():
# get the threshold stringsstring=THRESHOLD_STRINGthresholds= [xforxinget_thresholds(string)]
# get a list of metrics we need to obtain from each hostkeys=list({xforx, _inthresholds})
# obtain these metricsscript=get_script(keys)
results=dict(zip(keys, run(script)))
# evaluate the thresholdsreturnall(
simple_eval(expression, RESULT=results[key])
forkey, expressioninthresholds
)
print(
main()
)
Caveats:
Calls python with a generated program rather than calling a cylc subcommand which is bad for whitelisting resolved.
Uses pickle for serialisation (psutil isn't great for serialisation) uses JSON.
Uses eval though in a restricted way.
Pull requests welcome!
The text was updated successfully, but these errors were encountered:
At present the
cylc get-host-metrics
command used to pick one host from a group based on system activity uses Linux only commands to gather metrics which are fragile and not portable (e.g. to Darwin).The amazing
psutil
library provides portable abstractions which provide what we need and much more.Upgrade the
cylc get-host-metrics
command to allow users to specify "thresholds" as Python expressions usingpsutil
functions e.g:As a start point here is an approach which could be used:
Caveats:
Calls python with a generated program rather than calling a cylc subcommand which is bad for whitelistingresolved.Uses pickle for serialisation (psutil isn't great for serialisation)uses JSON.Pull requests welcome!
The text was updated successfully, but these errors were encountered: