Repository: ansible/ansible
Pull request: let meta tasks use tags
##### SUMMARY
Fixes #64558

##### ISSUE TYPE
- Bugfix Pull Request


Problem statement:
Tags doesn't work properly with module meta
##### SUMMARY
Module meta execute every time, even if the action has a different tag specified.

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
meta tasks (end_host, end_play)

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```
ansible 2.8.5
  config file = /root/3-ansible-modul/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Jun 20 2019, 20:27:34) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```
CACHE_PLUGIN(/root/3-ansible-modul/ansible.cfg) = jsonfile
CACHE_PLUGIN_CONNECTION(/root/3-ansible-modul/ansible.cfg) = /tmp/facts_cache
CACHE_PLUGIN_TIMEOUT(/root/3-ansible-modul/ansible.cfg) = 7200
DEFAULT_GATHERING(/root/3-ansible-modul/ansible.cfg) = smart
DEFAULT_HOST_LIST(/root/3-ansible-modul/ansible.cfg) = [u'/root/3-ansible-modul/multinode']
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
vm1_main (ansible host): CentOS Linux release 7.6.1810 (Core)
vm2 and vm3: CentOS Linux release 7.5.1804 (Core)
##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->
Run playbook below with tag "a".
<!--- Paste example playbooks or commands between quotes below -->
```
# cat playbook.yml
---
- hosts: all
  remote_user: root
  gather_facts: yes
  tasks:
  - name: "a1"
    debug:
      msg: "a1"
    tags: a

  - name: "modul"
    debug:
      msg: "modul"
    tags: modul

  - name: "modul - endhost"
    meta: end_host
    tags: modul

  - name: "a2"
    debug:
      msg: "a2"
    tags: a

  - name: "modul2"
    debug:
      msg: "modul2"
    tags: modul
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
Task "a2" should be also executed, because meta:end_host is under different tag.

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->
No matter what meta task i use, it always execute even if its action is specified within different tag.
<!--- Paste verbatim command output between quotes -->
```
# ansible-playbook playbook.yml -t a -vvv
PLAYBOOK: playbook.yml **********************************************************************************************
1 plays in playbook.yml

PLAY [all] **********************************************************************************************************
META: ran handlers

TASK [a1] ***********************************************************************************************************
task path: /root/3-ansible-modul/playbook.yml:7
ok: [vm1_main] => {
    "msg": "a1"
}
ok: [vm2] => {
    "msg": "a1"
}
ok: [vm3] => {
    "msg": "a1"
}
META: ending play for vm1_main
META: ending play for vm2
META: ending play for vm3

PLAY RECAP **********************************************************************************************************
vm1_main                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
vm2                        : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
vm3                        : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

