ansible-config: add 'validate' option (#83007)

We can now validate both ansible.cfg and 'ANSIBLE_' env vars
match either core (-t base), installed plugin(s) (-t <plugin_type>) or both (-t all)
pull/81594/head
Brian Coca 1 month ago committed by GitHub
parent 8bc0d809a6
commit 0c51a30d93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,2 @@
minor_changes:
- ansible-config has new 'validate' option to find mispelled/forgein configurations in ini file or environment variables.

@ -9,9 +9,10 @@ from __future__ import annotations
from ansible.cli import CLI from ansible.cli import CLI
import os import os
import yaml
import shlex import shlex
import subprocess import subprocess
import sys
import yaml
from collections.abc import Mapping from collections.abc import Mapping
@ -49,6 +50,37 @@ def get_constants():
return get_constants.cvars return get_constants.cvars
def _ansible_env_vars(varname):
''' return true or false depending if variable name is possibly a 'configurable' ansible env variable '''
return all(
[
varname.startswith("ANSIBLE_"),
not varname.startswith(("ANSIBLE_TEST_", "ANSIBLE_LINT_")),
varname not in ("ANSIBLE_CONFIG", "ANSIBLE_DEV_HOME"),
]
)
def _get_evar_list(settings):
data = []
for setting in settings:
if 'env' in settings[setting] and settings[setting]['env']:
for varname in settings[setting]['env']:
data.append(varname.get('name'))
return data
def _get_ini_entries(settings):
data = {}
for setting in settings:
if 'ini' in settings[setting] and settings[setting]['ini']:
for kv in settings[setting]['ini']:
if not kv['section'] in data:
data[kv['section']] = set()
data[kv['section']].add(kv['key'])
return data
class ConfigCLI(CLI): class ConfigCLI(CLI):
""" Config command line class """ """ Config command line class """
@ -99,9 +131,13 @@ class ConfigCLI(CLI):
init_parser.add_argument('--disabled', dest='commented', action='store_true', default=False, init_parser.add_argument('--disabled', dest='commented', action='store_true', default=False,
help='Prefixes all entries with a comment character to disable them') help='Prefixes all entries with a comment character to disable them')
# search_parser = subparsers.add_parser('find', help='Search configuration') validate_parser = subparsers.add_parser('validate',
# search_parser.set_defaults(func=self.execute_search) help='Validate the configuration file and environment variables. '
# search_parser.add_argument('args', help='Search term', metavar='<search term>') 'By default it only checks the base settings without accounting for plugins (see -t).',
parents=[common])
validate_parser.set_defaults(func=self.execute_validate)
validate_parser.add_argument('--format', '-f', dest='format', action='store', choices=['ini', 'env'] , default='ini',
help='Output format for init')
def post_process_args(self, options): def post_process_args(self, options):
options = super(ConfigCLI, self).post_process_args(options) options = super(ConfigCLI, self).post_process_args(options)
@ -239,6 +275,7 @@ class ConfigCLI(CLI):
for ptype in C.CONFIGURABLE_PLUGINS: for ptype in C.CONFIGURABLE_PLUGINS:
config_entries['PLUGINS'][ptype.upper()] = self._list_plugin_settings(ptype) config_entries['PLUGINS'][ptype.upper()] = self._list_plugin_settings(ptype)
elif context.CLIARGS['type'] != 'base': elif context.CLIARGS['type'] != 'base':
# only for requested types
config_entries['PLUGINS'][context.CLIARGS['type']] = self._list_plugin_settings(context.CLIARGS['type'], context.CLIARGS['args']) config_entries['PLUGINS'][context.CLIARGS['type']] = self._list_plugin_settings(context.CLIARGS['type'], context.CLIARGS['args'])
return config_entries return config_entries
@ -358,7 +395,7 @@ class ConfigCLI(CLI):
elif default is None: elif default is None:
default = '' default = ''
if context.CLIARGS['commented']: if context.CLIARGS.get('commented', False):
entry['key'] = ';%s' % entry['key'] entry['key'] = ';%s' % entry['key']
key = desc + '\n%s=%s' % (entry['key'], default) key = desc + '\n%s=%s' % (entry['key'], default)
@ -552,6 +589,64 @@ class ConfigCLI(CLI):
self.pager(to_text(text, errors='surrogate_or_strict')) self.pager(to_text(text, errors='surrogate_or_strict'))
def execute_validate(self):
found = False
config_entries = self._list_entries_from_args()
plugin_types = config_entries.pop('PLUGINS', None)
if context.CLIARGS['format'] == 'ini':
if C.CONFIG_FILE is not None:
# validate ini config since it is found
sections = _get_ini_entries(config_entries)
# Also from plugins
if plugin_types:
for ptype in plugin_types:
for plugin in plugin_types[ptype].keys():
plugin_sections = _get_ini_entries(plugin_types[ptype][plugin])
for s in plugin_sections:
if s in sections:
sections[s].update(plugin_sections[s])
else:
sections[s] = plugin_sections[s]
if sections:
p = C.config._parsers[C.CONFIG_FILE]
for s in p.sections():
# check for valid sections
if s not in sections:
display.error(f"Found unknown section '{s}' in '{C.CONFIG_FILE}.")
found = True
continue
# check keys in valid sections
for k in p.options(s):
if k not in sections[s]:
display.error(f"Found unknown key '{k}' in section '{s}' in '{C.CONFIG_FILE}.")
found = True
elif context.CLIARGS['format'] == 'env':
# validate any 'ANSIBLE_' env vars found
evars = [varname for varname in os.environ.keys() if _ansible_env_vars(varname)]
if evars:
data = _get_evar_list(config_entries)
if plugin_types:
for ptype in plugin_types:
for plugin in plugin_types[ptype].keys():
data.extend(_get_evar_list(plugin_types[ptype][plugin]))
for evar in evars:
if evar not in data:
display.error(f"Found unknown environment variable '{evar}'.")
found = True
# we found discrepancies!
if found:
sys.exit(1)
# allsgood
display.display("All configurations seem valid!")
def main(args=None): def main(args=None):
ConfigCLI.cli_executor(args) ConfigCLI.cli_executor(args)

