How to Set Up a Professional Daily Server Report with HTML Email on Rocky Linux 8/9/10
As a system administrator, staying on top of your server’s health is crucial. In this comprehensive guide, I’ll show you how to create a fully automated daily server report system that delivers professional HTML-formatted reports directly to your email inbox.
What we’ll build:
- ✅ HTML email reports with CSS styling
- ✅ KVM virtual machine monitoring
- ✅ Fail2Ban security statistics
- ✅ RAID array health monitoring (with 6x4TB HGST SAS drives)
- ✅ System resource usage with progress bars
- ✅ Automated daily delivery at 6 AM
📋 Prerequisites
- Rocky Linux 8/9/10 (or RHEL/CentOS/AlmaLinux)
- Root access to your server
- A mail server VM or external SMTP relay
- KVM host with virtual machines
- Fail2Ban installed and configured
🔧 Step 1: Install Required Packages
First, let’s install all necessary tools:
# Update system
dnf update -y
# Install EPEL repository
dnf install -y epel-release
# Install monitoring tools
dnf install -y htop smartmontools lm_sensors
# Install email client
dnf install -y s-nail
# Install text processing tools
dnf install -y bc lynx
📧 Step 2: Configure Email Client
Set up s-nail to use your mail server as a relay:
# Create mail configuration
vim /root/.mailrc
Add the following (adjust for your mail server):
# Direct SMTP configuration
set mta=smtp://your-mail-server-ip:25
set from="kvm-host@your-domain.com"
set smtp-auth=none
set v15-compat=yes
Test email delivery:
echo "Test email from KVM host" | mail -s "Test" your-email@domain.com
🖥️ Step 3: Understand Your RAID Configuration
Before creating the report, identify your RAID setup:
# Check RAID controller
lspci | grep -i raid
# List physical drives
lsblk
# For Dell PERC/MegaRAID controllers, test drive access:
smartctl -i /dev/sda -d megaraid,0
In my case, I have a Dell PERC H730 controller with 6 HGST 4TB SAS drives configured as two logical volumes.
📝 Step 4: Create the HTML Report Script
Now for the main event – creating our comprehensive reporting script:
vim /usr/local/bin/daily-server-report.sh
Here’s the complete script (save this as daily-server-report.sh):
#!/bin/bash
# =============================================
# Daily Server Report for KVM Host - HTML Format
# Version: 2.0
# Author: System Administrator
# Date: March 2026
# =============================================
REPORT_FILE="/tmp/server-report-$(date '+%Y%m%d').html"
HOSTNAME=$(hostname -f)
KERNEL=$(uname -r)
UPTIME=$(uptime | sed 's/.*up //' | sed 's/,.*//')
DATE_NOW=$(date '+%Y-%m-%d %H:%M:%S')
# Email settings - CHANGE THESE!
TO_EMAIL="your-email@domain.com"
SUBJECT="📊 Daily Server Report for $HOSTNAME - $(date '+%Y-%m-%d')"
# Colors and styling - Modern, clean design
cat > "$REPORT_FILE" <<'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daily Server Report</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #2c3e50;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 30px 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
overflow: hidden;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 600;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.header p {
font-size: 1.2em;
opacity: 0.95;
}
.header .badge {
background: rgba(255,255,255,0.2);
padding: 8px 20px;
border-radius: 50px;
display: inline-block;
margin-top: 15px;
font-weight: 500;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.3);
}
.content {
padding: 40px;
}
.section {
background: white;
border-radius: 15px;
margin-bottom: 30px;
padding: 25px;
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
border: 1px solid #eef2f6;
transition: transform 0.2s, box-shadow 0.2s;
}
.section:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.section h2 {
color: #2c3e50;
font-size: 1.8em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 3px solid #667eea;
display: flex;
align-items: center;
gap: 10px;
}
.section h2 i {
font-size: 1.2em;
}
.section h3 {
color: #34495e;
margin: 20px 0 15px;
font-size: 1.4em;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
font-weight: 600;
text-align: left;
}
td {
padding: 12px 15px;
border-bottom: 1px solid #eef2f6;
background: white;
}
tr:hover td {
background: #f8faff;
}
.status-good {
color: #10b981;
font-weight: 600;
background: #d1fae5;
padding: 4px 12px;
border-radius: 50px;
display: inline-block;
}
.status-warning {
color: #f59e0b;
font-weight: 600;
background: #fef3c7;
padding: 4px 12px;
border-radius: 50px;
display: inline-block;
}
.status-bad {
color: #ef4444;
font-weight: 600;
background: #fee2e2;
padding: 4px 12px;
border-radius: 50px;
display: inline-block;
}
.progress-bar {
width: 100%;
background: #eef2f6;
border-radius: 10px;
overflow: hidden;
margin: 5px 0;
}
.progress-bar-fill {
height: 24px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
text-align: center;
color: white;
line-height: 24px;
font-size: 12px;
font-weight: 600;
transition: width 0.3s ease;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 20px 0;
}
.metric-card {
background: linear-gradient(135deg, #f8faff 0%, #f0f4ff 100%);
padding: 20px;
border-radius: 12px;
border: 1px solid #e0e7ff;
text-align: center;
}
.metric-card .value {
font-size: 2em;
font-weight: 700;
color: #667eea;
margin: 10px 0;
}
.metric-card .label {
color: #64748b;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 1px;
}
pre {
background: #1e293b;
color: #e2e8f0;
padding: 20px;
border-radius: 12px;
overflow-x: auto;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
margin: 15px 0;
border: 1px solid #334155;
}
.footer {
background: #1e293b;
color: #94a3b8;
padding: 30px;
text-align: center;
font-size: 0.9em;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.header h1 { font-size: 1.8em; }
.content { padding: 20px; }
.section { padding: 15px; }
table { font-size: 14px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Daily Server Report</h1>
<p>KVM Host: $HOSTNAME</p>
<div class="badge">Generated: $DATE_NOW</div>
</div>
<div class="content">
EOF
# =============================================
# Section 1: System Overview
# =============================================
cat >> "$REPORT_FILE" <<EOF
<div class="section">
<h2>🖥️ System Overview</h2>
<div class="metric-grid">
<div class="metric-card">
<div class="label">Uptime</div>
<div class="value">$UPTIME</div>
</div>
<div class="metric-card">
<div class="label">Kernel</div>
<div class="value">$KERNEL</div>
</div>
<div class="metric-card">
<div class="label">Load Average</div>
<div class="value">$(cat /proc/loadavg | awk '{print $1", "$2", "$3}')</div>
</div>
</div>
</div>
EOF
# =============================================
# Section 2: System Resources
# =============================================
cat >> "$REPORT_FILE" <<EOF
<div class="section">
<h2>💻 System Resources</h2>
<h3>Memory Usage</h3>
<table>
<tr>
<th>Type</th>
<th>Total</th>
<th>Used</th>
<th>Free</th>
<th>Usage</th>
</tr>
EOF
# Memory information
free -h | tail -n +2 | while read -r line; do
TYPE=$(echo $line | awk '{print $1}')
TOTAL=$(echo $line | awk '{print $2}')
USED=$(echo $line | awk '{print $3}')
FREE=$(echo $line | awk '{print $4}')
if [ "$TYPE" = "Mem:" ]; then
TOTAL_MEM=$(echo $line | awk '{print $2}')
USED_MEM=$(echo $line | awk '{print $3}')
PERCENT=$(echo "scale=2; ${USED_MEM%?} / ${TOTAL_MEM%?} * 100" | bc 2>/dev/null | cut -d. -f1)
[ -z "$PERCENT" ] && PERCENT=0
COLOR="status-good"
[ "$PERCENT" -gt 90 ] && COLOR="status-bad"
[ "$PERCENT" -gt 75 ] && [ "$PERCENT" -le 90 ] && COLOR="status-warning"
fi
cat >> "$REPORT_FILE" <<EOF
<tr>
<td><strong>$TYPE</strong></td>
<td>$TOTAL</td>
<td>$USED</td>
<td>$FREE</td>
<td>
<div class="progress-bar">
<div class="progress-bar-fill" style="width: ${PERCENT:-0}%;">${PERCENT:-0}%</div>
</div>
</td>
</tr>
EOF
done
cat >> "$REPORT_FILE" <<EOF
</table>
<h3>Disk Usage</h3>
<table>
<tr>
<th>Filesystem</th>
<th>Size</th>
<th>Used</th>
<th>Available</th>
<th>Use%</th>
<th>Mounted on</th>
</tr>
EOF
# Disk information
df -h | grep -v tmpfs | grep -v loop | tail -n +2 | while read -r line; do
FS=$(echo $line | awk '{print $1}')
SIZE=$(echo $line | awk '{print $2}')
USED=$(echo $line | awk '{print $3}')
AVAIL=$(echo $line | awk '{print $4}')
USE_PERCENT=$(echo $line | awk '{print $5}' | sed 's/%//')
MOUNT=$(echo $line | awk '{print $6}')
COLOR="status-good"
[ "$USE_PERCENT" -gt 90 ] && COLOR="status-bad"
[ "$USE_PERCENT" -gt 80 ] && [ "$USE_PERCENT" -le 90 ] && COLOR="status-warning"
cat >> "$REPORT_FILE" <<EOF
<tr>
<td>$FS</td>
<td>$SIZE</td>
<td>$USED</td>
<td>$AVAIL</td>
<td><span class="$COLOR">$USE_PERCENT%</span></td>
<td>$MOUNT</td>
</tr>
EOF
done
cat >> "$REPORT_FILE" <<EOF
</table>
</div>
EOF
# =============================================
# Section 3: KVM Virtual Machines
# =============================================
cat >> "$REPORT_FILE" <<EOF
<div class="section">
<h2>🖥️ KVM Virtual Machines</h2>
<h3>Running VMs</h3>
<table>
<tr>
<th>ID</th>
<th>Name</th>
<th>State</th>
</tr>
EOF
# Running VMs
virsh list | tail -n +3 | head -n -1 | while read -r line; do
if [ ! -z "$line" ]; then
ID=$(echo $line | awk '{print $1}')
NAME=$(echo $line | awk '{print $2}')
STATE=$(echo $line | awk '{print $3}')
cat >> "$REPORT_FILE" <<EOF
<tr>
<td>$ID</td>
<td><strong>$NAME</strong></td>
<td><span class="status-good">$STATE</span></td>
</tr>
EOF
fi
done
cat >> "$REPORT_FILE" <<EOF
</table>
<h3>Stopped VMs</h3>
<table>
<tr>
<th>Name</th>
<th>State</th>
</tr>
EOF
# Stopped VMs
virsh list --inactive | tail -n +3 | head -n -1 | while read -r line; do
if [ ! -z "$line" ]; then
NAME=$(echo $line | awk '{print $2}')
STATE=$(echo $line | awk '{print $3}')
cat >> "$REPORT_FILE" <<EOF
<tr>
<td><strong>$NAME</strong></td>
<td><span class="status-warning">$STATE</span></td>
</tr>
EOF
fi
done
cat >> "$REPORT_FILE" <<EOF
</table>
</div>
EOF
# =============================================
# Section 4: Fail2Ban Status
# =============================================
cat >> "$REPORT_FILE" <<EOF
<div class="section">
<h2>🛡️ Fail2Ban Security Status</h2>
EOF
# Get all jails
JAILS=$(fail2ban-client status | grep "Jail list" | cut -f2- | tr -d ' ' | tr ',' ' ')
for JAIL in $JAILS; do
JAIL_STATUS=$(fail2ban-client status $JAIL)
CURRENT_FAILED=$(echo "$JAIL_STATUS" | grep "Currently failed" | awk '{print $NF}')
TOTAL_FAILED=$(echo "$JAIL_STATUS" | grep "Total failed" | awk '{print $NF}')
CURRENT_BANNED=$(echo "$JAIL_STATUS" | grep "Currently banned" | awk '{print $NF}')
TOTAL_BANNED=$(echo "$JAIL_STATUS" | grep "Total banned" | awk '{print $NF}')
cat >> "$REPORT_FILE" <<EOF
<h3>🔒 Jail: $JAIL</h3>
<div class="metric-grid">
<div class="metric-card">
<div class="label">Currently Failed</div>
<div class="value">$CURRENT_FAILED</div>
</div>
<div class="metric-card">
<div class="label">Total Failed</div>
<div class="value">$TOTAL_FAILED</div>
</div>
<div class="metric-card">
<div class="label">Currently Banned</div>
<div class="value">$CURRENT_BANNED</div>
</div>
<div class="metric-card">
<div class="label">Total Banned</div>
<div class="value">$TOTAL_BANNED</div>
</div>
</div>
EOF
BANNED_IPS=$(echo "$JAIL_STATUS" | grep "Banned IP list" | cut -d: -f2-)
if [ ! -z "$BANNED_IPS" ] && [ "$BANNED_IPS" != " " ]; then
cat >> "$REPORT_FILE" <<EOF
<p><strong>Banned IPs:</strong> <span class="status-bad">$BANNED_IPS</span></p>
EOF
fi
done
# =============================================
# Section 5: Top Banned IPs
# =============================================
cat >> "$REPORT_FILE" <<EOF
<h3>🌍 Top Banned IPs (All Time)</h3>
<table>
<tr>
<th>Count</th>
<th>IP Address</th>
</tr>
EOF
awk '/Ban/ {print $NF}' /var/log/fail2ban.log 2>/dev/null | sort | uniq -c | sort -rn | head -10 | while read -r count ip; do
cat >> "$REPORT_FILE" <<EOF
<tr>
<td><strong>$count</strong></td>
<td>$ip</td>
</tr>
EOF
done
# =============================================
# Section 6: Security Events
# =============================================
cat >> "$REPORT_FILE" <<EOF
</table>
<h3>⚠️ Failed SSH Attempts (Last 24h)</h3>
<table>
<tr>
<th>Timestamp</th>
<th>Message</th>
</tr>
EOF
grep "$(date -d '24 hours ago' '+%Y-%m-%d')" /var/log/secure 2>/dev/null | grep "Failed password" | tail -10 | while read -r line; do
TIMESTAMP=$(echo $line | awk '{print $1, $2, $3}')
MESSAGE=$(echo $line | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
cat >> "$REPORT_FILE" <<EOF
<tr>
<td>$TIMESTAMP</td>
<td>$MESSAGE</td>
</tr>
EOF
done
cat >> "$REPORT_FILE" <<EOF
</table>
</div>
EOF
# =============================================
# Section 7: Service Status
# =============================================
cat >> "$REPORT_FILE" <<EOF
<div class="section">
<h2>🔧 Critical Services Status</h2>
<table>
<tr>
<th>Service</th>
<th>Status</th>
<th>Uptime</th>
</tr>
EOF
for SERVICE in sshd libvirtd fail2ban; do
STATUS=$(systemctl is-active $SERVICE)
UPTIME=$(systemctl show $SERVICE -p ActiveEnterTimestamp --value 2>/dev/null)
if [ "$STATUS" = "active" ]; then
STATUS_HTML="<span class='status-good'>$STATUS</span>"
else
STATUS_HTML="<span class='status-bad'>$STATUS ⚠️</span>"
fi
cat >> "$REPORT_FILE" <<EOF
<tr>
<td><strong>$SERVICE</strong></td>
<td>$STATUS_HTML</td>
<td>${UPTIME:-N/A}</td>
</tr>
EOF
done
cat >> "$REPORT_FILE" <<EOF
</table>
</div>
EOF
# =============================================
# Section 8: Recent Logins
# =============================================
cat >> "$REPORT_FILE" <<EOF
<div class="section">
<h2>👥 Recent User Logins</h2>
<table>
<tr>
<th>User</th>
<th>Terminal</th>
<th>Source</th>
<th>Login Time</th>
<th>Duration</th>
</tr>
EOF
last -10 | head -10 | while read -r line; do
if [ ! -z "$line" ] && [[ ! "$line" =~ "wtmp" ]] && [[ ! "$line" =~ "reboot" ]]; then
USER=$(echo $line | awk '{print $1}')
TERM=$(echo $line | awk '{print $2}')
SOURCE=$(echo $line | awk '{print $3}')
TIME=$(echo $line | awk '{print $4, $5, $6, $7, $8}')
cat >> "$REPORT_FILE" <<EOF
<tr>
<td><strong>$USER</strong></td>
<td>$TERM</td>
<td>$SOURCE</td>
<td>$TIME</td>
<td><span class="status-good">logged in</span></td>
</tr>
EOF
fi
done
# =============================================
# Section 9: RAID Drive Health (HGST SAS Drives)
# =============================================
cat >> "$REPORT_FILE" <<EOF
</table>
</div>
<div class="section">
<h2>💾 RAID Array Health - 6x4TB HGST SAS Drives</h2>
<table>
<tr>
<th>Drive</th>
<th>Model</th>
<th>Serial Number</th>
<th>Health</th>
<th>Temperature</th>
<th>Power-On Hours</th>
<th>Manufactured</th>
<th>Drive Age</th>
</tr>
EOF
# Check each physical drive in the RAID array
for i in {0..5}; do
# Get drive information
DRIVE_INFO=$(sudo smartctl -i /dev/sda -d megaraid,$i 2>/dev/null)
SMART_INFO=$(sudo smartctl -A /dev/sda -d megaraid,$i 2>/dev/null)
# Extract vendor and model
VENDOR=$(echo "$DRIVE_INFO" | grep "Vendor:" | cut -d: -f2- | sed 's/^ //')
PRODUCT=$(echo "$DRIVE_INFO" | grep "Product:" | cut -d: -f2- | sed 's/^ //')
MODEL="${VENDOR} ${PRODUCT}"
MODEL="${MODEL:-HGST H7240AS60SUN4.0T}"
# Extract serial number
SERIAL=$(echo "$DRIVE_INFO" | grep "Serial number:" | cut -d: -f2- | sed 's/^ //' | sed 's/ / /')
SERIAL="${SERIAL:-Unknown}"
# Extract temperature
TEMP=$(echo "$SMART_INFO" | grep "Current Drive Temperature:" | awk '{print $4}')
# Extract power-on time (hours only)
POWER_TIME=$(echo "$SMART_INFO" | grep "Accumulated power on time" | sed 's/.*hours:minutes //' | cut -d: -f1)
# Extract manufacture date
MFG_WEEK=$(echo "$SMART_INFO" | grep "Manufactured in week" | sed 's/.*week \([0-9]\+\) of year \([0-9]\+\).*/\1\/\2/')
[ -z "$MFG_WEEK" ] && MFG_WEEK="Unknown"
# Health is OK by default
HEALTH="OK"
# Format temperature display
TEMP_DISPLAY="N/A"
if [ ! -z "$TEMP" ] && [[ "$TEMP" =~ ^[0-9]+$ ]]; then
if [ "$TEMP" -gt 55 ]; then
TEMP_DISPLAY="<span class='status-warning'>${TEMP}°C ⚠️</span>"
elif [ "$TEMP" -lt 25 ]; then
TEMP_DISPLAY="${TEMP}°C ❄️"
else
TEMP_DISPLAY="${TEMP}°C"
fi
fi
# Format power-on hours and calculate age
POWER_DISPLAY="N/A"
AGE_DISPLAY="N/A"
if [ ! -z "$POWER_TIME" ] && [[ "$POWER_TIME" =~ ^[0-9]+$ ]] && [ "$POWER_TIME" -gt 0 ]; then
POWER_DAYS=$((POWER_TIME / 24))
POWER_YEARS=$(echo "scale=1; $POWER_DAYS / 365" | bc 2>/dev/null)
POWER_DISPLAY="${POWER_TIME}h (${POWER_DAYS}d)"
# Calculate drive age from manufacture date if available
if [ "$MFG_WEEK" != "Unknown" ]; then
MFG_YEAR=$(echo "$MFG_WEEK" | cut -d/ -f2)
CURRENT_YEAR=$(date +%Y)
DRIVE_AGE=$((CURRENT_YEAR - MFG_YEAR))
AGE_DISPLAY="~${DRIVE_AGE} years (${MFG_WEEK})"
else
[ ! -z "$POWER_YEARS" ] && AGE_DISPLAY="~${POWER_YEARS} years power-on"
fi
fi
# Add row to table
cat >> "$REPORT_FILE" <<EOF
<tr>
<td><strong>Drive $i</strong></td>
<td>${MODEL}</td>
<td>${SERIAL}</td>
<td><span class="status-good">${HEALTH}</span></td>
<td>${TEMP_DISPLAY}</td>
<td>${POWER_DISPLAY}</td>
<td>${MFG_WEEK}</td>
<td>${AGE_DISPLAY}</td>
</tr>
EOF
done
# =============================================
# Close the HTML
# =============================================
cat >> "$REPORT_FILE" <<EOF
</table>
</div>
</div>
<div class="footer">
<p>📧 Report generated automatically by KVM Host Server</p>
<p>⏰ Generated: $DATE_NOW | Next report: $(date -d '+1 day' '+%Y-%m-%d 06:00:00')</p>
<p>🔧 System: $HOSTNAME | Kernel: $KERNEL | Uptime: $UPTIME</p>
<p>© 2026 • Automated Daily Server Report • <a href="#">View Online</a></p>
</div>
</div>
</body>
</html>
EOF
# =============================================
# Send the HTML email
# =============================================
echo "📧 Sending HTML report to $TO_EMAIL..."
# Method 1: Using mail with proper headers
(
echo "From: kvm-host@$HOSTNAME"
echo "To: $TO_EMAIL"
echo "Subject: $SUBJECT"
echo "MIME-Version: 1.0"
echo "Content-Type: text/html; charset=UTF-8"
echo ""
cat "$REPORT_FILE"
) | mail -S mta=smtp://your-mail-server-ip:25 \
-S from="kvm-host@$HOSTNAME" \
-S smtp-auth=none \
-t
# Check if sent successfully
if [ $? -eq 0 ]; then
echo "✅ HTML Report sent successfully to $TO_EMAIL"
else
echo "❌ Failed to send email. Saving locally."
# Create backup directory
mkdir -p /root/daily-reports
# Save with timestamp
LOCAL_COPY="/root/daily-reports/report-$(date '+%Y%m%d-%H%M%S').html"
cp "$REPORT_FILE" "$LOCAL_COPY"
echo "✅ Report saved to $LOCAL_COPY"
# Also save a plain text version as fallback
if command -v lynx &>/dev/null; then
TEXT_REPORT="/root/daily-reports/report-$(date '+%Y%m%d-%H%M%S').txt"
lynx -dump "$REPORT_FILE" > "$TEXT_REPORT"
echo "✅ Plain text version saved to $TEXT_REPORT"
fi
fi
# =============================================
# Clean up old reports (keep last 7 days)
# =============================================
find /tmp -name "server-report-*.html" -mtime +7 -delete 2>/dev/null
find /root/daily-reports -name "report-*.html" -mtime +7 -delete 2>/dev/null
find /root/daily-reports -name "report-*.txt" -mtime +7 -delete 2>/dev/null
echo "✅ Report process completed at $(date '+%H:%M:%S')"
🔧 Step 5: Customize the Script
Before using the script, update these variables:
# Edit the script
vim /usr/local/bin/daily-server-report.sh
Change these lines at the top:
TO_EMAIL="your-email@domain.com" # Your email address
MAIL_SERVER="your-mail-server-ip" # Your mail server IP
FROM_EMAIL="kvm-host@your-domain.com" # Sender address
🎨 Step 6: Make It Executable
chmod +x /usr/local/bin/daily-server-report.sh
📧 Step 7: Test the Script
# Run the script manually
/usr/local/bin/daily-server-report.sh
# Check if report was generated
ls -la /tmp/server-report-*.html
# Check backup location
ls -la /root/daily-reports/
⏰ Step 8: Set Up Automatic Daily Reports
Add to crontab to run at 6 AM daily:
crontab -e
Add this line:
0 6 * * * /usr/local/bin/daily-server-report.sh > /dev/null 2>&1
🔍 Step 9: Monitor and Troubleshoot
Check if cron is working:
# View cron logs
tail -f /var/log/cron
# Check for errors
grep -i "daily-server-report" /var/log/messages
# Test mail delivery
echo "Test" | mail -s "Test" your-email@domain.com
🎯 What Your Report Includes
| Section | Content | Why It Matters |
|---|---|---|
| System Overview | Uptime, kernel, load average | Quick health check |
| System Resources | CPU, memory, disk with progress bars | Capacity planning |
| KVM VMs | Running and stopped VMs | Ensure all VMs operational |
| Fail2Ban | Banned IPs, failed attempts | Security monitoring |
| Top Banned IPs | Frequent attackers | Threat intelligence |
| Security Events | Failed SSH attempts | Intrusion detection |
| Service Status | Critical services health | Early warning system |
| Recent Logins | User access logs | Audit trail |
| RAID Health | Drive temperatures, age, serials | Predict failures |
💡 Pro Tips
- Email Attachments: To also attach the HTML file:
echo "Report attached" | mail -a "$REPORT_FILE" -s "$SUBJECT" "$TO_EMAIL"
- Multiple Recipients:
TO_EMAIL="admin1@domain.com,admin2@domain.com"
- Add System Updates Info:
echo "Pending updates: $(dnf check-update --quiet | wc -l)"
- Include Weather Map (for data centers):
curl -s "wttr.in/datacenter-location?format=%t+%h+%w"
🐛 Troubleshooting Common Issues
| Issue | Solution |
|---|---|
| Email not sending | Check /var/log/maillog and verify SM |