Skip to content

Commit fa9affa

Browse files
committed
add new module
1 parent 8fca7cf commit fa9affa

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed

plugins/modules/tail_grep.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
# Copyright (c) 2024, Red Hat Inc.
4+
# Copyright (c) 2024, Guido Grazioli <ggraziol@redhat.com>
5+
# Apache License, Version 2.0 (see LICENSE or https://www.apache.org/licenses/LICENSE-2.0)
6+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7+
8+
from __future__ import annotations
9+
10+
11+
DOCUMENTATION = r'''
12+
---
13+
module: tail_grep
14+
version_added: 1.2.2
15+
short_description: Tail a logfile until a regex matcher is found or a timeout triggers
16+
description:
17+
- This module is used to follow some application logfile and return successfully when
18+
a search string or regex is found; otherwise fail after a timeout.
19+
options:
20+
path:
21+
description:
22+
- The file on the remote system to tail.
23+
type: path
24+
required: true
25+
regex:
26+
description:
27+
- The string or regular expression to search in the file.
28+
type: str
29+
required: true
30+
timeout:
31+
description:
32+
- After how many seconds to exit unsuccessfully without having found the search regex.
33+
required: false
34+
type: int
35+
default: 60
36+
from_regex:
37+
description:
38+
- Backwards from end of file, lines preceeding this string will not be considered for
39+
matching regex. By default, the whole file is read. If `$` is used, start from the
40+
first line written after the file is opened.
41+
required: false
42+
type: str
43+
default: ''
44+
delay:
45+
description:
46+
- How many seconds to wait after opening the file before starting to look for the regex.
47+
required: false
48+
type: int
49+
default: 0
50+
extends_documentation_fragment:
51+
- action_common_attributes
52+
attributes:
53+
check_mode:
54+
support: none
55+
diff_mode:
56+
support: none
57+
platform:
58+
platforms: posix
59+
author:
60+
- Guido Grazioli (@guidograzioli)
61+
'''
62+
63+
EXAMPLES = r'''
64+
- name: Tail activemq log until the successful start status code is found
65+
ansible.builtin.tail_grep:
66+
path: /var/log/activemq/artemis.log
67+
regex: AMQ220010
68+
'''
69+
70+
RETURN = r'''
71+
content:
72+
description: The full string that was matched by the search regex
73+
returned: success
74+
type: str
75+
sample: "Application has been started successfully."
76+
source:
77+
description: Actual path of file opened
78+
returned: success
79+
type: str
80+
sample: "/var/log/messages"
81+
'''
82+
83+
import errno
84+
import re
85+
import time
86+
87+
from typing import Iterator
88+
from ansible.module_utils.basic import AnsibleModule
89+
from ansible.module_utils.common.text.converters import to_native
90+
91+
92+
def amq_argument_spec():
93+
"""
94+
Returns argument_spec of options
95+
96+
:return: argument_spec dict
97+
"""
98+
return dict(
99+
path=dict(type='path', required=True),
100+
regex=dict(type='str', required=True),
101+
timeout=dict(type='int', required=False, default=60),
102+
from_regex=dict(type='str', required=False, default=''),
103+
delay=dict(type='int', required=False, default=0)
104+
)
105+
106+
107+
# from https://stackoverflow.com/a/54263201/389099
108+
def follow(file, sleep_sec=0.1):
109+
""" Yield each line from a file as they are written.
110+
`sleep_sec` is the time to sleep after empty reads. """
111+
line = ''
112+
while True:
113+
tmp = file.readline()
114+
if tmp is not None and tmp != "":
115+
line += tmp
116+
if line.endswith("\n"):
117+
yield line
118+
line = ''
119+
elif sleep_sec:
120+
time.sleep(sleep_sec)
121+
122+
123+
def main():
124+
module = AnsibleModule(argument_spec=amq_argument_spec(), supports_check_mode=False)
125+
126+
source = module.params['path']
127+
128+
try:
129+
with open(source, 'r') as source_fh:
130+
ts = time.time()
131+
for line in follow(file, 0.25):
132+
if re.match(regex, line):
133+
break
134+
if (time.time() >= ts + timeout):
135+
msg = "timeout reached without finding search string in file"
136+
module.fail_json(msg)
137+
except (IOError, OSError) as e:
138+
if e.errno == errno.ENOENT:
139+
msg = "file not found: %s" % source
140+
elif e.errno == errno.EACCES:
141+
msg = "file is not readable: %s" % source
142+
elif e.errno == errno.EISDIR:
143+
msg = "source is a directory and must be a file: %s" % source
144+
else:
145+
msg = "unable to slurp file: %s" % to_native(e, errors='surrogate_then_replace')
146+
147+
module.fail_json(msg)
148+
149+
module.exit_json(content=line, source=source)
150+
151+
152+
if __name__ == '__main__':
153+
main()

0 commit comments

Comments
 (0)