From a12e0a0e874c6c0d18a1a2d83dcb106d207136af Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 28 Apr 2022 19:58:58 -0400 Subject: [PATCH] ansible-config added json/yaml output to list/dump (#77447) fixes #733644 --- changelogs/fragments/config_formats.yml | 2 + lib/ansible/cli/config.py | 101 ++++++++++++++++-------- lib/ansible/cli/doc.py | 7 +- lib/ansible/module_utils/common/json.py | 4 + 4 files changed, 79 insertions(+), 35 deletions(-) create mode 100644 changelogs/fragments/config_formats.yml diff --git a/changelogs/fragments/config_formats.yml b/changelogs/fragments/config_formats.yml new file mode 100644 index 00000000000..8485e4303a5 --- /dev/null +++ b/changelogs/fragments/config_formats.yml @@ -0,0 +1,2 @@ +minor_changes: + - ansible-config adds JSON and YAML output formats for list and dump actions. diff --git a/lib/ansible/cli/config.py b/lib/ansible/cli/config.py index 352529fc2bf..7492ef88fd3 100755 --- a/lib/ansible/cli/config.py +++ b/lib/ansible/cli/config.py @@ -10,13 +10,12 @@ __metaclass__ = type from ansible.cli import CLI import os +import yaml import shlex import subprocess from collections.abc import Mapping -import yaml - from ansible import context import ansible.plugins.loader as plugin_loader @@ -25,6 +24,7 @@ from ansible.cli.arguments import option_helpers as opt_help 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.module_utils.common.json import json_dump from ansible.module_utils.six import string_types from ansible.parsing.quoting import is_quoted from ansible.parsing.yaml.dumper import AnsibleDumper @@ -35,6 +35,10 @@ from ansible.utils.path import unfrackpath display = Display() +def yaml_dump(data, default_flow_style=True): + return yaml.dump(data, Dumper=AnsibleDumper, default_flow_style=default_flow_style) + + def get_constants(): ''' helper method to ensure we can template based on existing constants ''' if not hasattr(get_constants, 'cvars'): @@ -72,11 +76,15 @@ class ConfigCLI(CLI): list_parser = subparsers.add_parser('list', help='Print all config options', parents=[common]) list_parser.set_defaults(func=self.execute_list) + list_parser.add_argument('--format', '-f', dest='format', action='store', choices=['json', 'yaml'], default='yaml', + help='Output format for list') 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', '--changed-only', dest='only_changed', action='store_true', help="Only show configurations that have changed from the default") + dump_parser.add_argument('--format', '-f', dest='format', action='store', choices=['json', 'yaml', 'display'], default='display', + help='Output format for dump') view_parser = subparsers.add_parser('view', help='View configuration file', parents=[common]) view_parser.set_defaults(func=self.execute_view) @@ -238,7 +246,12 @@ class ConfigCLI(CLI): ''' config_entries = self._list_entries_from_args() - self.pager(to_text(yaml.dump(config_entries, Dumper=AnsibleDumper), errors='surrogate_or_strict')) + if context.CLIARGS['format'] == 'yaml': + output = yaml_dump(config_entries) + elif context.CLIARGS['format'] == 'json': + output = json_dump(config_entries) + + self.pager(to_text(output, errors='surrogate_or_strict')) def _get_settings_vars(self, settings, subkey): @@ -288,7 +301,7 @@ class ConfigCLI(CLI): if subkey == 'env': data.append('%s%s=%s' % (prefix, entry, default)) elif subkey == 'vars': - data.append(prefix + to_text(yaml.dump({entry: default}, Dumper=AnsibleDumper, default_flow_style=False), errors='surrogate_or_strict')) + data.append(prefix + to_text(yaml_dump({entry: default}, default_flow_style=False), errors='surrogate_or_strict')) data.append('') return data @@ -377,28 +390,35 @@ class ConfigCLI(CLI): def _render_settings(self, config): - text = [] + entries = [] for setting in sorted(config): - changed = False - if isinstance(config[setting], Setting): - # proceed normally - if config[setting].origin == 'default': - color = 'green' - elif config[setting].origin == 'REQUIRED': - # should include '_terms', '_input', etc - color = 'red' + changed = (config[setting].origin != 'default') + + if context.CLIARGS['format'] == 'display': + if isinstance(config[setting], Setting): + # proceed normally + if config[setting].origin == 'default': + color = 'green' + elif config[setting].origin == 'REQUIRED': + # should include '_terms', '_input', etc + color = 'red' + else: + color = 'yellow' + msg = "%s(%s) = %s" % (setting, config[setting].origin, config[setting].value) else: - color = 'yellow' - changed = True - msg = "%s(%s) = %s" % (setting, config[setting].origin, config[setting].value) + color = 'green' + msg = "%s(%s) = %s" % (setting, 'default', config[setting].get('default')) + + entry = stringc(msg, color) else: - color = 'green' - msg = "%s(%s) = %s" % (setting, 'default', config[setting].get('default')) + entry = {} + for key in config[setting]._fields: + entry[key] = getattr(config[setting], key) if not context.CLIARGS['only_changed'] or changed: - text.append(stringc(msg, color)) + entries.append(entry) - return text + return entries def _get_global_configs(self): config = self.config.get_configuration_definitions(ignore_private=True).copy() @@ -414,7 +434,7 @@ class ConfigCLI(CLI): loader = getattr(plugin_loader, '%s_loader' % ptype) # acumulators - text = [] + output = [] config_entries = {} # build list @@ -469,10 +489,14 @@ class ConfigCLI(CLI): # 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 + if context.CLIARGS['format'] == 'display': + # avoid header for empty lists (only changed!) + output.append('\n%s:\n%s' % (finalname, '_' * len(finalname))) + output.extend(results) + else: + output.append({finalname: results}) + + return output def execute_dump(self): ''' @@ -480,19 +504,34 @@ class ConfigCLI(CLI): ''' if context.CLIARGS['type'] == 'base': # deal with base - text = self._get_global_configs() + output = self._get_global_configs() elif context.CLIARGS['type'] == 'all': # deal with base - text = self._get_global_configs() + output = 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, context.CLIARGS['args'])) + plugin_list = self._get_plugin_configs(ptype, context.CLIARGS['args']) + if context.CLIARGS['format'] == 'display': + output.append('\n%s:\n%s' % (ptype.upper(), '=' * len(ptype))) + output.extend(plugin_list) + else: + if ptype in ('modules', 'doc_fragments'): + pname = ptype.upper() + else: + pname = '%s_PLUGINS' % ptype.upper() + output.append({pname: plugin_list}) else: # deal with plugins - text = self._get_plugin_configs(context.CLIARGS['type'], context.CLIARGS['args']) + output = self._get_plugin_configs(context.CLIARGS['type'], context.CLIARGS['args']) + + if context.CLIARGS['format'] == 'display': + text = '\n'.join(output) + if context.CLIARGS['format'] == 'yaml': + text = yaml_dump(output) + elif context.CLIARGS['format'] == 'json': + text = json_dump(output) - self.pager(to_text('\n'.join(text), errors='surrogate_or_strict')) + self.pager(to_text(text, errors='surrogate_or_strict')) def main(args=None): diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index 8b37e52cf3c..5aa46e7d09d 100755 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -10,7 +10,6 @@ __metaclass__ = type # ansible.cli needs to be imported first, to ensure the source bin/* scripts run that code first from ansible.cli import CLI -import json import pkgutil import os import os.path @@ -29,7 +28,7 @@ from ansible.cli.arguments import option_helpers as opt_help from ansible.collections.list import list_collection_dirs from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError from ansible.module_utils._text import to_native, to_text -from ansible.module_utils.common.json import AnsibleJSONEncoder +from ansible.module_utils.common.json import json_dump from ansible.module_utils.common.yaml import yaml_dump from ansible.module_utils.compat import importlib from ansible.module_utils.six import string_types @@ -62,7 +61,7 @@ def add_collection_plugins(plugin_list, plugin_type, coll_filter=None): def jdump(text): try: - display.display(json.dumps(text, cls=AnsibleJSONEncoder, sort_keys=True, indent=4)) + display.display(json_dump(text)) except TypeError as e: display.vvv(traceback.format_exc()) raise AnsibleError('We could not convert all the documentation into JSON as there was a conversion issue: %s' % to_native(e)) @@ -700,7 +699,7 @@ class DocCLI(CLI, RoleMixin): if not fail_on_errors: # Check whether JSON serialization would break try: - json.dumps(docs, cls=AnsibleJSONEncoder) + json_dump(docs) except Exception as e: # pylint:disable=broad-except plugin_docs[plugin] = { 'error': 'Cannot serialize documentation as JSON: %s' % to_native(e), diff --git a/lib/ansible/module_utils/common/json.py b/lib/ansible/module_utils/common/json.py index 866a89663b0..727083ca238 100644 --- a/lib/ansible/module_utils/common/json.py +++ b/lib/ansible/module_utils/common/json.py @@ -39,6 +39,10 @@ def _preprocess_unsafe_encode(value): return value +def json_dump(structure): + return json.dumps(structure, cls=AnsibleJSONEncoder, sort_keys=True, indent=4) + + class AnsibleJSONEncoder(json.JSONEncoder): ''' Simple encoder class to deal with JSON encoding of Ansible internal types