Skip to content

Commit 74842ad

Browse files
authored
1st part of ansible config, adds ansible-config to view/manage configs (ansible#12797)
* Start of ansible config project moved configuration definitions to external yaml file vs hardcoded * updated constants to be a data strcutures that are looped over and also return origin of setting changed to manager/data scheme for base classes new cli ansible-config to view/manage ansible configuration settings * prints green for default/unchanged and yellow for those that have been overriden * added list action to show all configurable settings and their associated ini and env var names * allows specifying config file to see what result would look like * TBD update, edit and view options removed test for functions that have been removed env_Vars are now list of dicts allows for version_added and deprecation in future added a couple of descriptions for future doc autogeneration ensure test does not fail if delete_me exists normalized 'path expansion' added yaml config to setup packaging removed unused imports better encoding handling updated as per feedback * pep8
1 parent 4344132 commit 74842ad

20 files changed

+2032
-575
lines changed

bin/ansible

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ if __name__ == '__main__':
127127
exit_code = 99
128128
except Exception as e:
129129
have_cli_options = cli is not None and cli.options is not None
130-
display.error("Unexpected Exception: %s" % to_text(e), wrap_text=False)
130+
display.error("Unexpected Exception, this is probably a bug: %s" % to_text(e), wrap_text=False)
131131
if not have_cli_options or have_cli_options and cli.options.verbosity > 2:
132132
log_only = False
133133
else:

bin/ansible-config

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ansible

hacking/conf2yaml.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env python
2+
3+
import ast
4+
import yaml
5+
import os
6+
import sys
7+
from ansible.parsing.yaml.dumper import AnsibleDumper
8+
9+
things = {}
10+
stuff = {}
11+
12+
op_map = {
13+
ast.Add: '+',
14+
ast.Sub: '-',
15+
ast.Mult: '*',
16+
ast.Div: '/',
17+
}
18+
19+
20+
21+
def get_values(values):
22+
if not isinstance(values, list):
23+
return get_value(values)
24+
ret = []
25+
for value in values:
26+
ret.append(get_value(value))
27+
return ret
28+
29+
30+
def get_value(value):
31+
if hasattr(value, 'id'):
32+
ret = value.id
33+
elif hasattr(value, 's'):
34+
ret = value.s
35+
elif hasattr(value, 'n'):
36+
ret = value.n
37+
elif hasattr(value, 'left'):
38+
operator = op_map[type(value.op)]
39+
left = get_values(value.left)
40+
right = get_values(value.right)
41+
return '%s %s %s' % (left, operator, right)
42+
elif hasattr(value, 'value'):
43+
ret = value.value
44+
elif hasattr(value, 'elts'):
45+
ret = get_values(value.elts)
46+
elif isinstance(value, ast.Call):
47+
func, args, kwargs = get_call(value)
48+
args[:] = [repr(arg) for arg in args]
49+
for k, v in kwargs.items():
50+
args.append('%s=%s' % (k, repr(v)))
51+
return '%s(%s)' % (func, ', '.join(args))
52+
else:
53+
return value
54+
55+
return get_value(ret)
56+
57+
58+
def get_call(value):
59+
args = []
60+
for arg in value.args:
61+
v = get_value(arg)
62+
try:
63+
v = getattr(C, v, v)
64+
except:
65+
pass
66+
args.append(v)
67+
kwargs = {}
68+
for keyword in value.keywords:
69+
v = get_value(keyword.value)
70+
try:
71+
v = getattr(C, v, v)
72+
except:
73+
pass
74+
kwargs[keyword.arg] = v
75+
76+
func = get_value(value.func)
77+
try:
78+
attr = '.%s' % value.func.attr
79+
except:
80+
attr = ''
81+
return '%s%s' % (func, attr), args, kwargs
82+
83+
84+
with open(sys.argv[1]) as f:
85+
tree = ast.parse(f.read())
86+
87+
for item in tree.body:
88+
if hasattr(item, 'value') and isinstance(item.value, ast.Call):
89+
try:
90+
if item.value.func.id != 'get_config':
91+
continue
92+
except AttributeError:
93+
continue
94+
95+
_, args, kwargs = get_call(item.value)
96+
97+
name = get_value(item.targets[0])
98+
section = args[1].lower()
99+
config = args[2]
100+
101+
# new form
102+
if name not in stuff:
103+
stuff[name] = {}
104+
stuff[name] = {
105+
'desc': 'TODO: write it',
106+
'ini': [{'section': section, 'key': config}],
107+
'env': [args[3]],
108+
'default': args[4] if len(args) == 5 else None,
109+
'yaml': {'key': '%s.%s' % (section, config)},
110+
'vars': []
111+
}
112+
stuff[name].update(kwargs)
113+
114+
## ini like
115+
#if section not in things:
116+
# things[section] = {}
117+
118+
#things[section][config] = {
119+
# 'env_var': args[3],
120+
# 'default': args[4] if len(args) == 5 else 'UNKNOWN'
121+
#}
122+
#things[section][config].update(kwargs)
123+
print(yaml.dump(stuff, Dumper=AnsibleDumper, indent=2, width=170))
124+

lib/ansible/cli/__init__.py

+7-16
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,8 @@ def validate_conflicts(self, vault_opts=False, runas_opts=False, fork_opts=False
269269
(op.su or op.su_user) and (op.become or op.become_user) or
270270
(op.sudo or op.sudo_user) and (op.become or op.become_user)):
271271

272-
self.parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
273-
"and su arguments ('--su', '--su-user', and '--ask-su-pass') "
274-
"and become arguments ('--become', '--become-user', and '--ask-become-pass')"
275-
" are exclusive of each other")
272+
self.parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') and su arguments ('--su', '--su-user', and '--ask-su-pass') "
273+
"and become arguments ('--become', '--become-user', and '--ask-become-pass') are exclusive of each other")
276274

277275
if fork_opts:
278276
if op.forks < 1:
@@ -283,20 +281,13 @@ def expand_tilde(option, opt, value, parser):
283281
setattr(parser.values, option.dest, os.path.expanduser(value))
284282

285283
@staticmethod
286-
def unfrack_path(option, opt, value, parser):
287-
setattr(parser.values, option.dest, unfrackpath(value))
284+
def unfrack_paths(option, opt, value, parser):
285+
if isinstance(value, string_types):
286+
setattr(parser.values, option.dest, [unfrackpath(x) for x in value.split(os.sep)])
288287

289288
@staticmethod
290-
def expand_paths(option, opt, value, parser):
291-
"""optparse action callback to convert a PATH style string arg to a list of path strings.
292-
293-
For ex, cli arg of '-p /blip/foo:/foo/bar' would be split on the
294-
default os.pathsep and the option value would be set to
295-
the list ['/blip/foo', '/foo/bar']. Each path string in the list
296-
will also have '~/' values expand via os.path.expanduser()."""
297-
path_entries = value.split(os.pathsep)
298-
expanded_path_entries = [os.path.expanduser(path_entry) for path_entry in path_entries]
299-
setattr(parser.values, option.dest, expanded_path_entries)
289+
def unfrack_path(option, opt, value, parser):
290+
setattr(parser.values, option.dest, unfrackpath(value))
300291

301292
@staticmethod
302293
def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False, runtask_opts=False, vault_opts=False, module_opts=False,

