-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP]: Support for command-line plugins #6691
base: master
Are you sure you want to change the base?
Conversation
Problem: There's no way to extend job submission cli commands in Flux via plugins. Add a prototype CLIPlugin class and CLIPluginRegistry (which loads CLIPlugins).
Problem: Flux submission cli commands don't support plugins. Load any plugins in the default plugin path "{confdir}/cli/plugins/*.py" and call any plugin `add_options()` and `modify_jobspec()` callbacks at appropriate points.
Problem: flux.job.Jobspec.validate_jobspec() throws away the parsed jobspec object if validation is successful. However, it could be useful to return the resulting jobspec object. Return a tuple of (result, jobspec) from validate_jobspec().
Problem: Job submission cli plugins offer a validate() callback, but this callback is not used. Load plugins and run all validate callbacks in the jobspec validator plugin.
Problem: the CLI plugin interface has no testing. Add some.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a quick pass this evening. I haven't tested yet nor looked into why the validator isn't working. I'm not sure if I tested that in the previous prototype.
def __init__(self, prog): | ||
self.opt = None | ||
self.usage = None | ||
self.help = None | ||
self.version = None | ||
self.prog = prog |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be cleaner if the standard attributes were passed in in the initializer, preferably with the name first (I'd argue for a plugin "name" vs "opt" if we're going with -P name=val
. Drop the help
attribute for now, I'm wondering if we can get that from the class.__doc__
in the future, so let's leave it as a future enhancement.
def __init__(self, name, prog, usage, version=None):
Then plugins initialize themselves with super().__init__("foo", prog, usage="Enable foo")
def get_plugin_name(self): | ||
"""Return the KEY to invoke the plugin""" | ||
return self.opt | ||
|
||
def get_help_message(self): | ||
"""Return the message to print with --help""" | ||
return self.help | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since name
and usage
will be required attributes of the class, I'd drop these getters. Callers can just use plugin.name
and plugin.usage
|
||
|
||
class CLIPluginRegistry: | ||
"""Flux CLI plugin registry helper class""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to drop "helper" here when this docstring gets expanded. This isn't just a helper class but is the plugin registry class.
"--plugin", | ||
type=CLIPluginValue, | ||
action="append", | ||
help=f"{self.get_plugin_help_messages()}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you've noticed, this isn't going to display well in --help
output.
Let's add a function to organize all the current plugin names and short usage messages into a nice table, then emit that in a custom section of the help output for the commands, e.g.:
Options provided by plugins:
foo=VAL Set option foo to VAL.
bar[=VAL] Set a bar with optional VAL.
Also, the add_argument()
should not go in this class if we're treating them this way. Instead it just be with all the other add_argument()
calls in the cli base.py.
|
||
def get_plugin_help_messages(self): | ||
"""Return a string that has all self.plugin help messages""" | ||
## TODO: Utilize the HelpFormatter class in argparse to make | ||
## these messages better | ||
help = "" | ||
for plugin in self.plugins[1:]: | ||
help += str(plugin.get_plugin_name()) | ||
help += str(plugin.get_help_message()) | ||
help += "\n" | ||
return help |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be replaced with a function to pretty print the option+usage table as noted above.
self.parser = self.create_parser(prog, usage, description, exclude_io) | ||
self.group = self.parser.add_argument_group("Options provided by plugins") | ||
self.plugins.add_plugin_option(self.group) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding the -P, --plugin
option should be pulled out of the function and moved to where all the other options are added.
plugins = {} | ||
if args.plugin: | ||
for provided_arg in args.plugin: | ||
key, val = provided_arg.get_value() | ||
if key in self.plugins.get_plugin_options(): | ||
plugins[key] = val | ||
else: | ||
raise ValueError( | ||
f"Unsupported option provided to -P/--plugin: {key}" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better if this code was part of the CLIPluginRegistry class, which should take args and extract out the active plugins, raising an exception if there was any invalid input or a plugin raised an error in preinit.
values (:obj:`Dict`): Dictionary of KEY[=VALUE] pairs returned | ||
by parsing the arguments provided to the -P/--plugin | ||
option. KEY is None if plugin was not invoked. VALUE is the | ||
empty string if the plugin was invoked without VALUE. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
KEY is None if plugin was not invoked.
If values
is a dictionary then by "KEY is None" here do you mean "is not set in the dictionary"?
Is values
the dictionary of all NAME=VALUE
pairs provided to --plugin
? Maybe it would be better to use a Namespace
here and keep the same semantics as argparse, i.e. args.name
is None
if the plugin was not invoked, True
if it was invoked without an arg, and a string argument that the plugin is responsible for parsing if an argument was given.
|
||
test_under_flux 4 job | ||
|
||
export FLUX_CLI_PLUGINPATH=${FLUX_BUILD_DIR}/t/cli-plugins/cli/plugins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line needs to be moved above test_under_flux
or else the job validator will not have FLUX_CLI_PLUGINPATH
in its environment.
However, after fixing that every test is failing because the CustomFluxionPolicyPlugin
always raises the error hi hobbs
Thanks for this work @wihobbs! Some high level comments on the approach so far:
|
Thanks for taking the time to dissect all of this @grondo! It was getting difficult in the thick of this project to figure out what exactly to do next, so thanks for getting me going. I'll note a few things:
|
This is being put up as WIP so that people can start to comment on the proposed design and point out testing gaps.
This is built off of @grondo's cli-plugins branch, with the modification that plugins should be namespaced under a
-P
flag, similar to how clang's-Wl
flag works but more along the lines of-o/-S
"plus" in flux. Here you get-P KEY[=VAL]
but with plugin validation and ability for args to modify jobspecs and config files. It also incorporates the suggestion for aFLUX_CLI_PLUGINPATH
environment variable that overrides the standard path in confdir for testing.I'll fold up the outstanding TODOs here, most are documented in the comments:
argparse.HelpFormatter
class to create a better-formatted argument group for the pluginsself.plugins
in theCLIPluginRegistry
validate
callback for bar.py seems to be doing nothing; probably misunderstanding how the interface for the validator is supposed to workI have been staring at this all day and my brain is toast, so apologies if some very basic things are missing or out of place.