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.
pull/1982/head
Michael DeHaan 12 years ago
parent 28cf95e585
commit fed82c2188

@ -46,7 +46,7 @@ class Cli(object):
''' create an options parser for bin/ansible ''' ''' create an options parser for bin/ansible '''
parser = utils.base_parser(constants=C, runas_opts=True, subset_opts=True, async_opts=True, 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', parser.add_option('-a', '--args', dest='module_args',
help="module arguments", default=C.DEFAULT_MODULE_ARGS) help="module arguments", default=C.DEFAULT_MODULE_ARGS)
parser.add_option('-m', '--module-name', dest='module_name', parser.add_option('-m', '--module-name', dest='module_name',
@ -109,7 +109,8 @@ class Cli(object):
pattern=pattern, pattern=pattern,
callbacks=self.callbacks, sudo=options.sudo, callbacks=self.callbacks, sudo=options.sudo,
sudo_pass=sudopass,sudo_user=options.sudo_user, 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: if options.seconds:

@ -52,11 +52,13 @@ def main(args):
# create parser for CLI options # create parser for CLI options
usage = "%prog playbook.yml" 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, parser.add_option('-e', '--extra-vars', dest="extra_vars", default=None,
help="set additional key=value variables from the CLI") help="set additional key=value variables from the CLI")
parser.add_option('-t', '--tags', dest='tags', default='all', parser.add_option('-t', '--tags', dest='tags', default='all',
help="only run plays and tasks tagged with these values") 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', 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!") 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', parser.add_option('--syntax-check', dest='syntax', action='store_true',
@ -120,6 +122,7 @@ def main(args):
extra_vars=extra_vars, extra_vars=extra_vars,
private_key_file=options.private_key_file, private_key_file=options.private_key_file,
only_tags=only_tags, only_tags=only_tags,
check=options.check
) )
if options.listhosts: if options.listhosts:

@ -133,7 +133,7 @@ class AnsibleModule(object):
def __init__(self, argument_spec, bypass_checks=False, no_log=False, def __init__(self, argument_spec, bypass_checks=False, no_log=False,
check_invalid_arguments=True, mutually_exclusive=None, required_together=None, 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 common code for quickly building an ansible module in Python
@ -142,6 +142,8 @@ class AnsibleModule(object):
''' '''
self.argument_spec = argument_spec self.argument_spec = argument_spec
self.supports_check_mode = supports_check_mode
self.check_mode = False
if add_file_common_args: if add_file_common_args:
self.argument_spec.update(FILE_COMMON_ARGUMENTS) self.argument_spec.update(FILE_COMMON_ARGUMENTS)
@ -149,7 +151,7 @@ class AnsibleModule(object):
os.environ['LANG'] = MODULE_LANG os.environ['LANG'] = MODULE_LANG
(self.params, self.args) = self._load_params() (self.params, self.args) = self._load_params()
self._legal_inputs = [] self._legal_inputs = [ 'CHECKMODE' ]
self._handle_aliases() self._handle_aliases()
if check_invalid_arguments: if check_invalid_arguments:
@ -301,6 +303,8 @@ class AnsibleModule(object):
new_context[i] = cur_context[i] new_context[i] = cur_context[i]
if cur_context != new_context: if cur_context != new_context:
try: try:
if self.check_mode:
return True
rc = selinux.lsetfilecon(path, ':'.join(new_context)) rc = selinux.lsetfilecon(path, ':'.join(new_context))
except OSError: except OSError:
self.fail_json(path=path, msg='invalid selinux context', new_context=new_context, cur_context=cur_context, input_was=context) 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 uid = pwd.getpwnam(owner).pw_uid
except KeyError: except KeyError:
self.fail_json(path=path, msg='chown failed: failed to look up user %s' % owner) self.fail_json(path=path, msg='chown failed: failed to look up user %s' % owner)
if self.check_mode:
return True
try: try:
os.chown(path, uid, -1) os.chown(path, uid, -1)
except OSError: except OSError:
@ -332,6 +338,8 @@ class AnsibleModule(object):
return changed return changed
old_user, old_group = self.user_and_group(path) old_user, old_group = self.user_and_group(path)
if old_group != group: if old_group != group:
if self.check_mode:
return True
try: try:
gid = grp.getgrnam(group).gr_gid gid = grp.getgrnam(group).gr_gid
except KeyError: except KeyError:
@ -357,6 +365,8 @@ class AnsibleModule(object):
prev_mode = stat.S_IMODE(st[stat.ST_MODE]) prev_mode = stat.S_IMODE(st[stat.ST_MODE])
if prev_mode != mode: if prev_mode != mode:
if self.check_mode:
return True
# FIXME: comparison against string above will cause this to be executed # FIXME: comparison against string above will cause this to be executed
# every time # every time
try: try:
@ -451,6 +461,11 @@ class AnsibleModule(object):
def _check_invalid_arguments(self): def _check_invalid_arguments(self):
for (k,v) in self.params.iteritems(): 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: if k not in self._legal_inputs:
self.fail_json(msg="unsupported parameter for module: %s" % k) self.fail_json(msg="unsupported parameter for module: %s" % k)

@ -61,7 +61,8 @@ class PlayBook(object):
extra_vars = None, extra_vars = None,
only_tags = None, only_tags = None,
subset = C.DEFAULT_SUBSET, subset = C.DEFAULT_SUBSET,
inventory = None): inventory = None,
check = False):
""" """
playbook: path to a playbook file playbook: path to a playbook file
@ -79,6 +80,7 @@ class PlayBook(object):
stats: holds aggregrate data about events occuring to each host stats: holds aggregrate data about events occuring to each host
sudo: if not specified per play, requests all plays use sudo mode 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 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 self.SETUP_CACHE = SETUP_CACHE
@ -91,6 +93,7 @@ class PlayBook(object):
if only_tags is None: if only_tags is None:
only_tags = [ 'all' ] only_tags = [ 'all' ]
self.check = check
self.module_path = module_path self.module_path = module_path
self.forks = forks self.forks = forks
self.timeout = timeout self.timeout = timeout
@ -267,7 +270,8 @@ class PlayBook(object):
setup_cache=self.SETUP_CACHE, basedir=task.play.basedir, setup_cache=self.SETUP_CACHE, basedir=task.play.basedir,
conditional=task.only_if, callbacks=self.runner_callbacks, conditional=task.only_if, callbacks=self.runner_callbacks,
sudo=task.sudo, sudo_user=task.sudo_user, 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: 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, 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, 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, transport=play.transport, sudo_pass=self.sudo_pass, is_playbook=True, module_vars=play.vars,
check=self.check
).run() ).run()
self.stats.compute(setup_results, setup=True) self.stats.compute(setup_results, setup=True)