Hints from issue discussion:
Files identified in the description:
* [`lib/ansible/modules/utilities/helper/meta.py`](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/utilities/helper/meta.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
Due to Ansible using meta actions implicitly for internal functionality, they ignore tags as to not cause Ansible issues.

However, we probably could add some logic to tag those implicit meta tasks with always.

We just need to look at this more in depth.
Here is another example:

Consider the following file called `test.yml`:
```
---
- name: test
  hosts: localhost

  tasks:
  - name: add register
    command: ls
    register: ls_result
    tags: test
  - name: test tags
    meta: end_play
    tags: test
    when: ls_result.stdout_lines|length > 0
```
If you attempt to run:
`ansible-playbook test.yml --skip-tags "test"`
it will fail due to:
`The error was: error while evaluating conditional: 'ls_result' is undefined`

While I can work around this issue by changing the `when` to:
```
when:
  - ls_result is defined
  - ls_result.stdout_lines|length > 0
```
I'm sure you can appreciate that this is less than ideal.
we could make the implicit ones work like 'gather_facts' implicit tasks, use same tags as play and default to 'always' otherwise.
Files identified in the description:
* [`lib/ansible/modules/meta.py`](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/meta.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
I just create a fact to check with meta which is created only when the tag is used.
``` 
- set_fact:
      _sec: true
  tags:
    - never
    - sec

- meta: end_play
  when: _sec is true
```

Commit message: Support tags for explicit meta tasks

Add --list-tasks tests for meta tags

changelog

ci_complete

Patch to review:
diff --git a/changelogs/fragments/67508-meta-task-tags.yaml b/changelogs/fragments/67508-meta-task-tags.yaml
new file mode 100644
index 00000000000000..687c538184fcf6
--- /dev/null
+++ b/changelogs/fragments/67508-meta-task-tags.yaml
@@ -0,0 +1,2 @@
+minor_changes:
+  - User-defined ``meta`` tasks now support tags. Internally-created ``meta`` tasks continue to always run regardless of tags. (https://github.com/ansible/ansible/issues/64558)
diff --git a/lib/ansible/cli/playbook.py b/lib/ansible/cli/playbook.py
index 3429a283964ba9..edb19814fc3e87 100644
--- a/lib/ansible/cli/playbook.py
+++ b/lib/ansible/cli/playbook.py
@@ -162,7 +162,7 @@ def _process_block(b):
                                 if isinstance(task, Block):
                                     taskmsg += _process_block(task)
                                 else:
-                                    if task.action == 'meta':
+                                    if task.action == 'meta' and task.implicit is True:
                                         continue
 
                                     all_tags.update(task.tags)
diff --git a/lib/ansible/modules/meta.py b/lib/ansible/modules/meta.py
index 650fdd581dbff2..8da4fe8d38ffce 100644
--- a/lib/ansible/modules/meta.py
+++ b/lib/ansible/modules/meta.py
@@ -40,7 +40,6 @@
     - C(clear_facts) will remove the persistent facts from M(ansible.builtin.set_fact) using C(cacheable=True),
       but not the current host variable it creates for the current run.
     - Looping on meta tasks is not supported.
-    - Skipping C(meta) tasks using tags is not supported.
     - This module is also supported for Windows targets.
 seealso:
 - module: ansible.builtin.assert
diff --git a/lib/ansible/playbook/block.py b/lib/ansible/playbook/block.py
index 5f766e00836aea..cab89dd99251ac 100644
--- a/lib/ansible/playbook/block.py
+++ b/lib/ansible/playbook/block.py
@@ -374,7 +374,7 @@ def evaluate_and_append_task(target):
                     filtered_block = evaluate_block(task)
                     if filtered_block.has_tasks():
                         tmp_list.append(filtered_block)
-                elif (task.action == 'meta' or
+                elif ((task.action == 'meta' and task.implicit) or
                         (task.action == 'include' and task.evaluate_tags([], self._play.skip_tags, all_vars=all_vars)) or
                         task.evaluate_tags(self._play.only_tags, self._play.skip_tags, all_vars=all_vars)):
                     tmp_list.append(task)
diff --git a/test/integration/targets/tags/runme.sh b/test/integration/targets/tags/runme.sh
index 79c51e27efab8f..2984995209ee7c 100755
--- a/test/integration/targets/tags/runme.sh
+++ b/test/integration/targets/tags/runme.sh
@@ -14,7 +14,7 @@ export LC_ALL=en_US.UTF-8
 
 # Run everything by default
 [ "$("${COMMAND[@]}" | grep -F Task_with | xargs)" = \
-"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [くらとみ] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3]" ]
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [くらとみ] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3] Task_with_meta_tags TAGS: [meta_tag]" ]
 
 # Run the exact tags, and always
 [ "$("${COMMAND[@]}" --tags tag | grep -F Task_with | xargs)" = \
@@ -22,11 +22,15 @@ export LC_ALL=en_US.UTF-8
 
 # Skip one tag
 [ "$("${COMMAND[@]}" --skip-tags tag | grep -F Task_with | xargs)" = \
-"Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [くらとみ] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3]" ]
+"Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [くらとみ] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3] Task_with_meta_tags TAGS: [meta_tag]" ]
 
 # Skip a unicode tag
 [ "$("${COMMAND[@]}" --skip-tags 'くらとみ' | grep -F Task_with | xargs)" = \
-"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3]" ]
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3] Task_with_meta_tags TAGS: [meta_tag]" ]
+
+# Skip a meta task tag
+[ "$("${COMMAND[@]}" --skip-tags meta_tag | grep -F Task_with | xargs)" = \
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [くらとみ] Task_with_list_of_tags TAGS: [café, press] Task_without_tag TAGS: [] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3]" ]
 
 # Run just a unicode tag and always
 [ "$("${COMMAND[@]}" --tags 'くらとみ' | grep -F Task_with | xargs)" = \
@@ -48,9 +52,13 @@ export LC_ALL=en_US.UTF-8
 [ "$("${COMMAND[@]}" --tags tag3 | grep -F Task_with | xargs)" = \
 "Task_with_always_tag TAGS: [always] Task_with_templated_tags TAGS: [tag3]" ]
 
+# Run meta tags
+[ "$("${COMMAND[@]}" --tags meta_tag | grep -F Task_with | xargs)" = \
+"Task_with_always_tag TAGS: [always] Task_with_meta_tags TAGS: [meta_tag]" ]
+
 # Run tagged
 [ "$("${COMMAND[@]}" --tags tagged | grep -F Task_with | xargs)" = \
-"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [くらとみ] Task_with_list_of_tags TAGS: [café, press] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3]" ]
+"Task_with_tag TAGS: [tag] Task_with_always_tag TAGS: [always] Task_with_unicode_tag TAGS: [くらとみ] Task_with_list_of_tags TAGS: [café, press] Task_with_csv_tags TAGS: [tag1, tag2] Task_with_templated_tags TAGS: [tag3] Task_with_meta_tags TAGS: [meta_tag]" ]
 
 # Run untagged
 [ "$("${COMMAND[@]}" --tags untagged | grep -F Task_with | xargs)" = \
diff --git a/test/integration/targets/tags/test_tags.yml b/test/integration/targets/tags/test_tags.yml
index 76ac5ba4bd46f1..f0f9a72d73d03c 100644
--- a/test/integration/targets/tags/test_tags.yml
+++ b/test/integration/targets/tags/test_tags.yml
@@ -31,3 +31,6 @@
     - name: Task_with_templated_tags
       debug: msg=templated
       tags: "{{ the_tags }}"
+    - name: Task_with_meta_tags
+      meta: reset_connection
+      tags: meta_tag


Code review comments:
diff_hunk: @@ -162,7 +162,7 @@ def _process_block(b):
                                 if isinstance(task, Block):
                                     taskmsg += _process_block(task)
                                 else:
-                                    if task.action == 'meta':
+                                    if task.action == 'meta' and task.implicit is True:
path: lib/ansible/cli/playbook.py
text: @author:
```suggestion
                                    if task.action == 'meta' and task.implicit:
```

diff_hunk: @@ -162,7 +162,7 @@ def _process_block(b):
                                 if isinstance(task, Block):
                                     taskmsg += _process_block(task)
                                 else:
-                                    if task.action == 'meta':
+                                    if task.action == 'meta' and task.implicit is True:
path: lib/ansible/cli/playbook.py
text: @user1:
Just curious, why the strict `is True` check here?

@author:
dunno, oversight during many edits