lib/ansible/cli/config.py

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# (c) 2017, Ansible by Red Hat, Inc.
2+
#
3+
# Ansible is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# Ansible is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
15+
#
16+
# ansible-vault is a script that encrypts/decrypts YAML files. See
17+
# http://docs.ansible.com/playbooks_vault.html for more details.
18+
19+
from __future__ import (absolute_import, division, print_function)
20+
__metaclass__ = type
21+
22+
import os
23+
import shlex
24+
import subprocess
25+
import sys
26+
import yaml
27+
28+
from ansible.cli import CLI
29+
from ansible.config.data import Setting
30+
from ansible.config.manager import ConfigManager
31+
from ansible.errors import AnsibleError, AnsibleOptionsError
32+
from ansible.module_utils._text import to_native, to_text
33+
from ansible.parsing.yaml.dumper import AnsibleDumper
34+
from ansible.utils.color import stringc
35+
from ansible.utils.path import unfrackpath
36+
37+
38+
try:
39+
from __main__ import display
40+
except ImportError:
41+
from ansible.utils.display import Display
42+
display = Display()
43+
44+
45+
class ConfigCLI(CLI):
46+
""" Config command line class """
47+
48+
VALID_ACTIONS = ("view", "edit", "update", "dump", "list")
49+
50+
def __init__(self, args, callback=None):
51+
52+
self.config_file = None
53+
self.config = None
54+
super(ConfigCLI, self).__init__(args, callback)
55+
56+
def parse(self):
57+
58+
self.parser = CLI.base_parser(
59+
usage = "usage: %%prog [%s] [--help] [options] [ansible.cfg]" % "|".join(self.VALID_ACTIONS),
60+
epilog = "\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
61+
)
62+
63+
self.parser.add_option('-c', '--config', dest='config_file', help="path to configuration file, defaults to first file found in precedence.")
64+
65+
self.set_action()
66+
67+
# options specific to self.actions
68+
if self.action == "list":
69+
self.parser.set_usage("usage: %prog list [options] ")
70+
if self.action == "dump":
71+
self.parser.set_usage("usage: %prog dump [options] [-c ansible.cfg]")
72+
elif self.action == "view":
73+
self.parser.set_usage("usage: %prog view [options] [-c ansible.cfg] ")
74+
elif self.action == "edit":
75+
self.parser.set_usage("usage: %prog edit [options] [-c ansible.cfg]")
76+
elif self.action == "update":
77+
self.parser.add_option('-s', '--setting', dest='setting', help="config setting, the section defaults to 'defaults'")
78+
self.parser.set_usage("usage: %prog update [options] [-c ansible.cfg] -s '[section.]setting=value'")
79+
80+
self.options, self.args = self.parser.parse_args()
81+
display.verbosity = self.options.verbosity
82+
83+
def run(self):
84+
85+
super(ConfigCLI, self).run()
86+
87+
if self.options.config_file:
88+
self.config_file = unfrackpath(self.options.config_file, follow=False)
89+
self.config = ConfigManager(self.config_file)
90+
else:
91+
self.config = ConfigManager()
92+
self.config_file = self.config.data.get_setting('ANSIBLE_CONFIG')
93+
try:
94+
if not os.path.exists(self.config_file):
95+
raise AnsibleOptionsError("%s does not exist or is not accessible" % (self.config_file))
96+
elif not os.path.isfile(self.config_file):
97+
raise AnsibleOptionsError("%s is not a valid file" % (self.config_file))
98+
99+
os.environ['ANSIBLE_CONFIG'] = self.config_file
100+
except:
101+
if self.action in ['view']:
102+
raise
103+
elif self.action in ['edit', 'update']:
104+
display.warning("File does not exist, used empty file: %s" % self.config_file)
105+
106+
self.execute()
107+
108+
def execute_update(self):
109+
'''
110+
Updates a single setting in the specified ansible.cfg
111+
'''
112+
raise AnsibleError("Option not implemented yet")
113+
114+
if self.options.setting is None:
115+
raise AnsibleOptionsError("update option requries a setting to update")
116+
117+
(entry, value) = self.options.setting.split('=')
118+
if '.' in entry:
119+
(section, option) = entry.split('.')
120+
else:
121+
section = 'defaults'
122+
option = entry
123+
subprocess.call([
124+
'ansible',
125+
'-m','ini_file',
126+
'localhost',
127+
'-c','local',
128+
'-a','"dest=%s section=%s option=%s value=%s backup=yes"' % (self.config_file, section, option, value)
129+
])
130+
131+
def execute_view(self):
132+
'''
133+
Displays the current config file
134+
'''
135+
try:
136+
with open(self.config_file, 'rb') as f:
137+
self.pager(to_text(f.read(), errors='surrogate_or_strict'))
138+
except Exception as e:
139+
raise AnsibleError("Failed to open config file: %s" % to_native(e))
140+
141+
def execute_edit(self):
142+
'''
143+
Opens ansible.cfg in the default EDITOR
144+
'''
145+
raise AnsibleError("Option not implemented yet")
146+
try:
147+
editor = shlex.split(os.environ.get('EDITOR','vi'))
148+
editor.append(self.config_file)
149+
subprocess.call(editor)
150+
except Exception as e:
151+
raise AnsibleError("Failed to open editor: %s" % to_native(e))
152+
153+
def execute_list(self):
154+
'''
155+
list all current configs reading lib/constants.py and shows env and config file setting names
156+
'''
157+
self.pager(to_text(yaml.dump(self.config.initial_defs, Dumper=AnsibleDumper), errors='surrogate_or_strict'))
158+
159+
def execute_dump(self):
160+
'''
161+
Shows the current settings, merges ansible.cfg if specified
162+
'''
163+
text = []
164+
defaults = self.config.initial_defs.copy()
165+
for setting in self.config.data.get_settings():
166+
if setting.name in defaults:
167+
defaults[setting.name] = setting
168+
169+
for setting in sorted(defaults):
170+
if isinstance(defaults[setting], Setting):
171+
if defaults[setting].origin == 'default':
172+
color = 'green'
173+
else:
174+
color = 'yellow'
175+
msg = "%s(%s) = %s" % (setting, defaults[setting].origin, defaults[setting].value)
176+
else:
177+
color = 'green'
178+
msg = "%s(%s) = %s" % (setting, 'default', defaults[setting].get('default'))
179+
text.append(stringc(msg, color))
180+
181+
self.pager(to_text('\n'.join(text), errors='surrogate_or_strict'))