@ -114,10 +114,12 @@ class Runner(object):
module_vars=None, # a playbooks internals thing module_vars=None, # a playbooks internals thing
is_playbook=False, # running from playbook or not? is_playbook=False, # running from playbook or not?
inventory=None, # reference to Inventory object 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 # storage & defaults
self.check = check
self.setup_cache = utils.default(setup_cache, lambda: collections.defaultdict(dict)) self.setup_cache = utils.default(setup_cache, lambda: collections.defaultdict(dict))
self.basedir = utils.default(basedir, lambda: os.getcwd()) self.basedir = utils.default(basedir, lambda: os.getcwd())
self.callbacks = utils.default(callbacks, lambda: DefaultRunnerCallbacks()) self.callbacks = utils.default(callbacks, lambda: DefaultRunnerCallbacks())
@ -207,6 +209,11 @@ class Runner(object):
cmd = "" cmd = ""
if not is_new_style: 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) args = utils.template(self.basedir, args, inject)
argsfile = self._transfer_str(conn, tmp, 'arguments', args) argsfile = self._transfer_str(conn, tmp, 'arguments', args)
if async_jid is None: if async_jid is None:

@ -35,6 +35,10 @@ class ActionModule(object):
self.runner = runner self.runner = runner
def run(self, conn, tmp, module_name, module_args, inject): 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) args = parse_kv(module_args)
if not 'hostname' in args: if not 'hostname' in args:
raise ae("'hostname' is a required argument.") raise ae("'hostname' is a required argument.")

@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from ansible.runner.return_data import ReturnData
class ActionModule(object): class ActionModule(object):
def __init__(self, runner): def __init__(self, runner):
@ -23,6 +25,9 @@ class ActionModule(object):
def run(self, conn, tmp, module_name, module_args, inject): def run(self, conn, tmp, module_name, module_args, inject):
''' transfer the given module name, plus the async module, then run it ''' ''' 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 # shell and command module are the same
if module_name == 'shell': if module_name == 'shell':
module_name = 'command' module_name = 'command'

@ -69,6 +69,12 @@ class ActionModule(object):
exec_rc = None exec_rc = None
if local_md5 != remote_md5: 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 # transfer the file to a remote tmp location
tmp_src = tmp + os.path.basename(source) tmp_src = tmp + os.path.basename(source)
conn.put_file(source, tmp_src) conn.put_file(source, tmp_src)
@ -86,5 +92,7 @@ class ActionModule(object):
tmp_src = tmp + os.path.basename(source) tmp_src = tmp + os.path.basename(source)
module_args = "%s src=%s" % (module_args, tmp_src) 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) return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject)

@ -29,6 +29,10 @@ class ActionModule(object):
self.runner = runner self.runner = runner
def run(self, conn, tmp, module_name, module_args, inject): 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) args = utils.parse_kv(module_args)
if not 'msg' in args: if not 'msg' in args:
args['msg'] = 'Failed as requested from task' args['msg'] = 'Failed as requested from task'

@ -36,6 +36,9 @@ class ActionModule(object):
def run(self, conn, tmp, module_name, module_args, inject): def run(self, conn, tmp, module_name, module_args, inject):
''' handler for fetch operations ''' ''' 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 # load up options
options = utils.parse_kv(module_args) options = utils.parse_kv(module_args)
source = options.get('src', None) source = options.get('src', None)

