mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
This implements a basic --check mode which for now is only implemented on template & copy operations. More detail will be shared with the list
shortly.
This commit is contained in:
parent
28cf95e585
commit
fed82c2188
21 changed files with 125 additions and 24 deletions
|
@ -46,7 +46,7 @@ class Cli(object):
|
|||
''' create an options parser for bin/ansible '''
|
||||
|
||||
parser = utils.base_parser(constants=C, runas_opts=True, subset_opts=True, async_opts=True,
|
||||
output_opts=True, connect_opts=True, usage='%prog <host-pattern> [options]')
|
||||
output_opts=True, connect_opts=True, check_opts=True, usage='%prog <host-pattern> [options]')
|
||||
parser.add_option('-a', '--args', dest='module_args',
|
||||
help="module arguments", default=C.DEFAULT_MODULE_ARGS)
|
||||
parser.add_option('-m', '--module-name', dest='module_name',
|
||||
|
@ -109,7 +109,8 @@ class Cli(object):
|
|||
pattern=pattern,
|
||||
callbacks=self.callbacks, sudo=options.sudo,
|
||||
sudo_pass=sudopass,sudo_user=options.sudo_user,
|
||||
transport=options.connection, subset=options.subset
|
||||
transport=options.connection, subset=options.subset,
|
||||
check=options.check
|
||||
)
|
||||
|
||||
if options.seconds:
|
||||
|
|
|
@ -52,11 +52,13 @@ def main(args):
|
|||
|
||||
# create parser for CLI options
|
||||
usage = "%prog playbook.yml"
|
||||
parser = utils.base_parser(constants=C, usage=usage, connect_opts=True, runas_opts=True, subset_opts=True)
|
||||
parser = utils.base_parser(constants=C, usage=usage, connect_opts=True,
|
||||
runas_opts=True, subset_opts=True, check_opts=True)
|
||||
parser.add_option('-e', '--extra-vars', dest="extra_vars", default=None,
|
||||
help="set additional key=value variables from the CLI")
|
||||
parser.add_option('-t', '--tags', dest='tags', default='all',
|
||||
help="only run plays and tasks tagged with these values")
|
||||
# FIXME: list hosts is a common option and can be moved to utils/__init__.py
|
||||
parser.add_option('--list-hosts', dest='listhosts', action='store_true',
|
||||
help="dump out a list of hosts, each play will run against, does not run playbook!")
|
||||
parser.add_option('--syntax-check', dest='syntax', action='store_true',
|
||||
|
@ -120,6 +122,7 @@ def main(args):
|
|||
extra_vars=extra_vars,
|
||||
private_key_file=options.private_key_file,
|
||||
only_tags=only_tags,
|
||||
check=options.check
|
||||
)
|
||||
|
||||
if options.listhosts:
|
||||
|
|
|
@ -133,7 +133,7 @@ class AnsibleModule(object):
|
|||
|
||||
def __init__(self, argument_spec, bypass_checks=False, no_log=False,
|
||||
check_invalid_arguments=True, mutually_exclusive=None, required_together=None,
|
||||
required_one_of=None, add_file_common_args=False):
|
||||
required_one_of=None, add_file_common_args=False, supports_check_mode=False):
|
||||
|
||||
'''
|
||||
common code for quickly building an ansible module in Python
|
||||
|
@ -142,6 +142,8 @@ class AnsibleModule(object):
|
|||
'''
|
||||
|
||||
self.argument_spec = argument_spec
|
||||
self.supports_check_mode = supports_check_mode
|
||||
self.check_mode = False
|
||||
|
||||
if add_file_common_args:
|
||||
self.argument_spec.update(FILE_COMMON_ARGUMENTS)
|
||||
|
@ -149,7 +151,7 @@ class AnsibleModule(object):
|
|||
os.environ['LANG'] = MODULE_LANG
|
||||
(self.params, self.args) = self._load_params()
|
||||
|
||||
self._legal_inputs = []
|
||||
self._legal_inputs = [ 'CHECKMODE' ]
|
||||
self._handle_aliases()
|
||||
|
||||
if check_invalid_arguments:
|
||||
|
@ -301,6 +303,8 @@ class AnsibleModule(object):
|
|||
new_context[i] = cur_context[i]
|
||||
if cur_context != new_context:
|
||||
try:
|
||||
if self.check_mode:
|
||||
return True
|
||||
rc = selinux.lsetfilecon(path, ':'.join(new_context))
|
||||
except OSError:
|
||||
self.fail_json(path=path, msg='invalid selinux context', new_context=new_context, cur_context=cur_context, input_was=context)
|
||||
|
@ -319,6 +323,8 @@ class AnsibleModule(object):
|
|||
uid = pwd.getpwnam(owner).pw_uid
|
||||
except KeyError:
|
||||
self.fail_json(path=path, msg='chown failed: failed to look up user %s' % owner)
|
||||
if self.check_mode:
|
||||
return True
|
||||
try:
|
||||
os.chown(path, uid, -1)
|
||||
except OSError:
|
||||
|
@ -332,6 +338,8 @@ class AnsibleModule(object):
|
|||
return changed
|
||||
old_user, old_group = self.user_and_group(path)
|
||||
if old_group != group:
|
||||
if self.check_mode:
|
||||
return True
|
||||
try:
|
||||
gid = grp.getgrnam(group).gr_gid
|
||||
except KeyError:
|
||||
|
@ -357,6 +365,8 @@ class AnsibleModule(object):
|
|||
prev_mode = stat.S_IMODE(st[stat.ST_MODE])
|
||||
|
||||
if prev_mode != mode:
|
||||
if self.check_mode:
|
||||
return True
|
||||
# FIXME: comparison against string above will cause this to be executed
|
||||
# every time
|
||||
try:
|
||||
|
@ -451,6 +461,11 @@ class AnsibleModule(object):
|
|||
|
||||
def _check_invalid_arguments(self):
|
||||
for (k,v) in self.params.iteritems():
|
||||
if k == 'CHECKMODE':
|
||||
if not self.supports_check_mode:
|
||||
self.exit_json(skipped=True, msg="remote module does not support check mode")
|
||||
if self.supports_check_mode:
|
||||
self.check_mode = True
|
||||
if k not in self._legal_inputs:
|
||||
self.fail_json(msg="unsupported parameter for module: %s" % k)
|
||||
|
||||
|
|
|
@ -61,7 +61,8 @@ class PlayBook(object):
|
|||
extra_vars = None,
|
||||
only_tags = None,
|
||||
subset = C.DEFAULT_SUBSET,
|
||||
inventory = None):
|
||||
inventory = None,
|
||||
check = False):
|
||||
|
||||
"""
|
||||
playbook: path to a playbook file
|
||||
|
@ -79,6 +80,7 @@ class PlayBook(object):
|
|||
stats: holds aggregrate data about events occuring to each host
|
||||
sudo: if not specified per play, requests all plays use sudo mode
|
||||
inventory: can be specified instead of host_list to use a pre-existing inventory object
|
||||
check: don't change anything, just try to detect some potential changes
|
||||
"""
|
||||
|
||||
self.SETUP_CACHE = SETUP_CACHE
|
||||
|
@ -91,6 +93,7 @@ class PlayBook(object):
|
|||
if only_tags is None:
|
||||
only_tags = [ 'all' ]
|
||||
|
||||
self.check = check
|
||||
self.module_path = module_path
|
||||
self.forks = forks
|
||||
self.timeout = timeout
|
||||
|
@ -267,7 +270,8 @@ class PlayBook(object):
|
|||
setup_cache=self.SETUP_CACHE, basedir=task.play.basedir,
|
||||
conditional=task.only_if, callbacks=self.runner_callbacks,
|
||||
sudo=task.sudo, sudo_user=task.sudo_user,
|
||||
transport=task.transport, sudo_pass=task.sudo_pass, is_playbook=True
|
||||
transport=task.transport, sudo_pass=task.sudo_pass, is_playbook=True,
|
||||
check=self.check
|
||||
)
|
||||
|
||||
if task.async_seconds == 0:
|
||||
|
@ -373,6 +377,7 @@ class PlayBook(object):
|
|||
remote_pass=self.remote_pass, remote_port=play.remote_port, private_key_file=self.private_key_file,
|
||||
setup_cache=self.SETUP_CACHE, callbacks=self.runner_callbacks, sudo=play.sudo, sudo_user=play.sudo_user,
|
||||
transport=play.transport, sudo_pass=self.sudo_pass, is_playbook=True, module_vars=play.vars,
|
||||
check=self.check
|
||||
).run()
|
||||
self.stats.compute(setup_results, setup=True)
|
||||
|
||||
|
|
|
@ -114,10 +114,12 @@ class Runner(object):
|
|||
module_vars=None, # a playbooks internals thing
|
||||
is_playbook=False, # running from playbook or not?
|
||||
inventory=None, # reference to Inventory object
|
||||
subset=None # subset pattern
|
||||
subset=None, # subset pattern
|
||||
check=False # don't make any changes, just try to probe for potential changes
|
||||
):
|
||||
|
||||
# storage & defaults
|
||||
self.check = check
|
||||
self.setup_cache = utils.default(setup_cache, lambda: collections.defaultdict(dict))
|
||||
self.basedir = utils.default(basedir, lambda: os.getcwd())
|
||||
self.callbacks = utils.default(callbacks, lambda: DefaultRunnerCallbacks())
|
||||
|
@ -207,6 +209,11 @@ class Runner(object):
|
|||
|
||||
cmd = ""
|
||||
if not is_new_style:
|
||||
if 'CHECKMODE=True' in args:
|
||||
# if module isn't using AnsibleModuleCommon infrastructure we can't be certain it knows how to
|
||||
# do --check mode, so to be safe we will not run it.
|
||||
return ReturnData(conn=conn, result=dict(skippped=True, msg="cannot run check mode against old-style modules"))
|
||||
|
||||
args = utils.template(self.basedir, args, inject)
|
||||
argsfile = self._transfer_str(conn, tmp, 'arguments', args)
|
||||
if async_jid is None:
|
||||
|
|
|
@ -35,6 +35,10 @@ class ActionModule(object):
|
|||
self.runner = runner
|
||||
|
||||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
|
||||
if self.runner.check:
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module')
|
||||
|
||||
args = parse_kv(module_args)
|
||||
if not 'hostname' in args:
|
||||
raise ae("'hostname' is a required argument.")
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from ansible.runner.return_data import ReturnData
|
||||
|
||||
class ActionModule(object):
|
||||
|
||||
def __init__(self, runner):
|
||||
|
@ -23,6 +25,9 @@ class ActionModule(object):
|
|||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
''' transfer the given module name, plus the async module, then run it '''
|
||||
|
||||
if self.runner.check:
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module'))
|
||||
|
||||
# shell and command module are the same
|
||||
if module_name == 'shell':
|
||||
module_name = 'command'
|
||||
|
|
|
@ -69,6 +69,12 @@ class ActionModule(object):
|
|||
|
||||
exec_rc = None
|
||||
if local_md5 != remote_md5:
|
||||
|
||||
if self.runner.check:
|
||||
# TODO: if the filesize is small, include a nice pretty-printed diff by
|
||||
# calling a (new) diff callback
|
||||
return ReturnData(conn=conn, result=dict(changed=True))
|
||||
|
||||
# transfer the file to a remote tmp location
|
||||
tmp_src = tmp + os.path.basename(source)
|
||||
conn.put_file(source, tmp_src)
|
||||
|
@ -86,5 +92,7 @@ class ActionModule(object):
|
|||
|
||||
tmp_src = tmp + os.path.basename(source)
|
||||
module_args = "%s src=%s" % (module_args, tmp_src)
|
||||
if self.runner.check:
|
||||
module_args = "%s CHECKMODE=True" % module_args
|
||||
return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject)
|
||||
|
||||
|
|
|
@ -29,6 +29,10 @@ class ActionModule(object):
|
|||
self.runner = runner
|
||||
|
||||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
|
||||
# note: the fail module does not need to pay attention to check mode
|
||||
# it always runs.
|
||||
|
||||
args = utils.parse_kv(module_args)
|
||||
if not 'msg' in args:
|
||||
args['msg'] = 'Failed as requested from task'
|
||||
|
|
|
@ -36,6 +36,9 @@ class ActionModule(object):
|
|||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
''' handler for fetch operations '''
|
||||
|
||||
if self.runner.check:
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not (yet) supported for this module'))
|
||||
|
||||
# load up options
|
||||
options = utils.parse_kv(module_args)
|
||||
source = options.get('src', None)
|
||||
|
|
|
@ -33,6 +33,10 @@ class ActionModule(object):
|
|||
self.runner = runner
|
||||
|
||||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
|
||||
# the group_by module does not need to pay attention to check mode.
|
||||
# it always runs.
|
||||
|
||||
args = parse_kv(self.runner.module_args)
|
||||
if not 'key' in args:
|
||||
raise ae("'key' is a required argument.")
|
||||
|
|
|
@ -36,6 +36,13 @@ class ActionModule(object):
|
|||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
''' transfer & execute a module that is not 'copy' or 'template' '''
|
||||
|
||||
if self.runner.check:
|
||||
if module_name in [ 'shell', 'command' ]:
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for %s' % module_name))
|
||||
# else let the module parsing code decide, though this will only be allowed for AnsibleModuleCommon using
|
||||
# python modules for now
|
||||
module_args += " CHECKMODE=True"
|
||||
|
||||
# shell and command are the same module
|
||||
if module_name == 'shell':
|
||||
module_name = 'command'
|
||||
|
|
|
@ -48,6 +48,10 @@ class ActionModule(object):
|
|||
|
||||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
''' run the pause action module '''
|
||||
|
||||
# note: this module does not need to pay attention to the 'check'
|
||||
# flag, it always runs
|
||||
|
||||
hosts = ', '.join(self.runner.host_set)
|
||||
args = parse_kv(template(self.runner.basedir, module_args, inject))
|
||||
|
||||
|
|
|
@ -29,6 +29,11 @@ class ActionModule(object):
|
|||
self.runner = runner
|
||||
|
||||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
|
||||
if self.runner.check:
|
||||
# in --check mode, always skip this module execution
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True))
|
||||
|
||||
executable = ''
|
||||
# From library/command, keep in sync
|
||||
r = re.compile(r'(^|\s)(executable)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)\s|$)')
|
||||
|
|
|
@ -31,6 +31,10 @@ class ActionModule(object):
|
|||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
''' handler for file transfer operations '''
|
||||
|
||||
if self.runner.check:
|
||||
# in check mode, always skip this module
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module'))
|
||||
|
||||
tokens = shlex.split(module_args)
|
||||
source = tokens[0]
|
||||
# FIXME: error handling
|
||||
|
|
|
@ -29,6 +29,9 @@ class ActionModule(object):
|
|||
def run(self, conn, tmp, module_name, module_args, inject):
|
||||
''' handler for template operations '''
|
||||
|
||||
# note: since this module just calls the copy module, the --check mode support
|
||||
# can be implemented entirely over there
|
||||
|
||||
if not self.runner.is_playbook:
|
||||
raise errors.AnsibleError("in current versions of ansible, templates are only usable in playbooks")
|
||||
|
||||
|
@ -62,21 +65,32 @@ class ActionModule(object):
|
|||
base = os.path.basename(source)
|
||||
dest = os.path.join(dest, base)
|
||||
|
||||
# template the source data locally & transfer
|
||||
# template the source data locally & get ready to transfer
|
||||
try:
|
||||
resultant = utils.template_from_file(self.runner.basedir, source, inject)
|
||||
except Exception, e:
|
||||
result = dict(failed=True, msg=str(e))
|
||||
return ReturnData(conn=conn, comm_ok=False, result=result)
|
||||
|
||||
local_md5 = utils.md5s(resultant)
|
||||
remote_md5 = self.runner._remote_md5(conn, tmp, dest)
|
||||
|
||||
if local_md5 != remote_md5:
|
||||
|
||||
# template is different from the remote value
|
||||
|
||||
xfered = self.runner._transfer_str(conn, tmp, 'source', resultant)
|
||||
# fix file permissions when the copy is done as a different user
|
||||
if self.runner.sudo and self.runner.sudo_user != 'root':
|
||||
self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered,
|
||||
tmp)
|
||||
self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered, tmp)
|
||||
|
||||
# run the copy module
|
||||
module_args = "%s src=%s dest=%s" % (module_args, xfered, dest)
|
||||
|
||||
if self.runner.check:
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True))
|
||||
else:
|
||||
return self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject)
|
||||
|
||||
else:
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=False))
|
||||
|
||||
|
|
|
@ -392,7 +392,7 @@ def increment_debug(option, opt, value, parser):
|
|||
VERBOSITY += 1
|
||||
|
||||
def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
|
||||
async_opts=False, connect_opts=False, subset_opts=False):
|
||||
async_opts=False, connect_opts=False, subset_opts=False, check_opts=False):
|
||||
''' create an options parser for any ansible script '''
|
||||
|
||||
parser = SortedOptParser(usage, version=version("%prog"))
|
||||
|
@ -449,6 +449,11 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
|
|||
parser.add_option('-B', '--background', dest='seconds', type='int', default=0,
|
||||
help='run asynchronously, failing after X seconds (default=N/A)')
|
||||
|
||||
if check_opts:
|
||||
parser.add_option("-C", "--check", default=False, dest='check', action='store_true',
|
||||
help="don't make any changes, instead try to predict some of the changes that may occur"
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def do_encrypt(result, encrypt, salt_size=None, salt=None):
|
||||
|
|
|
@ -69,7 +69,7 @@ def main():
|
|||
dest=dict(required=True),
|
||||
backup=dict(default=False, choices=BOOLEANS),
|
||||
),
|
||||
add_file_common_args=True
|
||||
add_file_common_args=True,
|
||||
)
|
||||
|
||||
src = os.path.expanduser(module.params['src'])
|
||||
|
|
|
@ -134,7 +134,8 @@ def main():
|
|||
state = dict(choices=['file','directory','link','absent'], default='file'),
|
||||
path = dict(aliases=['dest', 'name'], required=True),
|
||||
),
|
||||
add_file_common_args=True
|
||||
add_file_common_args=True,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
params = module.params
|
||||
|
|
|
@ -36,7 +36,8 @@ author: Michael DeHaan
|
|||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict()
|
||||
argument_spec = dict(),
|
||||
supports_check_mode = True
|
||||
)
|
||||
module.exit_json(ping='pong')
|
||||
|
||||
|
|
|
@ -914,7 +914,8 @@ def run_setup(module):
|
|||
def main():
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict()
|
||||
argument_spec = dict(),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
data = run_setup(module)
|
||||
module.exit_json(**data)
|
||||
|
|
Loading…
Reference in a new issue