1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4"""
5This module is the entrypoint of the `logparse` shell command and also contains
6single-use functions which don't fit elsewhere. All user interaction with
7logparse should be through this module.
8
9This module provides the following methods:
10 - `main()`: Set up arguments, config, logging, and execute parsers
11 - `rotate()`: Rotate logs using systemd logrotate
12 - `rotate_sim()`: Simulate log rotation
13"""
14
15import argparse
16from copy import copy
17import logging
18import logging.handlers
19import os
20from sys import exit, stdin, version
21from subprocess import check_output
22from datetime import datetime
23
24import logparse
25from logparse import formatting, mail, config, load_parsers, util
26
27
28def main():
29 """
30 Initialisation and general management of logparse functionaliy.
31 """
32
33 # Get arguments
34
35 global argparser
36 argparser = get_argparser()
37
38 # Load config
39
40 config.prefs = config.loadconf(argparser.parse_args().config)
41 if argparser.parse_args().time_period:
42 config.prefs.set("logparse", "period",
43 argparser.parse_args().time_period)
44
45 # Set up logging
46 logger = logging.getLogger(__name__)
47 if (argparser.parse_args().quiet
48 or config.prefs.getboolean("logparse", "quiet")):
49 logparse.logger.setLevel(logging.CRITICAL)
50 elif (argparser.parse_args().verbose
51 or config.prefs.getboolean("logparse", "verbose")):
52 logparse.logger.setLevel(logging.DEBUG)
53 logger.debug("Verbose mode turned on")
54 else:
55 logparse.logger.setLevel(logging.INFO)
56
57
58
59 # Time analysis
60
61 global start
62 start = datetime.now()
63 logger.info("Beginning log analysis at {0} {1}".format(
64 start.strftime(formatting.DATEFMT), start.strftime(formatting.TIMEFMT)))
65 logger.debug("This is {0} version {1}, running on Python {2}".format(
66 logparse.__name__, logparse.__version__, version.replace('\n', '')))
67
68 # Write header
69
70 formatting.init_var()
71
72 if argparser.parse_args().plain:
73 output = formatting.PlaintextOutput(
74 linewidth=config.prefs.getint("plain", "linewidth"))
75 output.append_header()
76 else:
77 output = formatting.HtmlOutput()
78 output.append_header(config.prefs.get("html", "header"))
79
80 # Find parsers
81
82 parser_names = []
83 ignore_logs = []
84 if argparser.parse_args().logs:
85 parser_names = set(argparser.parse_args().logs.split())
86 elif config.prefs.get("logparse", "parsers"):
87 parser_names = set(config.prefs.get("logparse", "parsers").split())
88
89 if argparser.parse_args().ignore_logs:
90 ignore_logs = argparser.parse_args().ignore_logs.split()
91 elif config.prefs.get("logparse", "ignore-parsers"):
92 ignore_logs = config.prefs.get("logparse", "ignore-parsers").split()
93
94 # Set up parsers
95
96 loader = load_parsers.ParserLoader()
97
98 try:
99 loader.check_systemd()
100 except Exception as e:
101 logger.error("Failed to check systemd dependencies: ".format(e))
102
103 if parser_names:
104 for parser_name in parser_names:
105 if parser_name not in ignore_logs:
106 loader.search(parser_name)
107 else:
108 loader.load_pkg()
109 if ignore_logs:
110 loader.ignore(ignore_logs)
111
112 # Execute parsers
113
114 executed_parsers = []
115
116 for parser in loader.parsers:
117 if (argparser.parse_args().verbose
118 or config.prefs.getboolean("logparse", "verbose")):
119 output.append_section(parser.parse_log())
120
121 else:
122 try:
123 output.append_section(parser.parse_log())
124 except Exception as e:
125 logger.error("Uncaught error executing logger {0}: {1}".format(
126 parser.name, e))
127 executed_parsers.append(parser.name)
128
129 if len(executed_parsers) == 0:
130 exit()
131
132 # Write footer
133 output.append_footer()
134
135 # Write output
136 if ((argparser.parse_args().destination
137 or config.prefs.get("logparse", "output"))
138 and not argparser.parse_args().no_write):
139
140 # Determine destination path
141 if argparser.parse_args().destination:
142 dest_path = argparser.parse_args().destination
143 else:
144 dest_path = config.prefs.get("logparse", "output")
145
146 logger.debug("Outputting to {0}".format(dest_path))
147
148 # Determine whether to clobber old file
149 if (not os.path.isfile(dest_path)) \
150 and not (argparser.parse_args().overwrite
151 or config.prefs.getboolean("logparse", "overwrite")):
152
153 if (argparser.parse_args().embed_styles
154 or config.prefs.getboolean("html", "embed-styles")) \
155 and not (argparser.parse_args().plain
156 or config.prefs.getboolean("plain", "plain")):
157 # Embed CSS stylesheet
158 output.embed_css(config.prefs.get("html", "css"))
159 output.write_embedded(dest_path)
160 else:
161 output.write(dest_path)
162
163 elif logging.root.level == logging.CRITICAL:
164
165 # Don't write output if running in quiet mode (only stdout)
166 pass
167
168 else:
169
170 logger.warning("Destination file already exists")
171 if input("Would you like to overwrite {0}? (y/n) [n] "
172 .format(dest_path)) == 'y':
173 if (argparser.parse_args().embed_styles
174 or config.prefs.getboolean("html", "embed-styles")) \
175 and not (argparser.parse_args().plain
176 or config.prefs.getboolean("plain", "plain")):
177
178 output.embed_css(config.prefs.get("html", "css"))
179 output.write_embedded(dest_path)
180
181 else:
182 output.write(dest_path)
183 else:
184 logger.warning("No output written")
185
186 # Send email if requested
187
188 if (str(argparser.parse_args().to) or str(config.prefs.get("mail", "to"))) \
189 and not argparser.parse_args().no_mail:
190
191 if str(argparser.parse_args().to):
192 to = argparser.parse_args().to
193 else:
194 to = config.prefs.get("mail", "to")
195
196 mail.sendmail(
197 mailbin=config.prefs.get("mail", "mailbin"),
198 body=(output.embed_css(config.prefs.get("html", "css"))
199 if isinstance(output, formatting.HtmlOutput) else output.content),
200 recipient=to,
201 subject=formatting.fsubject(config.prefs.get("mail", "subject")),
202 html=isinstance(output, formatting.HtmlOutput),
203 sender=config.prefs.get("mail", "from"))
204
205 # Rotate logs if requested
206
207 if not argparser.parse_args().no_rotate:
208 if (argparser.parse_args().simulate
209 or config.prefs.getboolean("logparse", "rotate")):
210 rotate_sim()
211 elif (config.prefs.getboolean("logparse", "rotate")
212 or argparser.parse_args().rotate):
213 rotate()
214 else:
215 logger.debug("User doesn't want to rotate logs")
216 else:
217 logger.debug("User doesn't want to rotate logs")
218
219 # Finish up
220
221 finish = datetime.now()
222 logger.info("Finished parsing logs at {0} {1} (total time: {2})".format(
223 finish.strftime(formatting.DATEFMT),
224 finish.strftime(formatting.TIMEFMT),
225 finish - start))
226
227 if argparser.parse_args().printout:
228 if isinstance(output, formatting.HtmlOutput) \
229 and argparser.parse_args().embed_styles \
230 or config.prefs.getboolean("html", "embed-styles"):
231 output.print_stdout_embedded()
232 else:
233 output.print_stdout()
234
235 return
236
237
238def get_argparser():
239 """
240 Initialise arguments (in a separate function for documentation purposes)
241 """
242
243 argparser = argparse.ArgumentParser(description=
244 'Grab logs of some common services and send them by email')
245 argparser.add_argument('-t','--to', required=False,
246 help='mail recipient (\"to\" address)')
247 argparser.add_argument('-c', '--config', required=False,
248 default="/etc/logparse/logparse.conf",
249 help='path to config file')
250 argparser.add_argument('-p', '--print', required=False, dest='printout',
251 action='store_true', default=False,
252 help='print HTML to stdout')
253 argparser.add_argument('-d', '--destination', required=False,
254 help='file to output HTML')
255 argparser.add_argument('-f', '--overwrite', required=False,
256 action='store_true', default=False,
257 help='force overwrite an existing output file')
258 argparser.add_argument('-v', '--verbose', required=False, default=False,
259 action='store_true',
260 help='verbose console/syslog output (for debugging)')
261 argparser.add_argument('-r', '--rotate', required=False, default=False,
262 action='store_true',
263 help='force rotate log files using systemd logrotate (overrides \
264 --rotate and "rotate" in logparse.conf)')
265 argparser.add_argument('-nr', '--no-rotate', required=False, default=False,
266 action='store_true',
267 help='do not rotate log files (overrides config)')
268 argparser.add_argument('-s', '--simulate', required=False, default=False,
269 action="store_true",
270 help="test run logrotate (do not actually change files)")
271 argparser.add_argument('-l', '--logs', required=False,
272 help='services to analyse')
273 argparser.add_argument('-nl', '--ignore-logs', required=False,
274 help='skip these services (takes precedence over -l)')
275 argparser.add_argument('-es', '--embed-styles', required=False,
276 default=False, action='store_true',
277 help='make CSS rules inline rather than linking the file')
278 argparser.add_argument('-nh', '--plain', required=False, default=False,
279 action='store_true', help='write/send plain text rather than HTML')
280 argparser.add_argument('-q', '--quiet', required=False, default=False,
281 action='store_true', help='no output to stdout')
282 argparser.add_argument('-nm', '--no-mail', required=False, default=False,
283 action="store_true",
284 help="do not send email (overrides config file)")
285 argparser.add_argument('-nw', '--no-write', required=False, default=False,
286 action="store_true",
287 help="do not write output file (overrides config file)")
288 argparser.add_argument('-tp', '--time-period', required=False,
289 help="time period to analyse logs for (applies to all parsers)")
290
291 return argparser
292
293
294
295
296def rotate():
297 """
298 Rotate logs using systemd logrotate. This requires root privileges, and a
299 basic check for this is attempted below. Root password will be prompted
300 for if permissions are not automatically granted.
301 """
302
303 logger = logging.getLogger(__name__)
304 try:
305 if not os.geteuid() == 0:
306 if stdin.isatty():
307 logger.warning("Not running as root, using sudo \
308 (may require password to be entered)")
309 rotate_shell = check_output(
310 "sudo logrotate /etc/logrotate.conf", shell=True)
311 else:
312 raise PermissionError("Root priviliges are required to run \
313 logrotate but were not provided")
314 else:
315 rotate_shell = check_output(
316 "/usr/sbin/logrotate /etc/logrotate.conf", shell=True)
317 logger.info("Rotated logfiles")
318 logger.debug("logrotate output: " + rotate_shell)
319 except Exception as e:
320 logger.warning("Failed to rotate log files: " + str(e))
321
322
323def rotate_sim(): # Simulate log rotation
324 """
325 Simulate log rotation using logrotate's -d flag. This does not require root
326 privileges, but permission errors will be shown in the output without it.
327 """
328
329 logger = logging.getLogger(__name__)
330 try:
331 if not os.geteuid() == 0:
332 logger.warning("Cannot run logrotate as root - \
333 you will see permission errors in the output below")
334 sim_cmd = "logrotate -d /etc/logrotate.conf"
335 logger.debug("Here is the output of `{0}` (simulated):".format(sim_cmd))
336 sim = check_output(sim_cmd, shell=True)
337 logger.debug(sim)
338 except Exception as e:
339 logger.warning("Failed to get logrotate simulation: " + str(e))