mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-03 00:07:05 +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