@ -0,0 +1,5 @@
[defaults]
cow_selection=random
[ssh_connection]
control_path=/var/tmp

@ -1,4 +1,4 @@
- name: test ansible-config for valid output and no dupes - name: test ansible-config init for valid output and no dupes
block: block:
- name: Create temporary file - name: Create temporary file
tempfile: tempfile:
@ -12,3 +12,47 @@
- name: run ini tester, for correctness and dupes - name: run ini tester, for correctness and dupes
shell: "{{ansible_playbook_python}} '{{role_path}}/files/ini_dupes.py' '{{ini_tempfile.path}}'" shell: "{{ansible_playbook_python}} '{{role_path}}/files/ini_dupes.py' '{{ini_tempfile.path}}'"
- name: test ansible-config validate
block:
# not testing w/o -t all as ansible-test uses it's own plugins and would give false positives
- name: validate config files
shell: ansible-config validate -t all -v
register: valid_cfg
loop:
- empty.cfg
- base_valid.cfg
- base_all_valid.cfg
- invalid_base.cfg
- invalid_plugins_config.ini
ignore_errors: true
environment:
ANSIBLE_CONFIG: "{{role_path ~ '/files/' ~ item}}"
- name: ensure expected cfg check results
assert:
that:
- valid_cfg['results'][0] is success
- valid_cfg['results'][1] is success
- valid_cfg['results'][2] is success
- valid_cfg['results'][3] is failed
- valid_cfg['results'][4] is failed
- name: validate env vars
shell: ansible-config validate -t all -v -f env
register: valid_env
environment:
ANSIBLE_COW_SELECTION: 1
- name: validate env vars
shell: ansible-config validate -t all -v -f env
register: invalid_env
ignore_errors: true
environment:
ANSIBLE_COW_DESTRUCTION: 1
- name: ensure env check is what we expected
assert:
that:
- valid_env is success
- invalid_env is failed

Loading…
Cancel
Save