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:
Michael DeHaan 2013-02-03 19:46:25 -05:00
parent 28cf95e585
commit fed82c2188
21 changed files with 125 additions and 24 deletions

View file

@ -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:

View file

@ -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:

View file

@ -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:
@ -300,7 +302,9 @@ class AnsibleModule(object):
if context[i] is None:
new_context[i] = cur_context[i]
if cur_context != new_context:
try:
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)

View file

@ -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)

View file

@ -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:

View file

@ -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.")

View file

@ -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'

View file

@ -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)

View file

@ -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'

View file

@ -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)

View file

@ -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.")

View file

@ -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'

View file

@ -47,7 +47,11 @@ class ActionModule(object):
}
def run(self, conn, tmp, module_name, module_args, inject):
''' run the pause actionmodule '''
''' 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))

View file

@ -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|$)')

View file

@ -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

View file

@ -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)
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)
local_md5 = utils.md5s(resultant)
remote_md5 = self.runner._remote_md5(conn, tmp, dest)
# run the copy module
module_args = "%s src=%s dest=%s" % (module_args, xfered, dest)
return self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject)
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)
# 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))

View file

@ -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):

View file

@ -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'])

View file

@ -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

View file

@ -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')

View file

@ -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)