mirror of
				https://github.com/mail-in-a-box/mailinabox.git
				synced 2025-10-30 18:50:53 +00:00 
			
		
		
		
	setup: add a setup mod to attach a logwatch report to daily status checks emails
This commit is contained in:
		
							parent
							
								
									93f7a84f44
								
							
						
					
					
						commit
						2ac391796e
					
				| @ -961,6 +961,16 @@ def run_and_output_changes(env, pool): | ||||
| 	if os.path.exists(cache_fn): | ||||
| 		prev = json.load(open(cache_fn)) | ||||
| 
 | ||||
| 		# execute hooks | ||||
| 		hook_data = { | ||||
| 			'op':'output_changes_begin', | ||||
| 			'env':env, | ||||
| 			'cur': cur, | ||||
| 			'prev': prev, | ||||
| 			'output':out | ||||
| 		} | ||||
| 		hooks.exec_hooks('status_checks', hook_data) | ||||
| 
 | ||||
| 		# Group the serial output into categories by the headings. | ||||
| 		def group_by_heading(lines): | ||||
| 			from collections import OrderedDict | ||||
| @ -1051,7 +1061,10 @@ class FileOutput: | ||||
| 		self.print_block(message, first_line="✖  ") | ||||
| 
 | ||||
| 	def print_warning(self, message): | ||||
| 		self.print_block(message, first_line="?  ") | ||||
| 		self.print_block(message, first_line="⚠  ") | ||||
| 
 | ||||
| 	def print_info(self, message): | ||||
| 		self.print_block(message, first_line="ℹ  ") | ||||
| 
 | ||||
| 	def print_block(self, message, first_line="   "): | ||||
| 		print(first_line, end='', file=self.buf) | ||||
| @ -1097,7 +1110,7 @@ class BufferedOutput: | ||||
| 	def __init__(self, with_lines=None): | ||||
| 		self.buf = [] if not with_lines else with_lines | ||||
| 	def __getattr__(self, attr): | ||||
| 		if attr not in ("add_heading", "print_ok", "print_error", "print_warning", "print_block", "print_line"): | ||||
| 		if attr not in ("add_heading", "print_ok", "print_error", "print_warning", "print_info", "print_block", "print_line"): | ||||
| 			raise AttributeError | ||||
| 		# Return a function that just records the call & arguments to our buffer. | ||||
| 		def w(*args, **kwargs): | ||||
|  | ||||
							
								
								
									
										114
									
								
								setup/mods.available/hooks/logwatch-hooks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								setup/mods.available/hooks/logwatch-hooks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,114 @@ | ||||
| ##### | ||||
| ##### This file is part of Mail-in-a-Box-LDAP which is released under the | ||||
| ##### terms of the GNU Affero General Public License as published by the | ||||
| ##### Free Software Foundation, either version 3 of the License, or (at | ||||
| ##### your option) any later version. See file LICENSE or go to | ||||
| ##### https://github.com/downtownallday/mailinabox-ldap for full license | ||||
| ##### details. | ||||
| ##### | ||||
| 
 | ||||
| 
 | ||||
| # | ||||
| # This is a status_checks management hook for the logwatch setup mod. | ||||
| # | ||||
| # It adds logwatch output to status checks. In most circumstances it | ||||
| # will cause a "status checks change notice" email to be sent every | ||||
| # day when daily_tasks.sh runs status checks around 3 am. | ||||
| # | ||||
| # The hook is enabled by placing the file in directory | ||||
| # LOCAL_MODS_DIR/managment_hooks_d. | ||||
| # | ||||
| 
 | ||||
| import os, time | ||||
| import logging | ||||
| from utils import shell | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| 
 | ||||
| def do_hook(hook_name, hook_data, mods_env): | ||||
|     if hook_name != 'status_checks': | ||||
|         # we only care about hooking status_checks | ||||
|         log.debug('hook - ignoring hook %s', hook_name) | ||||
|         return False | ||||
|      | ||||
|     if hook_data['op'] != 'output_changes_end': | ||||
|         # we only want to append for --show-changes | ||||
|         log.debug('hook - ignoring hook op %s:%s', hook_name, hook_data['op']) | ||||
|         return False | ||||
| 
 | ||||
|     output = hook_data['output'] | ||||
|      | ||||
|     if not os.path.exists("/usr/sbin/logwatch"): | ||||
|         output.print_error("logwatch is not installed") | ||||
|         return True | ||||
| 
 | ||||
|     # determine scope and period of the logwatch log file scan | ||||
|     since_mtime = hook_data['since'] if 'since' in hook_data else 0 | ||||
|     if since_mtime <= 0: | ||||
|         since = 'since 24 hours ago for that hour' | ||||
|         since_desc = 'since 24 hours ago' | ||||
|     else: | ||||
|         local_str = time.strftime( | ||||
|             "%Y-%m-%d %H:%M:%S", | ||||
|             time.localtime(since_mtime) | ||||
|         ) | ||||
|         since = 'since %s for that second' % local_str | ||||
|         since_desc = 'since %s' % local_str | ||||
| 
 | ||||
