diff --git a/ovs/extensions/healthcheck/expose_to_cli.py b/ovs/extensions/healthcheck/expose_to_cli.py index b308b76..3aa880e 100644 --- a/ovs/extensions/healthcheck/expose_to_cli.py +++ b/ovs/extensions/healthcheck/expose_to_cli.py @@ -29,10 +29,11 @@ import click import inspect from functools import wraps +from ovs.extensions.generic.configuration import Configuration from ovs.extensions.healthcheck.decorators import node_check from ovs.extensions.healthcheck.result import HCResults -from ovs.extensions.storage.volatilefactory import VolatileFactory from ovs.extensions.healthcheck.logger import Logger +from ovs.extensions.storage.volatilefactory import VolatileFactory # @todo Make it recursive. Current layout enforces SUBMODULE COMMAND, SUBMODULE, SUB, COMMAND is not possible @@ -66,6 +67,8 @@ def option(*param_decls, **attrs): """ Decorator to create an option value for the exposed method Wraps around the click decorator + Please note: creating commands should be done using the click decorator for commands to read in the __click_params__ attribute + to attach the right options to it. :param param_decls: All possible param declarations (eg '--to-json', '-t') :param attrs: All possible attributes. See click.Option for all possible items """ @@ -88,7 +91,7 @@ class CLIContext(object): pass -class CLI(click.MultiCommand): +class CLI(click.Group): """ Click CLI which dynamically loads all possible commands Implementations require an entry point @@ -122,6 +125,7 @@ def list_commands(self, ctx): :param ctx: Passed context :return: List of files to look for commands """ + _ = ctx sub_commands = self._discover_methods().keys() # Returns all underlying modules sub_commands.sort() return sub_commands @@ -135,6 +139,10 @@ def get_command(self, ctx, name): :return: Function pointer to the command or None when no import could happen :rtype: callable """ + cmd = self.commands.get(name) + if cmd: + return cmd + # More extensive - build the command and register discovery_data = self._discover_methods() if name in discovery_data.keys(): # The current passed name is a module. Wrap it up in a group and add all commands under it dynamically @@ -146,6 +154,7 @@ def get_command(self, ctx, name): cl = getattr(mod, function_data['class'])() module_commands[function_name] = click.Command(function_name, callback=getattr(cl, function_data['function'])) ret = self.GROUP_MODULE_CLASS(name, module_commands) + self.add_command(ret) return ret @classmethod @@ -217,13 +226,13 @@ def get_and_cache(): # Able to use the cache, has not expired yet del exposed_methods['expires'] return exposed_methods - except: + except Exception: cls.logger.exception('Unable to retrieve the exposed resources from cache') exposed_methods = discover() try: cls._discovery_cache = exposed_methods cls._volatile_client.set(cls.CACHE_KEY, exposed_methods) - except: + except Exception: cls.logger.exception('Unable to cache the exposed resources') del exposed_methods['expires'] return exposed_methods @@ -243,7 +252,7 @@ class CLIAddonGroup(CLI): """ Handles retrieving the right command """ - # @todo make it recurive here. The depth of the relation should indicate returning a command or antoher CLIAddonGroup + # @todo make it recursive here. The depth of the relation should indicate returning a command or another CLIAddonGroup def __init__(self, *args, **kwargs): # type: (*any, **any) -> None @@ -286,6 +295,10 @@ def get_command(self, ctx, name): :return: Function pointer to the command or None when no import could happen :rtype: callable """ + cmd = self.commands.get(name) + if cmd: + return cmd + # More extensive - build the command and register # @todo Make recursive with other groups discovery_data = self._discover_methods() # Will be coming from cache current_module_name = ctx.command.name @@ -295,10 +308,13 @@ def get_command(self, ctx, name): mod = imp.load_source(function_data['module_name'], function_data['location']) cl = getattr(mod, function_data['class'])() method_to_run = getattr(cl, function_data['function']) - click_command = click.command(name=name, - help=function_data.get('help'), - short_help=function_data.get('short_help')) - return click_command(method_to_run) + # Wrap around the click decorator to extract the option arguments using the function parameters + click_command_wrap = click.command(name=name, + help=function_data.get('help'), + short_help=function_data.get('short_help')) + cmd = click_command_wrap(method_to_run) + self.add_command(cmd) + return cmd ############################### # Healthcheck implementations # @@ -341,6 +357,9 @@ class HealthCheckShared(object): logger = Logger("healthcheck-ovs_clirunner") CMD_FOLDER = os.path.join(os.path.dirname(__file__), 'suites') # Folder to query for commands + CONTEXT_SETTINGS_KEY = '/ovs/healthcheck/default_arguments' + _context_settings = {} # Cache + @staticmethod def get_healthcheck_results(result_handler): # type (HCResults) -> dict @@ -362,6 +381,12 @@ def get_healthcheck_results(result_handler): # returns dict with minimal and detailed information return {'result': result, 'recap': dict(recount)} + @classmethod + def get_default_arguments(cls): + if not cls._context_settings: + cls._context_settings = Configuration.get(cls.CONTEXT_SETTINGS_KEY, default={}) + return cls._context_settings + class HealthcheckAddonGroup(CLIAddonGroup): """ @@ -379,7 +404,7 @@ class HealthcheckAddonGroup(CLIAddonGroup): def __init__(self, *args, **kwargs): # type: (*any, **any) -> None # Allow modules to be invoked without any other options behind them for backwards compatibility - super(HealthcheckAddonGroup, self).__init__(chain=True, + super(HealthcheckAddonGroup, self).__init__(chain=True, # Chain allows for multiple commands to specified invoke_without_command=True, callback=click.pass_context(self.run_methods_in_module), *args, **kwargs) @@ -407,6 +432,10 @@ def get_command(self, ctx, name): :return: Function pointer to the command or None when no import could happen :rtype: callable """ + cmd = self.commands.get(name) + if cmd: + return cmd + # More extensive - build the command and register discovery_data = self._discover_methods() # Will be coming from cache result_handler = ctx.obj.result_handler # type: HCResults current_module_name = ctx.command.name @@ -421,11 +450,13 @@ def get_command(self, ctx, name): method_to_run = getattr(cl, function_data['function']) full_name = '{0}-{1}'.format(module_name, name) wrapped_function = (self.healthcheck_wrapper(result_handler, full_name)(method_to_run)) # Inject our Healthcheck arguments - # Wrap around the click decorator to extract the option arguments - click_command = click.command(name=name, - help=function_data.get('help'), - short_help=function_data.get('short_help')) - return click_command(wrapped_function) + # Wrap around the click decorator to extract the option arguments using the function parameters + click_command_wrap = click.command(name=name, + help=function_data.get('help'), + short_help=function_data.get('short_help')) + cmd = click_command_wrap(wrapped_function) + self.add_command(cmd) + return cmd def healthcheck_wrapper(self, result_handler, test_name): # type: (HCResults, str) -> callable @@ -462,7 +493,7 @@ def new_function(*args, **kwargs): except (click.Abort, KeyboardInterrupt): self.logger.warning('Caught keyboard interrupt during {0}. Output may be incomplete!'.format(test_name)) raise HealthcheckTerminatedException(result_handler=result_handler) # Will be handled more globally. The whole Healthcheck should abort - except: + except Exception: self.logger.exception('Unhandled exception caught when executing {0}'.format(test_name)) result_handler.exception('Unhandled exception caught when executing {0}'.format(test_name)) # Change the name to the desired one @@ -500,15 +531,17 @@ class HealthCheckCLI(CLI): logger = HealthCheckShared.logger - def __init__(self, *args, **kwargs): - # type: (*any, **any) -> None + def __init__(self, context_settings=None, *args, **kwargs): + # type: (dict, *any, **any) -> None """ Initializes a CLI instance Injects a healthcheck specific callback """ - super(HealthCheckCLI, self).__init__(chain=True, - invoke_without_command=True, + if context_settings is None: + context_settings = dict(default_map=HealthCheckShared.get_default_arguments()) + super(HealthCheckCLI, self).__init__(invoke_without_command=True, result_callback=self.healthcheck_result_handler, + context_settings=context_settings, *args, **kwargs) def parse_args(self, ctx, args): @@ -536,11 +569,16 @@ def get_command(self, ctx, name): :return: Function pointer to the command or None when no import could happen :rtype: callable """ + cmd = self.commands.get(name) + if cmd: + return cmd + # More extensive - build the command and register discovery_data = self._discover_methods() if name in discovery_data.keys(): # The current passed name is a module. Wrap it up in a group and add all commands under it dynamically - ret = self.GROUP_MODULE_CLASS(name=name) - return ret + cmd = self.GROUP_MODULE_CLASS(name=name) + self.add_command(cmd) + return cmd @staticmethod @click.pass_context @@ -584,6 +622,28 @@ def invoke(self, ctx): except (click.Abort, KeyboardInterrupt): raise HealthcheckTerminatedException(result_handler=ctx.obj.result_handler) + def generate_configuration_options(self, cmd=None, ctx=None): + """ + Generate a dict with all possible configuration options available + Only to be + """ + options = {} + cmd = cmd or self + ctx = cmd.make_context(cmd.name, [], ctx) + if not ctx.obj: # Inject the result handler as the Healthcheck wraps the functions and requires this object + result_handler = HCResults(unattended=False, to_json=False) + ctx.obj = HealthCheckCLiContext(result_handler) + if hasattr(cmd, 'list_commands'): + for sub_cmd_name in cmd.list_commands(ctx): + sub_cmd = cmd.get_command(ctx, sub_cmd_name) + sub_cmd_options = {sub_cmd.name: self.generate_configuration_options(sub_cmd, ctx)} + options.update(sub_cmd_options) + else: + for param in cmd.params: + handled_value, _ = param.handle_parse_result(ctx, {}, []) + options.update({param.name: handled_value}) + return options + @click.group(cls=HealthCheckCLI) @click.option('--unattended', is_flag=True, help='Only output the results in a compact format') @@ -618,6 +678,20 @@ def run_method(cls, *args, **kwargs): """ Executes the given method like it would be executed through the CLI """ + _ = kwargs if not isinstance(args, tuple): args = (args,) return healthcheck_entry_point(args) + + @classmethod + def generate_configuration_options(cls, re_use_current_settings=False): + # type: (bool) -> dict + """ + Generate a complete structure indicating where tweaking is possible together with the default values + :param re_use_current_settings: Re-use the settings currently set. Defaults to False + It will regenerate a complete structure and apply the already set values if set to True + :return: All options available to the healthcheck + :rtype: dict + """ + cli = HealthCheckCLI(context_settings=None if re_use_current_settings else {}) + return cli.generate_configuration_options()