diff --git a/changelogs/fragments/ansible_config_plugins.yml b/changelogs/fragments/ansible_config_plugins.yml new file mode 100644 index 00000000000..1f0e7e4fd20 --- /dev/null +++ b/changelogs/fragments/ansible_config_plugins.yml @@ -0,0 +1,2 @@ +minor_changes: + - ansible-config now supports displaying plugin configuration info. diff --git a/lib/ansible/cli/config.py b/lib/ansible/cli/config.py index 286aad7eff7..80479e2ec73 100644 --- a/lib/ansible/cli/config.py +++ b/lib/ansible/cli/config.py @@ -10,9 +10,12 @@ import subprocess import yaml from ansible import context +import ansible.plugins.loader as plugin_loader + +from ansible import constants as C from ansible.cli import CLI from ansible.cli.arguments import option_helpers as opt_help -from ansible.config.manager import ConfigManager, Setting, find_ini_config_file +from ansible.config.manager import ConfigManager, Setting from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.module_utils._text import to_native, to_text, to_bytes from ansible.parsing.yaml.dumper import AnsibleDumper @@ -42,6 +45,8 @@ class ConfigCLI(CLI): opt_help.add_verbosity_options(common) common.add_argument('-c', '--config', dest='config_file', help="path to configuration file, defaults to first file found in precedence.") + common.add_argument("-t", "--type", action="store", default='base', dest='type', choices=['all', 'base'] + list(C.CONFIGURABLE_PLUGINS), + help="Show configuration for a plugin type, for a specific plugin's options see ansible-doc.") subparsers = self.parser.add_subparsers(dest='action') subparsers.required = True @@ -51,7 +56,7 @@ class ConfigCLI(CLI): dump_parser = subparsers.add_parser('dump', help='Dump configuration', parents=[common]) dump_parser.set_defaults(func=self.execute_dump) - dump_parser.add_argument('--only-changed', dest='only_changed', action='store_true', + dump_parser.add_argument('--only-changed', '--changed-only', dest='only_changed', action='store_true', help="Only show configurations that have changed from the default") view_parser = subparsers.add_parser('view', help='View configuration file', parents=[common]) @@ -85,8 +90,8 @@ class ConfigCLI(CLI): else: raise AnsibleOptionsError('The provided configuration file is missing or not accessible: %s' % to_native(self.config_file)) else: - self.config = ConfigManager() - self.config_file = find_ini_config_file() + self.config = C.config + self.config_file = self.config._config_file if self.config_file: try: @@ -155,34 +160,137 @@ class ConfigCLI(CLI): except Exception as e: raise AnsibleError("Failed to open editor: %s" % to_native(e)) + def _list_plugin_settings(self, ptype): + entries = {} + loader = getattr(plugin_loader, '%s_loader' % ptype) + for plugin in loader.all(class_only=True): + finalname = name = plugin._load_name + if name.startswith('_'): + # alias or deprecated + if os.path.islink(plugin._original_path): + continue + else: + finalname = name.replace('_', '', 1) + ' (DEPRECATED)' + + entries[finalname] = self.config.get_configuration_definitions(ptype, name) + + return entries + def execute_list(self): ''' list all current configs reading lib/constants.py and shows env and config file setting names ''' - self.pager(to_text(yaml.dump(self.config.get_configuration_definitions(ignore_private=True), Dumper=AnsibleDumper), errors='surrogate_or_strict')) + + config_entries = {} + if context.CLIARGS['type'] == 'base': + # this dumps main/common configs + config_entries = self.config.get_configuration_definitions(ignore_private=True) + elif context.CLIARGS['type'] == 'all': + # get global + config_entries = self.config.get_configuration_definitions(ignore_private=True) + + config_entries['PLUGINS'] = {} + # now each plugin type + for ptype in C.CONFIGURABLE_PLUGINS: + config_entries['PLUGINS'][ptype.upper()] = self._list_plugin_settings(ptype) + else: + config_entries = self._list_plugin_settings(context.CLIARGS['type']) + + self.pager(to_text(yaml.dump(config_entries, Dumper=AnsibleDumper), errors='surrogate_or_strict')) + + def _render_settings(self, config): + + text = [] + for setting in sorted(config): + if isinstance(config[setting], Setting): + if config[setting].origin == 'default': + color = 'green' + elif config[setting].origin == 'REQUIRED': + color = 'red' + else: + color = 'yellow' + msg = "%s(%s) = %s" % (setting, config[setting].origin, config[setting].value) + else: + color = 'green' + msg = "%s(%s) = %s" % (setting, 'default', config[setting].get('default')) + if not context.CLIARGS['only_changed'] or color == 'yellow': + text.append(stringc(msg, color)) + + return text + + def _get_global_configs(self): + config = self.config.get_configuration_definitions(ignore_private=True).copy() + for setting in self.config.data.get_settings(): + if setting.name in config: + config[setting.name] = setting + + return self._render_settings(config) + + def _get_plugin_configs(self, ptype): + + # prep loading + loader = getattr(plugin_loader, '%s_loader' % ptype) + + # acumulators + text = [] + config_entries = {} + for plugin in loader.all(class_only=True): + + # in case of deprecastion they diverge + finalname = name = plugin._load_name + if name.startswith('_'): + if os.path.islink(plugin._original_path): + # skip alias + continue + # deprecated, but use 'nice name' + finalname = name.replace('_', '', 1) + ' (DEPRECATED)' + + # default entries per plugin + config_entries[finalname] = self.config.get_configuration_definitions(ptype, name) + + try: + # populate config entries by loading plugin + dump = loader.get(name, class_only=True) + except Exception as e: + display.warning('Skipping "%s" %s plugin, as we cannot load plugin to check config due to : %s' % (name, ptype, to_native(e))) + continue + + # actually get the values + for setting in config_entries[finalname].keys(): + try: + v, o = C.config.get_config_value_and_origin(setting, plugin_type=ptype, plugin_name=name) + except AnsibleError as e: + if to_text(e).startswith('No setting was provided for required configuration'): + v = None + o = 'REQUIRED' + else: + raise e + config_entries[finalname][setting] = Setting(setting, v, o, None) + + # pretty please! + results = self._render_settings(config_entries[finalname]) + if results: + # avoid header for empty lists (only changed!) + text.append('\n%s:\n%s' % (finalname, '_' * len(finalname))) + text.extend(results) + return text def execute_dump(self): ''' Shows the current settings, merges ansible.cfg if specified ''' - # FIXME: deal with plugins, not just base config - text = [] - defaults = self.config.get_configuration_definitions(ignore_private=True).copy() - for setting in self.config.data.get_settings(): - if setting.name in defaults: - defaults[setting.name] = setting - - for setting in sorted(defaults): - if isinstance(defaults[setting], Setting): - if defaults[setting].origin == 'default': - color = 'green' - else: - color = 'yellow' - msg = "%s(%s) = %s" % (setting, defaults[setting].origin, defaults[setting].value) - else: - color = 'green' - msg = "%s(%s) = %s" % (setting, 'default', defaults[setting].get('default')) - if not context.CLIARGS['only_changed'] or color == 'yellow': - text.append(stringc(msg, color)) + if context.CLIARGS['type'] == 'base': + # deal with base + text = self._get_global_configs() + elif context.CLIARGS['type'] == 'all': + # deal with base + text = self._get_global_configs() + # deal with plugins + for ptype in C.CONFIGURABLE_PLUGINS: + text.append('\n%s:\n%s' % (ptype.upper(), '=' * len(ptype))) + text.extend(self._get_plugin_configs(ptype)) + else: + # deal with plugins + text = self._get_plugin_configs(context.CLIARGS['type']) self.pager(to_text('\n'.join(text), errors='surrogate_or_strict'))