|     # run logwatch | ||||
|     report = shell( | ||||
|         'check_output', | ||||
|         [ | ||||
|             '/usr/sbin/logwatch', | ||||
|             '--range', since, | ||||
|             '--output', 'stdout', | ||||
|             '--format', 'text', | ||||
|             '--service', 'all', | ||||
|             '--service', '-zz-disk_space', | ||||
|             '--service', '-zz-network', | ||||
|             '--service', '-zz-sys', | ||||
|         ], | ||||
|         capture_stderr=True, | ||||
|         trap=False | ||||
|     ) | ||||
| 
 | ||||
|     # defer outputting the heading text because if there is no | ||||
|     # logwatch output we care about, we don't want any output at all | ||||
|     # (which could avoid a status check email) | ||||
|     heading_done = False     | ||||
|     def try_output_heading(): | ||||
|         nonlocal heading_done | ||||
|         if heading_done: return | ||||
|         output.add_heading('System Log Watch (%s)' % since_desc); | ||||
|         heading_done=True | ||||
| 
 | ||||
|     in_summary = False  # true if we're currently processing the logwatch summary text, which is the text output by logwatch that is surrounded by lines containing hashes. we ignore the summary text | ||||
|     blank_line_count = 1 # keep track of the count of adjacent blank lines | ||||
|     output_info = False # true if we've called output.print_info at least once | ||||
|      | ||||
|     for line in report.split('\n'): | ||||
|         line = line.strip(); | ||||
|         if line == '': | ||||
|             blank_line_count += 1 | ||||
|             if blank_line_count == 1 and output_info: | ||||
|                 output.print_line('') | ||||
|          | ||||
|         elif line.startswith('##'): | ||||
|             in_summary = '## Logwatch' in line | ||||
|                  | ||||
|         elif line.startswith('--'): | ||||
|             if ' Begin --' in line: | ||||
|                 start = line.find(' ') | ||||
|                 end = line.rfind(' Begin --') | ||||
|                 try_output_heading() | ||||
|                 output.print_info("%s" % line[start+1:end]) | ||||
|                 output_info = True | ||||
|                 blank_line_count = 0 | ||||
|                  | ||||
|         else: | ||||
|             if not in_summary: | ||||
|                 try_output_heading() | ||||
|                 output.print_line(line) | ||||
|                 blank_line_count = 0 | ||||
|      | ||||
|     return True | ||||
							
								
								
									
										57
									
								
								setup/mods.available/logwatch.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										57
									
								
								setup/mods.available/logwatch.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,57 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| # | ||||
| # this adds the logwatch tool to the system (see, | ||||
| # https://ubuntu.com/server/docs/logwatch) and executes it as part of | ||||
| # normal status checks with it's output attached to daily status | ||||
| # checks emails | ||||
| # | ||||
| # created: 2022-11-03 | ||||
| # author: downtownallday | ||||
| # removal: run `local/logwatch.sh remove`, then delete symbolic link | ||||
| #          (`rm local/logwatch.sh`) | ||||
| 
 | ||||
| . /etc/mailinabox.conf | ||||
| . setup/functions.sh | ||||
| 
 | ||||
| logwatch_remove() { | ||||
|     remove_hook_handler "logwatch-hooks.py" | ||||
|     hide_output apt-get purge logwatch -y | ||||
| } | ||||
| 
 | ||||