lib/ansible/cli/galaxy.py

+5-10
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,9 @@ def set_action(self):
117117
if self.action not in ("delete", "import", "init", "login", "setup"):
118118
# NOTE: while the option type=str, the default is a list, and the
119119
# callback will set the value to a list.
120-
self.parser.add_option('-p', '--roles-path', dest='roles_path', action="callback", callback=CLI.expand_paths, type=str,
121-
default=C.DEFAULT_ROLES_PATH,
122-
help='The path to the directory containing your roles. The default is the roles_path configured in your ansible.cfg '
123-
'file (/etc/ansible/roles if not configured)')
124-
120+
self.parser.add_option('-p', '--roles-path', dest='roles_path', action="callback", callback=CLI.unfrack_paths, default=C.DEFAULT_ROLES_PATH,
121+
help='The path to the directory containing your roles. The default is the roles_path configured in your ansible.cfg'
122+
'file (/etc/ansible/roles if not configured)', type="string")
125123
if self.action in ("init", "install"):
126124
self.parser.add_option('-f', '--force', dest='force', action='store_true', default=False, help='Force overwriting an existing role')
127125

@@ -308,16 +306,13 @@ def execute_install(self):
308306
uses the args list of roles to be installed, unless -f was specified. The list of roles
309307
can be a name (which will be downloaded via the galaxy API and github), or it can be a local .tar.gz file.
310308
"""
311-
312309
role_file = self.get_opt("role_file", None)
313310

314311
if len(self.args) == 0 and role_file is None:
315-
# the user needs to specify one of either --role-file
316-
# or specify a single user/role name
312+
# the user needs to specify one of either --role-file or specify a single user/role name
317313
raise AnsibleOptionsError("- you must specify a user/role name or a roles file")
318314
elif len(self.args) == 1 and role_file is not None:
319-
# using a role file is mutually exclusive of specifying
320-
# the role name on the command line
315+
# using a role file is mutually exclusive of specifying the role name on the command line
321316
raise AnsibleOptionsError("- please specify a user/role name, or a roles file, but not both")
322317

323318
no_deps = self.get_opt("no_deps", False)

0 commit comments

Comments
 (0)