diff --git a/CHANGELOG.md b/CHANGELOG.md index ac9ee7b5fd..8c3d19a288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Ansible Changes By Release Major new features: * Windows support (alpha) using native PowerShell remoting +* Tasks can now specify `run_once: true`, meaning they will be executed exactly once. This can be combined with delegate_to to trigger actions you want done just the one time versus for every host in inventory. New inventory scripts: diff --git a/docsite/rst/playbooks_delegation.rst b/docsite/rst/playbooks_delegation.rst index e298ebd690..7fa49733a7 100644 --- a/docsite/rst/playbooks_delegation.rst +++ b/docsite/rst/playbooks_delegation.rst @@ -118,6 +118,40 @@ Here is an example:: Note that you must have passphrase-less SSH keys or an ssh-agent configured for this to work, otherwise rsync will need to ask for a passphrase. +.. _run_once: + +Run Once +```````` + +In some cases there may be a need to only run a task one time and only on one host. This can be achieved +by configuring "run_once" on a task:: + + --- + # ... + + tasks: + + # ... + + - command: /opt/application/upgrade_db.py + run_once: true + + # ... + +This can be optionally paired with "delegate_to" to specify an individual host to execute on:: + + - command: /opt/application/upgrade_db.py + run_once: true + delegate_to: web01.example.org + +When "run_once" is not used with "delegate_to" it will execute on the first host, as defined by inventory, +in the group(s) of hosts targeted by the play. e.g. webservers[0] if the play targeted "hosts: webservers". + +This aproach is similar, although more concise and cleaner than applying a conditional to a task such as:: + + - command: /opt/application/upgrade_db.py + when: inventory_hostname == webservers[0] + .. _local_playbooks: Local Playbooks diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 4188d65d54..74cbeeaa96 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -405,6 +405,7 @@ class PlayBook(object): vault_pass = self.vault_password, run_hosts=hosts, no_log=task.no_log, + run_once=task.run_once, ) runner.module_vars.update({'play_hosts': hosts}) diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 0a7e5515a8..bd9b0d0d12 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -31,7 +31,7 @@ class Task(object): 'local_action', 'transport', 'sudo', 'remote_user', 'sudo_user', 'sudo_pass', 'items_lookup_plugin', 'items_lookup_terms', 'environment', 'args', 'any_errors_fatal', 'changed_when', 'failed_when', 'always_run', 'delay', 'retries', 'until', - 'su', 'su_user', 'su_pass', 'no_log', + 'su', 'su_user', 'su_pass', 'no_log', 'run_once', ] # to prevent typos and such @@ -41,7 +41,7 @@ class Task(object): 'delegate_to', 'local_action', 'transport', 'remote_user', 'sudo', 'sudo_user', 'sudo_pass', 'when', 'connection', 'environment', 'args', 'any_errors_fatal', 'changed_when', 'failed_when', 'always_run', 'delay', 'retries', 'until', - 'su', 'su_user', 'su_pass', 'no_log', + 'su', 'su_user', 'su_pass', 'no_log', 'run_once', ] def __init__(self, play, ds, module_vars=None, default_vars=None, additional_conditions=None, role_name=None): @@ -122,6 +122,7 @@ class Task(object): self.environment = ds.get('environment', {}) self.role_name = role_name self.no_log = utils.boolean(ds.get('no_log', "false")) + self.run_once = utils.boolean(ds.get('run_once', 'false')) #Code to allow do until feature in a Task if 'until' in ds: diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index e810885e3b..585ea37f76 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -144,6 +144,7 @@ class Runner(object): vault_pass=None, run_hosts=None, # an optional list of pre-calculated hosts to run on no_log=False, # option to enable/disable logging for a given task + run_once=False, # option to enable/disable host bypass loop for a given task ): # used to lock multiprocess inputs and outputs at various levels @@ -197,6 +198,7 @@ class Runner(object): self.su_pass = su_pass self.vault_pass = vault_pass self.no_log = no_log + self.run_once = run_once if self.transport == 'smart': # if the transport is 'smart' see if SSH can support ControlPersist if not use paramiko @@ -1200,7 +1202,7 @@ class Runner(object): if self.forks == 0 or self.forks > len(hosts): self.forks = len(hosts) - if p and getattr(p, 'BYPASS_HOST_LOOP', None): + if (p and (getattr(p, 'BYPASS_HOST_LOOP', None)) or self.run_once): # Expose the current hostgroup to the bypassing plugins self.host_set = hosts