Skip to content

Instantly share code, notes, and snippets.

@mosquito
Last active March 27, 2024 10:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mosquito/d35f42efa288672dac9cfde3c67a6921 to your computer and use it in GitHub Desktop.
Save mosquito/d35f42efa288672dac9cfde3c67a6921 to your computer and use it in GitHub Desktop.
BOOBEN is a system boot notifier

BOOBEN

BOOBEN is a Python script that notifies you when your system starts or stops, using your system's Mail Transfer Agent (MTA) to send out emails. Right out of the box, it includes useful info like server status, the time of the event, ZFS pool status, network interface details, and logs from journald and dmesg. Feel free to tweak it by adding what you need or removing what you don't. It's made to be easy to customize to your liking.

Requirements

  • Python 3
  • A configured system MTA (like sendmail) that respects /etc/aliases.

Installation

  1. Copy the script to /usr/local/bin/booben.
  2. Ensure the script is executable: chmod +x /usr/local/bin/booben.

Systemd Integration

To automate email generation on system start and stop, configure a systemd service.

  1. Create a systemd service file /etc/systemd/system/booben.service with the following content:
[Unit]
Description=Send mail on system start and stop
Wants=network-online.target
After=network.target network-online.target

[Service]
Type=oneshot
# Should be tweaked for your MTA
ExecStart=/bin/bash -c '/usr/local/bin/booben start | sendmail --aliases=/etc/aliases root'
ExecStop=/bin/bash -c '/usr/local/bin/booben stop | sendmail --aliases=/etc/aliases root'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
  1. Reload systemd to recognize the new service: sudo systemctl daemon-reload.
  2. Enable the service: sudo systemctl enable booben.service.

Usage

While the script is typically invoked by systemd, you can manually trigger email generation:

  • For a start event: /usr/local/bin/booben start.
  • For a stop event: /usr/local/bin/booben stop.

Systemd handles the execution at system start and stop, piping the script's output to sendmail for email delivery.

Configuration

Edit /etc/aliases to specify the notification email's recipient(s), directing emails intended for root to the appropriate address(es).

Notes

  • This script generates email content but relies on your system's MTA to send the emails. Ensure your MTA is correctly configured.
  • The use of gzip compression for logs helps reduce email size.

Customization

booben is designed with flexibility in mind, making it straightforward to customize the set of commands executed and the information fields included in the notification emails. Here's how you can customize booben to fit your specific needs:

Modifying Commands and Information Fields

The script consists of various classes representing different sections of the email body, including CommandSection for executing shell commands and capturing their output, and FileSection for attaching logs and other files. To customize the information included in the emails:

  1. Add or Remove Sections: You can modify the create_email function to add or remove instances of CommandSection, FileSection, or any custom sections you define. Each section is designed to capture specific pieces of information or system states.

  2. Customize Shell Commands: In the CommandSection instances, change the shell_command parameter to any shell command whose output you wish to include in the notification email. For example, replacing 'zpool status -v' with another command like 'df -h' to report disk space usage.

  3. Adjust Log Files: The LogSection class is a specialized form of FileSection for attaching log files. Modify the command parameter to change which logs are captured and compressed into gzip format for attachment.

Example: Adding a Disk Usage Section

To add a section reporting disk usage, you would insert the following line into the create_email function:

message_body.add_row('Disk Usage', CommandSection('df -h'))

This line creates a new CommandSection that executes the df -h command, capturing its output in a readable format for inclusion in the email.

Example: Attaching a Custom Log File

To attach a custom log file, you can create a new LogSection instance with the log file's path and desired filename for the attachment:

files.append(LogSection("errors.log", 'grep "error" /path/to/your/logfile'))

This line appends a new LogSection to the files list, which reads and compresses the specified log file, attaching it to the email.

Notes

  • When adding shell commands, ensure they can be executed without user interaction and complete in a reasonable amount of time.
  • Review the commands to ensure they do not expose sensitive information unintendedly, as the email content will include their output.

By following these guidelines, you can tailor booben to meet your system monitoring and notification needs, ensuring you're always informed about the key aspects of your system's status at boot and shutdown.

#!/usr/bin/env python3
import argparse
import datetime
import gzip
import socket
import subprocess
import sys
import tempfile
import zlib
from email import encoders
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
class Section:
def to_mime(self):
raise NotImplementedError("Subclasses must implement this method")
class RawSection(Section):
def __init__(self, content):
self.content = content
def to_mime(self):
return self.content
class MessageBody(Section):
def __init__(self):
self.rows = []
def add_row(self, title, section):
if not isinstance(section, RawSection):
raise ValueError("section must be an instance of BodySection")
self.rows.append((title, section.to_mime()))
def to_mime(self):
html_content = '<html><body>\n'
for title, content in self.rows:
html_content += f'<h3>{title}</h3>\n<p>{content}</p>\n\n'
html_content += '</body></html>\n'
return MIMEText(html_content, 'html')
class FileSection(Section):
def __init__(self, filename, content):
self.filename = filename
self.content = content
def to_mime(self):
part = MIMEBase('application', 'octet-stream')
part.set_payload(self.content)
encoders.encode_base64(part)
part.add_header('Content-Disposition', f'attachment; filename="{self.filename}"')
return part
class CommandSection(RawSection):
def __init__(self, shell_command):
self.shell_command = shell_command
def to_mime(self):
try:
process = subprocess.run(self.shell_command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return f'<pre>\n{process.stdout}\n</pre>'
except subprocess.CalledProcessError as e:
return (
f"<ul>"
f"<li><b>Command</b>: {e.cmd}</li>"
f"<li><b>Return code</b>: {e.returncode}</li>"
f"<li><b>Stderr</b>:<pre>{e.stderr}</pre></li>"
f"</ul>"
)
class LogSection(FileSection):
def __init__(self, filename: str, command: str):
with tempfile.TemporaryFile() as tmp:
with gzip.open(tmp, 'wb') as log:
log.write(subprocess.check_output(command, shell=True))
tmp.seek(0)
super().__init__(f"{filename}.gz", tmp.read())
def create_email(args):
email = MIMEMultipart()
date_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
hostname = socket.getfqdn()
if args.command == 'start':
email['Subject'] = f"Server {hostname} started at {date_str}"
elif args.command == 'stop':
email['Subject'] = f"Server {hostname} stopping since {date_str}"
else:
raise RuntimeError(f"Bad command {args.command}")
message_body = MessageBody()
message_body.add_row('Status', RawSection(f"Server {args.command}"))
message_body.add_row('Time', RawSection(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
message_body.add_row('ZFS status', CommandSection('zpool status -v'))
message_body.add_row('Network interfaces', CommandSection('ip address list'))
email.attach(message_body.to_mime())
files = [
LogSection("journald.error.log", 'journalctl --boot 0 -o with-unit -p 3 -n 2000'),
LogSection("dmesg.log", 'dmesg -xP --noescape --time-format iso'),
]
for file in files:
email.attach(file.to_mime())
return email.as_string()
def main():
parser = argparse.ArgumentParser(description='Send notification emails on system boot events.')
parser.add_argument('command', choices=['start', 'stop'], help='Indicates whether the system is starting or stopping.')
args = parser.parse_args()
print(create_email(args))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment