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)
This commit is contained in:
Brian Coca 2024-04-17 16:26:05 -04:00 committed by GitHub
parent 8bc0d809a6
commit 0c51a30d93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 158 additions and 6 deletions

View File

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

View File

@ -9,9 +9,10 @@ from __future__ import annotations
from ansible.cli import CLI
import os
import yaml
import shlex
import subprocess
import sys
import yaml
from collections.abc import Mapping
@ -49,6 +50,37 @@ def get_constants():
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):
""" Config command line class """
@ -99,9 +131,13 @@ class ConfigCLI(CLI):
init_parser.add_argument('--disabled', dest='commented', action='store_true', default=False,
help='Prefixes all entries with a comment character to disable them')
# search_parser = subparsers.add_parser('find', help='Search configuration')
# search_parser.set_defaults(func=self.execute_search)
# search_parser.add_argument('args', help='Search term', metavar='<search term>')
validate_parser = subparsers.add_parser('validate',
help='Validate the configuration file and environment variables. '
'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):
options = super(ConfigCLI, self).post_process_args(options)
@ -239,6 +275,7 @@ class ConfigCLI(CLI):
for ptype in C.CONFIGURABLE_PLUGINS:
config_entries['PLUGINS'][ptype.upper()] = self._list_plugin_settings(ptype)
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'])
return config_entries
@ -358,7 +395,7 @@ class ConfigCLI(CLI):
elif default is None:
default = ''
if context.CLIARGS['commented']:
if context.CLIARGS.get('commented', False):
entry['key'] = ';%s' % entry['key']
key = desc + '\n%s=%s' % (entry['key'], default)
@ -552,6 +589,64 @@ class ConfigCLI(CLI):
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):
ConfigCLI.cli_executor(args)

View File

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

View File

@ -0,0 +1,2 @@
[defaults]
cow_selection=random

View File

@ -0,0 +1,2 @@
[defaults]
cow_destruction=random

View File

@ -0,0 +1,2 @@
[ssh_connection]
controller_road=/var/tmp

View File

@ -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:
- name: Create temporary file
tempfile:
@ -12,3 +12,47 @@
- name: run ini tester, for correctness and dupes
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