@ -33,6 +33,10 @@ class ActionModule(object):
self.runner = runner self.runner = runner
def run(self, conn, tmp, module_name, module_args, inject): 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) args = parse_kv(self.runner.module_args)
if not 'key' in args: if not 'key' in args:
raise ae("'key' is a required argument.") raise ae("'key' is a required argument.")

@ -36,6 +36,13 @@ class ActionModule(object):
def run(self, conn, tmp, module_name, module_args, inject): def run(self, conn, tmp, module_name, module_args, inject):
''' transfer & execute a module that is not 'copy' or 'template' ''' ''' 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 # shell and command are the same module
if module_name == 'shell': if module_name == 'shell':
module_name = 'command' module_name = 'command'

@ -48,6 +48,10 @@ class ActionModule(object):
def run(self, conn, tmp, module_name, module_args, inject): def run(self, conn, tmp, module_name, module_args, inject):
''' run the pause action module ''' ''' 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) hosts = ', '.join(self.runner.host_set)
args = parse_kv(template(self.runner.basedir, module_args, inject)) args = parse_kv(template(self.runner.basedir, module_args, inject))

@ -29,6 +29,11 @@ class ActionModule(object):
self.runner = runner self.runner = runner
def run(self, conn, tmp, module_name, module_args, inject): 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 = '' executable = ''
# From library/command, keep in sync # From library/command, keep in sync
r = re.compile(r'(^|\s)(executable)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)\s|$)') 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): def run(self, conn, tmp, module_name, module_args, inject):
''' handler for file transfer operations ''' ''' 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) tokens = shlex.split(module_args)
source = tokens[0] source = tokens[0]
# FIXME: error handling # FIXME: error handling

@ -29,6 +29,9 @@ class ActionModule(object):
def run(self, conn, tmp, module_name, module_args, inject): def run(self, conn, tmp, module_name, module_args, inject):
''' handler for template operations ''' ''' 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: if not self.runner.is_playbook:
raise errors.AnsibleError("in current versions of ansible, templates are only usable in playbooks") 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) base = os.path.basename(source)
dest = os.path.join(dest, base) dest = os.path.join(dest, base)
# template the source data locally & transfer # template the source data locally & get ready to transfer
try: try:
resultant = utils.template_from_file(self.runner.basedir, source, inject) resultant = utils.template_from_file(self.runner.basedir, source, inject)
except Exception, e: except Exception, e:
result = dict(failed=True, msg=str(e)) result = dict(failed=True, msg=str(e))
return ReturnData(conn=conn, comm_ok=False, result=result) 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) xfered = self.runner._transfer_str(conn, tmp, 'source', resultant)
# fix file permissions when the copy is done as a different user # fix file permissions when the copy is done as a different user
if self.runner.sudo and self.runner.sudo_user != 'root': if self.runner.sudo and self.runner.sudo_user != 'root':
self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered, self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered, tmp)
tmp)
# run the copy module # run the copy module
module_args = "%s src=%s dest=%s" % (module_args, xfered, dest) module_args = "%s src=%s dest=%s" % (module_args, xfered, dest)
return self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject)
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 VERBOSITY += 1
def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, 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 ''' ''' create an options parser for any ansible script '''
parser = SortedOptParser(usage, version=version("%prog")) 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, parser.add_option('-B', '--background', dest='seconds', type='int', default=0,
help='run asynchronously, failing after X seconds (default=N/A)') 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 return parser
def do_encrypt(result, encrypt, salt_size=None, salt=None): def do_encrypt(result, encrypt, salt_size=None, salt=None):

@ -69,7 +69,7 @@ def main():
dest=dict(required=True), dest=dict(required=True),
backup=dict(default=False, choices=BOOLEANS), backup=dict(default=False, choices=BOOLEANS),
), ),
add_file_common_args=True add_file_common_args=True,
) )
src = os.path.expanduser(module.params['src']) src = os.path.expanduser(module.params['src'])

@ -134,7 +134,8 @@ def main():
state = dict(choices=['file','directory','link','absent'], default='file'), state = dict(choices=['file','directory','link','absent'], default='file'),
path = dict(aliases=['dest', 'name'], required=True), path = dict(aliases=['dest', 'name'], required=True),
), ),
add_file_common_args=True add_file_common_args=True,
supports_check_mode=True
) )
params = module.params params = module.params

@ -36,7 +36,8 @@ author: Michael DeHaan
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict() argument_spec = dict(),
supports_check_mode = True
) )
module.exit_json(ping='pong') module.exit_json(ping='pong')

@ -914,7 +914,8 @@ def run_setup(module):
def main(): def main():
global module global module
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict() argument_spec = dict(),
supports_check_mode = True,
) )
data = run_setup(module) data = run_setup(module)
module.exit_json(**data) module.exit_json(**data)

Loading…
Cancel
Save