From 95fa770ba0d9b59b1877974bcae98b7606b3b19d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 8 Feb 2015 22:51:18 -0500 Subject: [PATCH 1/6] adds complex tag management Adds a special tag: - always: always runs no matter what --tags, --skip-tags say Adds 4 special keywords for --tags/skip-tags - all: all tagged + untagged tasks - tagged: only tagged tasks - untagged: only untagged tasks - always: only run tasks tagged 'always' --- lib/ansible/playbook/__init__.py | 45 ++++++++++++++++++++++++-------- lib/ansible/playbook/play.py | 23 ++++++++++++++-- lib/ansible/playbook/task.py | 5 +++- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 7f4fd8cb1f..5402ee530e 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -36,6 +36,7 @@ import pipes # holds all other variables about a host SETUP_CACHE = ansible.cache.FactCache() VARS_CACHE = collections.defaultdict(dict) +RESERVED_TAGS = ['all','tagged','untagged','always'] class PlayBook(object): @@ -314,6 +315,7 @@ class PlayBook(object): assert play is not None matched_tags, unmatched_tags = play.compare_tags(self.only_tags) + matched_tags_all = matched_tags_all | matched_tags unmatched_tags_all = unmatched_tags_all | unmatched_tags @@ -332,10 +334,13 @@ class PlayBook(object): # the user can correct the arguments. unknown_tags = ((set(self.only_tags) | set(self.skip_tags)) - (matched_tags_all | unmatched_tags_all)) - unknown_tags.discard('all') + + for t in RESERVED_TAGS: + unknown_tags.discard(t) if len(unknown_tags) > 0: - unmatched_tags_all.discard('all') + for t in RESERVED_TAGS: + unmatched_tags_all.discard(t) msg = 'tag(s) not found in playbook: %s. possible values: %s' unknown = ','.join(sorted(unknown_tags)) unmatched = ','.join(sorted(unmatched_tags_all)) @@ -730,19 +735,37 @@ class PlayBook(object): # skip calling the handler till the play is finished continue - # only run the task if the requested tags match + # only run the task if the requested tags match or has 'always' tag should_run = False - for x in self.only_tags: + if 'always' in task.tags: + should_run = True + else: + u = set(['untagged']) + task_set = set(task.tags) - for y in task.tags: - if x == y: + if 'all' in self.only_tags: + should_run = True + elif 'tagged' in self.only_tags: + if task_set != u: + should_run = True + elif 'untagged' in self.only_tags: + if task_set == u: + should_run = True + else: + if len(set(task.tags).intersection(self.only_tags)) > 0: should_run = True - break - # Check for tags that we need to skip - if should_run: - if any(x in task.tags for x in self.skip_tags): - should_run = False + # Check for tags that we need to skip + if 'tagged' in self.skip_tags: + if task_set == u: + should_run = False + elif 'untagged' in self.only_tags: + if task_set != u: + should_run = False + else: + if should_run: + if len(set(task.tags).intersection(self.skip_tags)) > 0: + should_run = False if should_run: diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py index 74aa6a9f79..f1b3aacfad 100644 --- a/lib/ansible/playbook/play.py +++ b/lib/ansible/playbook/play.py @@ -783,8 +783,27 @@ class Play(object): # compare the lists of tags using sets and return the matched and unmatched all_tags_set = set(all_tags) tags_set = set(tags) - matched_tags = all_tags_set & tags_set - unmatched_tags = all_tags_set - tags_set + + matched_tags = all_tags_set.intersection(tags_set) + unmatched_tags = all_tags_set.difference(tags_set) + + a = set(['always']) + u = set(['untagged']) + if 'always' in all_tags_set: + matched_tags = matched_tags.union(a) + unmatched_tags = all_tags_set.difference(a) + + if 'all' in tags_set: + matched_tags = matched_tags.union(all_tags_set) + unmatched_tags = set() + + if 'tagged' in tags_set: + matched_tags = all_tags_set.difference(u) + unmatched_tags = u + + if 'untagged' in tags_set and 'untagged' in all_tags_set: + matched_tags = matched_tags.union(u) + unmatched_tags = unmatched_tags.difference(u) return matched_tags, unmatched_tags diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index bdffba5527..f9a090b363 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -129,7 +129,7 @@ class Task(object): # load various attributes self.name = ds.get('name', None) - self.tags = [ 'all' ] + self.tags = [ 'untagged' ] self.register = ds.get('register', None) self.sudo = utils.boolean(ds.get('sudo', play.sudo)) self.su = utils.boolean(ds.get('su', play.su)) @@ -316,6 +316,9 @@ class Task(object): self.tags.extend(apply_tags) self.tags.extend(import_tags) + if len(self.tags) > 1: + self.tags.remove('untagged') + if additional_conditions: new_conditions = additional_conditions[:] if self.when: From 6579c5ddf5037569f4f009090cea2eca11b324fc Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 8 Feb 2015 23:06:34 -0500 Subject: [PATCH 2/6] fixed skipped tags --- lib/ansible/playbook/__init__.py | 38 ++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 5402ee530e..1e717e6f27 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -736,7 +736,6 @@ class PlayBook(object): continue # only run the task if the requested tags match or has 'always' tag - should_run = False if 'always' in task.tags: should_run = True else: @@ -745,27 +744,32 @@ class PlayBook(object): if 'all' in self.only_tags: should_run = True - elif 'tagged' in self.only_tags: - if task_set != u: - should_run = True - elif 'untagged' in self.only_tags: - if task_set == u: - should_run = True else: - if len(set(task.tags).intersection(self.only_tags)) > 0: - should_run = True + should_run = False + if 'tagged' in self.only_tags: + if task_set != u: + should_run = True + elif 'untagged' in self.only_tags: + if task_set == u: + should_run = True + else: + if len(task_set.intersection(self.only_tags)) > 0: + should_run = True # Check for tags that we need to skip - if 'tagged' in self.skip_tags: - if task_set == u: - should_run = False - elif 'untagged' in self.only_tags: - if task_set != u: - should_run = False + if 'all' in self.skip_tags: + should_run = False else: - if should_run: - if len(set(task.tags).intersection(self.skip_tags)) > 0: + if 'tagged' in self.skip_tags: + if task_set != u: should_run = False + elif 'untagged' in self.skip_tags: + if task_set == u: + should_run = False + else: + if should_run: + if len(task_set.intersection(self.skip_tags)) > 0: + should_run = False if should_run: From 6fbbf9c118a9309a29060ca6711abc1eda001f8d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 9 Feb 2015 11:59:50 -0500 Subject: [PATCH 3/6] simplified if conditions --- lib/ansible/playbook/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 1e717e6f27..a8ec9b8a45 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -753,7 +753,7 @@ class PlayBook(object): if task_set == u: should_run = True else: - if len(task_set.intersection(self.only_tags)) > 0: + if task_set.intersection(self.only_tags): should_run = True # Check for tags that we need to skip @@ -768,7 +768,7 @@ class PlayBook(object): should_run = False else: if should_run: - if len(task_set.intersection(self.skip_tags)) > 0: + if task_set.intersection(self.skip_tags): should_run = False if should_run: From 65c08d1364dd97e784f8a853dda10fbf41542399 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 12 Feb 2015 20:58:30 -0500 Subject: [PATCH 4/6] added tests refactored task selection into method list-tasks now uses common method of task selection always is now skippable if specified in --skip-tags --- bin/ansible-playbook | 26 ++------- lib/ansible/playbook/__init__.py | 96 +++++++++++++++++--------------- test/integration/Makefile | 11 +++- test/integration/test_tags.yml | 15 +++++ 4 files changed, 81 insertions(+), 67 deletions(-) create mode 100644 test/integration/test_tags.yml diff --git a/bin/ansible-playbook b/bin/ansible-playbook index a3b2958179..2a692a2e3d 100755 --- a/bin/ansible-playbook +++ b/bin/ansible-playbook @@ -227,22 +227,6 @@ def main(args): label = play.name hosts = pb.inventory.list_hosts(play.hosts) - # Filter all tasks by given tags - if pb.only_tags != 'all': - if options.subset and not hosts: - continue - matched_tags, unmatched_tags = play.compare_tags(pb.only_tags) - - # Remove skipped tasks - matched_tags = matched_tags - set(pb.skip_tags) - - unmatched_tags.discard('all') - unknown_tags = ((set(pb.only_tags) | set(pb.skip_tags)) - - (matched_tags | unmatched_tags)) - - if unknown_tags: - continue - if options.listhosts: print ' play #%d (%s): host count=%d' % (playnum, label, len(hosts)) for host in hosts: @@ -251,12 +235,10 @@ def main(args): if options.listtasks: print ' play #%d (%s):' % (playnum, label) - for task in play.tasks(): - if (set(task.tags).intersection(pb.only_tags) and not - set(task.tags).intersection(pb.skip_tags)): - if getattr(task, 'name', None) is not None: - # meta tasks have no names - print ' %s' % task.name + for task in pb.tasks_to_run_in_play(play): + if getattr(task, 'name', None) is not None: + # meta tasks have no names + print ' %s' % task.name if options.listhosts or options.listtasks: print '' continue diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index a8ec9b8a45..b4030565b5 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -672,7 +672,53 @@ class PlayBook(object): return filename # ***************************************************** + def tasks_to_run_in_play(self, play): + tasks = [] + + for task in play.tasks(): + # only run the task if the requested tags match or has 'always' tag + u = set(['untagged']) + task_set = set(task.tags) + + if 'always' in task.tags: + should_run = True + else: + if 'all' in self.only_tags: + should_run = True + else: + should_run = False + if 'tagged' in self.only_tags: + if task_set != u: + should_run = True + elif 'untagged' in self.only_tags: + if task_set == u: + should_run = True + else: + if task_set.intersection(self.only_tags): + should_run = True + + # Check for tags that we need to skip + if 'all' in self.skip_tags: + should_run = False + else: + if 'tagged' in self.skip_tags: + if task_set != u: + should_run = False + elif 'untagged' in self.skip_tags: + if task_set == u: + should_run = False + else: + if should_run: + if task_set.intersection(self.skip_tags): + should_run = False + + if should_run: + tasks.append(task) + + return tasks + + # ***************************************************** def _run_play(self, play): ''' run a list of tasks for a given pattern, in order ''' @@ -725,7 +771,7 @@ class PlayBook(object): play._play_hosts = self._trim_unavailable_hosts(on_hosts) self.inventory.also_restrict_to(on_hosts) - for task in play.tasks(): + for task in self.tasks_to_run_in_play(play): if task.meta is not None: # meta tasks can force handlers to run mid-play @@ -735,49 +781,11 @@ class PlayBook(object): # skip calling the handler till the play is finished continue - # only run the task if the requested tags match or has 'always' tag - if 'always' in task.tags: - should_run = True - else: - u = set(['untagged']) - task_set = set(task.tags) - - if 'all' in self.only_tags: - should_run = True - else: - should_run = False - if 'tagged' in self.only_tags: - if task_set != u: - should_run = True - elif 'untagged' in self.only_tags: - if task_set == u: - should_run = True - else: - if task_set.intersection(self.only_tags): - should_run = True - - # Check for tags that we need to skip - if 'all' in self.skip_tags: - should_run = False - else: - if 'tagged' in self.skip_tags: - if task_set != u: - should_run = False - elif 'untagged' in self.skip_tags: - if task_set == u: - should_run = False - else: - if should_run: - if task_set.intersection(self.skip_tags): - should_run = False - - if should_run: - - if not self._run_task(play, task, False): - # whether no hosts matched is fatal or not depends if it was on the initial step. - # if we got exactly no hosts on the first step (setup!) then the host group - # just didn't match anything and that's ok - return False + if not self._run_task(play, task, False): + # whether no hosts matched is fatal or not depends if it was on the initial step. + # if we got exactly no hosts on the first step (setup!) then the host group + # just didn't match anything and that's ok + return False # Get a new list of what hosts are left as available, the ones that # did not go fail/dark during the task diff --git a/test/integration/Makefile b/test/integration/Makefile index c708e08e40..c533e06b15 100644 --- a/test/integration/Makefile +++ b/test/integration/Makefile @@ -21,7 +21,7 @@ VAULT_PASSWORD_FILE = vault-password CONSUL_RUNNING := $(shell python consul_running.py) -all: parsing test_var_precedence unicode non_destructive destructive includes check_mode test_hash test_handlers test_group_by test_vault +all: parsing test_var_precedence unicode non_destructive destructive includes check_mode test_hash test_handlers test_group_by test_vault test_tags parsing: ansible-playbook bad_parsing.yml -i $(INVENTORY) -e @$(VARS_FILE) $(CREDENTIALS_ARG) -vvv $(TEST_FLAGS) --tags prepare,common,scenario1; [ $$? -eq 3 ] @@ -82,6 +82,15 @@ test_delegate_to: test_winrm: ansible-playbook test_winrm.yml -i inventory.winrm -e @$(VARS_FILE) $(CREDENTIALS_ARG) -v $(TEST_FLAGS) +test_tags: + # Run everything by default + [ "$$(ansible-playbook --list-tasks test_tags.yml -i $(INVENTORY) -e @$(VARS_FILE) $(CREDENTIALS_ARG) -v $(TEST_FLAGS) | fgrep Task_with | xargs)" = "Task_with_tag Task_with_always_tag Task_without_tag" ] + # Run the exact tags, and always + [ "$$(ansible-playbook --list-tasks --tags tag test_tags.yml -i $(INVENTORY) -e @$(VARS_FILE) $(CREDENTIALS_ARG) -v $(TEST_FLAGS) | fgrep Task_with | xargs)" = "Task_with_tag Task_with_always_tag" ] + # Skip one tag + [ "$$(ansible-playbook --list-tasks --skip-tags tag test_tags.yml -i $(INVENTORY) -e @$(VARS_FILE) $(CREDENTIALS_ARG) -v $(TEST_FLAGS) | fgrep Task_with | xargs)" = "Task_with_always_tag Task_without_tag" ] + + cloud: amazon rackspace cloud_cleanup: amazon_cleanup rackspace_cleanup diff --git a/test/integration/test_tags.yml b/test/integration/test_tags.yml new file mode 100644 index 0000000000..650c4725ae --- /dev/null +++ b/test/integration/test_tags.yml @@ -0,0 +1,15 @@ +--- +- name: verify tags work as expected + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Task_with_tag + debug: msg= + tags: tag + - name: Task_with_always_tag + debug: msg= + tags: always + - name: Task_without_tag + debug: msg= + From af573db47a11586197694125d9168fa54920579a Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 12 Feb 2015 21:15:44 -0500 Subject: [PATCH 5/6] documented new tag behaviour --- docsite/rst/playbooks_tags.rst | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/docsite/rst/playbooks_tags.rst b/docsite/rst/playbooks_tags.rst index 308d451c88..01c4f6fa2b 100644 --- a/docsite/rst/playbooks_tags.rst +++ b/docsite/rst/playbooks_tags.rst @@ -1,8 +1,8 @@ Tags ==== -If you have a large playbook it may become useful to be able to run a -specific part of the configuration without running the whole playbook. +If you have a large playbook it may become useful to be able to run a +specific part of the configuration without running the whole playbook. Both plays and tasks support a "tags:" attribute for this reason. @@ -24,7 +24,7 @@ Example:: If you wanted to just run the "configuration" and "packages" part of a very long playbook, you could do this:: ansible-playbook example.yml --tags "configuration,packages" - + On the other hand, if you want to run a playbook *without* certain tasks, you could do this:: ansible-playbook example.yml --skip-tags "notification" @@ -40,6 +40,28 @@ And you may also tag basic include statements:: Both of these have the function of tagging every single task inside the include statement. + +Special Tags +```````````` + +There is a special 'always' tag that will always run a task, unless specifically skipped (--skip-tags always) + +Example:: + + tasks: + + - debug: msg="Always runs" + tags: + - always + + - debug: msg="runs when you use tag1" + tags: + - tag1 + +There are another 3 special keywords for tags, 'tagged', 'untagged' and 'all', which run only tagged, only untagged +and all tasks respectively. By default ansible runs as if --tags all had been specified. + + .. seealso:: :doc:`playbooks` From f6a6df2169ed92ca27e105b70c747c3fae70a5bb Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 13 Feb 2015 08:27:35 -0500 Subject: [PATCH 6/6] removed unused method --- lib/ansible/playbook/play.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py index f1b3aacfad..b473423c3a 100644 --- a/lib/ansible/playbook/play.py +++ b/lib/ansible/playbook/play.py @@ -669,20 +669,6 @@ class Play(object): # ************************************************* - def _is_valid_tag(self, tag_list): - """ - Check to see if the list of tags passed in is in the list of tags - we only want (playbook.only_tags), or if it is not in the list of - tags we don't want (playbook.skip_tags). - """ - matched_skip_tags = set(tag_list) & set(self.playbook.skip_tags) - matched_only_tags = set(tag_list) & set(self.playbook.only_tags) - if len(matched_skip_tags) > 0 or (self.playbook.only_tags != ['all'] and len(matched_only_tags) == 0): - return False - return True - - # ************************************************* - def tasks(self): ''' return task objects for this play ''' return self._tasks