From 9c71f176f3d2e76109c3d5f29b3d1ce1671abb8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn?= <dev@bjoern.mosler.ch>
Date: Tue, 20 Nov 2018 20:19:17 +0100
Subject: [PATCH] =?UTF-8?q?Make=20wait=5Ffor=20return=20matched=20groups?=
 =?UTF-8?q?=20defined=20in=20search=5Fregex.=20Closes=20#=E2=80=A6=20(#476?=
 =?UTF-8?q?90)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Make wait_for return matched groups defined in search_regex. Closes #25020.

* Fix formatting issues.

* Fix issues raised in review.

- Use output_dir instead of hardcoded /tmp for temp files
- Sleep for only 3s instead of 10s
- Revert indent change
---
 .../modules/utilities/logic/wait_for.py       | 37 ++++++++++-
 .../targets/wait_for/tasks/main.yml           | 61 +++++++++++++------
 2 files changed, 76 insertions(+), 22 deletions(-)

diff --git a/lib/ansible/modules/utilities/logic/wait_for.py b/lib/ansible/modules/utilities/logic/wait_for.py
index 3111cc3db97..6b9e3508e6b 100644
--- a/lib/ansible/modules/utilities/logic/wait_for.py
+++ b/lib/ansible/modules/utilities/logic/wait_for.py
@@ -134,6 +134,14 @@ EXAMPLES = r'''
     path: /tmp/foo
     search_regex: completed
 
+- name: Wait until regex pattern matches in the file /tmp/foo and print the matched group
+  wait_for:
+    path: /tmp/foo
+    search_regex: completed (?P<task>\w+)
+  register: waitfor
+- debug:
+    msg: Completed {{ waitfor['groupdict']['task'] }}
+
 - name: Wait until the lock file is removed
   wait_for:
     path: /var/lock/file.lock
@@ -176,6 +184,20 @@ elapsed:
   returned: always
   type: int
   sample: 23
+match_groups:
+  description: Tuple containing all the subgroups of the match as returned by U(https://docs.python.org/2/library/re.html#re.MatchObject.groups)
+  returned: always
+  type: list
+  sample: ['match 1', 'match 2']
+match_groupdict:
+  description: Dictionary containing all the named subgroups of the match, keyed by the subgroup name,
+    as returned by U(https://docs.python.org/2/library/re.html#re.MatchObject.groupdict)
+  returned: always
+  type: dict
+  sample:
+    {
+      'group': 'match'
+    }
 '''
 
 import binascii
@@ -468,6 +490,9 @@ def main():
     else:
         compiled_search_re = None
 
+    match_groupdict = {}
+    match_groups = ()
+
     if port and path:
         module.fail_json(msg="port and path parameter can not both be passed to wait_for", elapsed=0)
     if path and state == 'stopped':
@@ -537,8 +562,13 @@ def main():
                     try:
                         f = open(path)
                         try:
-                            if re.search(compiled_search_re, f.read()):
-                                # String found, success!
+                            search = re.search(compiled_search_re, f.read())
+                            if search:
+                                if search.groupdict():
+                                    match_groupdict = search.groupdict()
+                                if search.groups():
+                                    match_groups = search.groups()
+
                                 break
                         finally:
                             f.close()
@@ -630,7 +660,8 @@ def main():
             module.fail_json(msg=msg or "Timeout when waiting for %s:%s to drain" % (host, port), elapsed=elapsed.seconds)
 
     elapsed = datetime.datetime.utcnow() - start
-    module.exit_json(state=state, port=port, search_regex=search_regex, path=path, elapsed=elapsed.seconds)
+    module.exit_json(state=state, port=port, search_regex=search_regex, match_groups=match_groups, match_groupdict=match_groupdict, path=path,
+                     elapsed=elapsed.seconds)
 
 
 if __name__ == '__main__':
diff --git a/test/integration/targets/wait_for/tasks/main.yml b/test/integration/targets/wait_for/tasks/main.yml
index 908d2d8d039..4d1c9f319df 100644
--- a/test/integration/targets/wait_for/tasks/main.yml
+++ b/test/integration/targets/wait_for/tasks/main.yml
@@ -11,17 +11,17 @@
 
 - name: setup a path
   file:
-    path: /tmp/wait_for_file
+    path: "{{ output_dir }}/wait_for_file"
     state: touch
 
-- name: setup remove a file after 10s
-  shell: sleep 10 && rm /tmp/wait_for_file
+- name: setup remove a file after 3s
+  shell: sleep 3 && rm {{ output_dir }}/wait_for_file
   async: 20
   poll: 0
 
 - name: test for absent path
   wait_for:
-    path: /tmp/wait_for_file
+    path: "{{ output_dir }}/wait_for_file"
     state: absent
     timeout: 20
   register: waitfor
@@ -29,47 +29,70 @@
   assert:
     that:
       - waitfor is successful
-      - "waitfor.path == '/tmp/wait_for_file'"
-      - waitfor.elapsed >= 5
+      - waitfor.path == "{{ output_dir | expanduser }}/wait_for_file"
+      - waitfor.elapsed >= 2
       - waitfor.elapsed <= 15
 
-- name: setup create a file after 10s
-  shell: sleep 10 && touch /tmp/wait_for_file
+- name: setup create a file after 3s
+  shell: sleep 3 && touch {{ output_dir }}/wait_for_file
   async: 20
   poll: 0
 
 - name: test for present path
   wait_for:
-    path: /tmp/wait_for_file
-    timeout: 20
+    path:  "{{ output_dir }}/wait_for_file"
+    timeout: 5
   register: waitfor
 - name: verify test for absent path
   assert:
     that:
       - waitfor is successful
-      - "waitfor.path == '/tmp/wait_for_file'"
-      - waitfor.elapsed >= 5
+      - waitfor.path == "{{ output_dir | expanduser }}/wait_for_file"
+      - waitfor.elapsed >= 2
       - waitfor.elapsed <= 15
 
-- name: setup write keyword to file after 10s
-  shell: rm -f /tmp/wait_for_keyword && sleep 10 && echo completed > /tmp/wait_for_keyword
+- name: setup write keyword to file after 3s
+  shell: sleep 3 && echo completed > {{output_dir}}/wait_for_keyword
   async: 20
   poll: 0
 
 - name: test wait for keyword in file
   wait_for:
-    path: /tmp/wait_for_keyword
+    path: "{{output_dir}}/wait_for_keyword"
     search_regex: completed
-    timeout: 20
+    timeout: 5
   register: waitfor
-- name: verify test wait for port timeout
+
+- name: verify test wait for keyword in file
   assert:
     that:
       - waitfor is successful
       - "waitfor.search_regex == 'completed'"
-      - waitfor.elapsed >= 5
+      - waitfor.elapsed >= 2
       - waitfor.elapsed <= 15
 
+- name: setup write keyword to file after 3s
+  shell: sleep 3 && echo "completed data 123" > {{output_dir}}/wait_for_keyword
+  async: 20
+  poll: 0
+
+- name: test wait for keyword in file with match groups
+  wait_for:
+    path: "{{output_dir}}/wait_for_keyword"
+    search_regex: completed (?P<foo>\w+) ([0-9]+)
+    timeout: 5
+  register: waitfor
+
+- name: verify test wait for keyword in file with match groups
+  assert:
+    that:
+    - waitfor is successful
+    - waitfor.elapsed >= 2
+    - waitfor.elapsed <= 15
+    - waitfor['match_groupdict'] | length == 1
+    - waitfor['match_groupdict']['foo'] == 'data'
+    - waitfor['match_groups'] == ['data', '123']
+
 - name: test wait for port timeout
   wait_for:
     port: 12121
@@ -98,7 +121,7 @@
       - "waitfor.msg == 'fail with custom message'"
 
 - name: setup start SimpleHTTPServer
-  shell: sleep 10 && cd {{ files_dir }} && {{ ansible_python.executable }} {{ output_dir}}/testserver.py {{ http_port }}
+  shell: sleep 3 && cd {{ files_dir }} && {{ ansible_python.executable }} {{ output_dir}}/testserver.py {{ http_port }}
   async: 120 # this test set can take ~1m to run on FreeBSD (via Shippable)
   poll: 0