| logwatch_install() { | ||||
|     echo "Installing logwatch" | ||||
|     apt_install logwatch | ||||
|     # remove cron entry added by logwatch installer, which emails daily | ||||
|     rm -f /etc/cron.daily/00logwatch | ||||
|     mkdir -p /var/cache/logwatch | ||||
| 
 | ||||
|     # settings in the logwatch.conf file become defaults when running | ||||
|     # the cli /usr/sbin/logwatch (cli arguments override the conf | ||||
|     # file) | ||||
|     local settings=( | ||||
|         "MailTo=administrator@$PRIMARY_HOSTNAME" | ||||
|         "MailFrom=\"$PRIMARY_HOSTNAME\" <administrator@$PRIMARY_HOSTNAME>" | ||||
|     ) | ||||
|          | ||||
|     if [ ! -e /etc/logwatch/conf/logwatch.conf ]; then | ||||
|         cp /usr/share/logwatch/default.conf/logwatch.conf /etc/logwatch/conf/ | ||||
|         settings+=( | ||||
|             "Output=mail" | ||||
|             "Format=html" | ||||
|             "Service=All" | ||||
|             "Detail=Low" | ||||
|         ) | ||||
|     fi | ||||
| 
 | ||||
|     tools/editconf.py /etc/logwatch/conf/logwatch.conf -case-insensitive "${settings[@]}"     | ||||
|     install_hook_handler "setup/mods.available/hooks/logwatch-hooks.py" | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| if [ "${1:-}" = "remove" ]; then | ||||
|     logwatch_remove | ||||
| else | ||||
|     logwatch_install | ||||
| fi | ||||
| 
 | ||||
| @ -54,6 +54,8 @@ comment_char = "#" | ||||
| folded_lines = False | ||||
| testing = False | ||||
| ini_section = None | ||||
| case_insensitive_names = False | ||||
| case_insensitive_values = False | ||||
| while settings[0][0] == "-" and settings[0] != "--": | ||||
| 	opt = settings.pop(0) | ||||
| 	if opt == "-s": | ||||
| @ -75,19 +77,34 @@ while settings[0][0] == "-" and settings[0] != "--": | ||||
| 		comment_char = settings.pop(0) | ||||
| 	elif opt == "-ini-section": | ||||
| 		ini_section = settings.pop(0) | ||||
| 	elif opt == "-case-insensitive": | ||||
| 		case_insensitive_names = True | ||||
| 		case_insensitive_values = True | ||||
| 	elif opt == "-t": | ||||
| 		testing = True | ||||
| 	else: | ||||
| 		print("Invalid option.") | ||||
| 		sys.exit(1) | ||||
| 
 | ||||
| class Setting(object): | ||||
| 	def __init__(self, setting): | ||||
| 		self.name, self.val = setting.split("=", 1) | ||||
|                 # add_only: do not modify existing value | ||||
| 		self.add_only = self.name.startswith("+") | ||||
| 		if self.add_only: self.name=self.name[1:] | ||||
| 	def val_eq(self, other_val, case_insensitive): | ||||
| 		if not case_insensitive: | ||||
| 			r = self.val == other_val | ||||
| 		else: | ||||
| 			r = self.val.lower() == other_val.lower() | ||||
| 		return r | ||||
| # sanity check command line | ||||
| for setting in settings: | ||||
| 	try: | ||||
| 		name, value = setting.split("=", 1) | ||||
| 	except: | ||||
| 		import subprocess | ||||
| 		print("Invalid command line: ", subprocess.list2cmdline(sys.argv)) | ||||
| try: | ||||
| 	settings = [ Setting(x) for x in settings ] | ||||
| except: | ||||
| 	import subprocess | ||||
| 	print("Invalid command line: ", subprocess.list2cmdline(sys.argv)) | ||||
| 
 | ||||
| 
 | ||||
| # create the new config file in memory | ||||
| 
 | ||||
| @ -111,7 +128,7 @@ while len(input_lines) > 0: | ||||
| 			# Put any settings we didn't see at the end of the section. | ||||
| 			for i in range(len(settings)): | ||||
| 				if i not in found: | ||||
| 					name, val = settings[i].split("=", 1) | ||||
| 					name, val = (settings[i].name, settings[i].val) | ||||
| 					if not (not val and erase_setting): | ||||
| 					        buf += name + delimiter + val + "\n" | ||||
| 		cur_section = line.strip()[1:-1].strip().lower() | ||||
| @ -126,19 +143,26 @@ while len(input_lines) > 0: | ||||
| 	# See if this line is for any settings passed on the command line. | ||||
| 	for i in range(len(settings)): | ||||
| 		# Check if this line contain this setting from the command-line arguments. | ||||
| 		name, val = settings[i].split("=", 1) | ||||
| 		name, val = (settings[i].name, settings[i].val) | ||||
| 		flags = re.S | (re.I if case_insensitive_names else 0) | ||||
| 		m = re.match( | ||||
| 			   "(\s*)" | ||||
| 			 + "(" + re.escape(comment_char) + "\s*)?" | ||||
| 			 + re.escape(name) + delimiter_re + "(.*?)\s*$", | ||||
| 			 line, re.S) | ||||
| 			 line, flags) | ||||
| 		if not m: continue | ||||
| 		indent, is_comment, existing_val = m.groups() | ||||
| 
 | ||||
|                 # With + before the name, don't modify the existing value | ||||
| 		if settings[i].add_only: | ||||
| 			found.add(i) | ||||
| 			buf += line | ||||
| 			break | ||||
| 
 | ||||
| 		# If this is already the setting, keep it in the file, except: | ||||
| 		# * If we've already seen it before, then remove this duplicate line. | ||||
| 		# * If val is empty and erase_setting is on, then comment it out. | ||||
| 		if is_comment is None and existing_val == val and not (not val and erase_setting): | ||||
| 		if is_comment is None and settings[i].val_eq(existing_val, case_insensitive_values) and not (not val and erase_setting): | ||||
| 			# It may be that we've already inserted this setting higher | ||||
| 			# in the file so check for that first. | ||||
| 			if i in found: break | ||||
| @ -173,11 +197,11 @@ while len(input_lines) > 0: | ||||
| # Put any settings we didn't see at the end of the file, | ||||
| # except settings being cleared. | ||||
| if not ini_section or cur_section == ini_section.lower(): | ||||
|         for i in range(len(settings)): | ||||
|                 if (i not in found): | ||||
|                         name, val = settings[i].split("=", 1) | ||||
|                         if not (not val and erase_setting): | ||||
|                                 buf += name + delimiter + val + "\n" | ||||
| 	for i in range(len(settings)): | ||||
| 		if (i not in found): | ||||
| 			name, val = (settings[i].name, settings[i].val) | ||||
| 			if not (not val and erase_setting): | ||||
| 				buf += name + delimiter + val + "\n" | ||||
| 
 | ||||
| if not testing: | ||||
| 	# Write out the new file. | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user