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:
parent
8bc0d809a6
commit
0c51a30d93
|
@ -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
|
||||
|
||||
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)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
[defaults]
|
||||
cow_selection=random
|
||||
|
||||
[ssh_connection]
|
||||
control_path=/var/tmp
|
|
@ -0,0 +1,2 @@
|
|||
[defaults]
|
||||
cow_selection=random
|
|
@ -0,0 +1,2 @@
|
|||
[defaults]
|
||||
cow_destruction=random
|
|
@ -0,0 +1,2 @@
|
|||
[ssh_connection]
|
||||
controller_road=/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:
|
||||
- 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
|
||||
|
|
Loading…
Reference in New Issue