first commit

This commit is contained in:
cpu
2026-07-11 12:14:59 +00:00
commit f492147b53
8 changed files with 272 additions and 0 deletions

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
# Git
.git
.gitignore
# Secrets (CRITICAL: Do not bake .env files into the image!)
.env
.env.*
# Docker
Dockerfile
.dockerignore
# Python
__pycache__/
*.py[cod]
*~
venv/
.venv/
env/
# IDEs / Editor files
.vscode/
.idea/
*.swp
*.swo

26
.env.example Normal file
View File

@@ -0,0 +1,26 @@
# --- AI Configuration ---
OLLAMA_API_URL=http://localhost:11434/api/generate
OLLAMA_MODEL=llama3.2:1b
AI_SYSTEM_PROMPT="You are a Linux Sysadmin. Look at these server error logs from yesterday. Summarize the main issues, identify any potential attacks (like failed brute force logins), and ignore harmless background noise. Keep it brief."
# --- Email Reporting Configuration ---
REPORT_TO_EMAIL=admin@yourdomain.com
REPORT_FROM_EMAIL=system@yourdomain.com
SMTP_SERVER=mail.yourdomain.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
# --- Log Analysis Configuration ---
JOURNAL_UNIT=mailserver
MAX_ERRORS_TO_ANALYZE=50
# Comma-separated list of words that trigger the AI
ERROR_KEYWORDS=error,failed,warning,fatal,timeout
# --- Execution Options ---
# Set to 'true' to see detailed background processes, 'false' for clean logs
DEBUG=true
# Set to 'true' to run also immediately when the container starts, 'false' to wait for the schedule
RUN_NOW=true
# Time format must be HH:MM in 24-hour format
REPORT_TIME=08:00

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM python:3.11-slim
# Install systemd so the container has the 'journalctl' command available
RUN apt-get update && \
apt-get install -y systemd && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy our script
COPY daily_summary.py .
# Run Python in unbuffered mode (-u) so we can see logs in real-time via 'docker logs'
CMD ["python", "-u", "daily_summary.py"]

0
README.md Normal file
View File

166
daily_summary.py Normal file
View File

@@ -0,0 +1,166 @@
import os
import subprocess
import requests
import smtplib
from email.message import EmailMessage
import time
import schedule
# --- CONFIGURATION (Loaded from environment variables) ---
OLLAMA_API = os.getenv("OLLAMA_API_URL", "http://localhost:11434/api/generate")
MODEL = os.getenv("OLLAMA_MODEL", "llama3.2:1b")
AI_SYSTEM_PROMPT = os.getenv("AI_SYSTEM_PROMPT", "You are a Linux Sysadmin. Summarize these logs briefly.")
MY_EMAIL = os.getenv("REPORT_TO_EMAIL")
FROM_EMAIL = os.getenv("REPORT_FROM_EMAIL")
SMTP_SERVER = os.getenv("SMTP_SERVER", "127.0.0.1")
SMTP_PORT = int(os.getenv("SMTP_PORT", 25))
SMTP_USER = os.getenv("SMTP_USER", "")
SMTP_PASS = os.getenv("SMTP_PASS", "")
JOURNAL_UNIT = os.getenv("JOURNAL_UNIT", "mailserver")
MAX_ERRORS = int(os.getenv("MAX_ERRORS_TO_ANALYZE", 50))
# Parse comma-separated keywords into a list
keyword_string = os.getenv("ERROR_KEYWORDS", "error,failed,warning,fatal,timeout")
KEYWORDS = [k.strip().lower() for k in keyword_string.split(',')]
# --- EXECUTION TOGGLES ---
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
RUN_NOW = os.getenv("RUN_NOW", "false").lower() in ("true", "1", "yes")
REPORT_TIME = os.getenv("REPORT_TIME", "08:00")
def debug_print(message):
"""Prints only if DEBUG=true in .env"""
if DEBUG:
print(f"[DEBUG] {message}")
def get_yesterdays_logs():
print(f"Fetching logs for '{JOURNAL_UNIT}' from yesterday...")
debug_print(f"Running command: journalctl -u {JOURNAL_UNIT} --since yesterday --until today")
result = subprocess.run(
['journalctl', '-u', JOURNAL_UNIT, '--since', 'yesterday', '--until', 'today'],
capture_output=True, text=True
)
lines = result.stdout.splitlines()
debug_print(f"Fetched {len(lines)} total lines from journalctl.")
return lines
def process_logs(logs):
total_lines = len(logs)
errors = []
debug_print(f"Filtering logs using keywords: {KEYWORDS}")
for line in logs:
line_lower = line.lower()
if any(keyword in line_lower for keyword in KEYWORDS):
errors.append(line)
debug_print(f"Found {len(errors)} matching lines before truncation.")
# Truncate to maximum allowed errors to save CPU time
errors = errors[-MAX_ERRORS:]
debug_print(f"Truncated to {len(errors)} lines for AI processing (MAX_ERRORS={MAX_ERRORS}).")
return total_lines, errors
def get_ai_summary(errors):
if not errors:
debug_print("Skipping AI API call because error list is empty.")
return "No errors or warnings were found in the logs yesterday. Everything looks healthy!"
print(f"Asking AI to summarize {len(errors)} errors... (This will take a few minutes)")
error_text = "\n".join(errors)
# Combine the system prompt from .env with the logs
prompt = f"{AI_SYSTEM_PROMPT}\n\nLOGS:\n{error_text}"
payload = {
"model": MODEL,
"prompt": prompt,
"stream": False
}
debug_print(f"Sending prompt to Ollama at {OLLAMA_API} using model '{MODEL}'")
debug_print(f"System Prompt used: {AI_SYSTEM_PROMPT}")
debug_print(f"Total payload length: {len(prompt)} characters.")
try:
response = requests.post(OLLAMA_API, json=payload)
debug_print(f"AI response status code: {response.status_code}")
return response.json().get('response', 'Error: AI returned empty response.')
except Exception as e:
debug_print(f"AI Connection Exception: {str(e)}")
return f"Failed to connect to local AI: {str(e)}"
def send_email_report(total_traffic, ai_summary):
if not MY_EMAIL or not FROM_EMAIL:
print("Skipping email: REPORT_TO_EMAIL or REPORT_FROM_EMAIL not set.")
return
print(f"Sending email report to {MY_EMAIL}...")
debug_print(f"SMTP Server: {SMTP_SERVER}:{SMTP_PORT}")
debug_print(f"SMTP Auth User: {'<configured>' if SMTP_USER else '<none>'}")
msg = EmailMessage()
msg['Subject'] = f"Daily AI Summary: {JOURNAL_UNIT}"
msg['From'] = FROM_EMAIL
msg['To'] = MY_EMAIL
body = (
f"DAILY SERVER REPORT: {JOURNAL_UNIT}\n"
f"----------------------------------------\n"
f"Total Log Lines Processed: {total_traffic}\n\n"
f"AI Analysis of Errors/Warnings:\n"
f"{ai_summary}\n"
)
msg.set_content(body)
try:
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
if SMTP_USER:
debug_print("Attempting STARTTLS and Login...")
server.starttls()
server.login(SMTP_USER, SMTP_PASS)
debug_print("Transmitting email payload...")
server.send_message(msg)
server.quit()
print("Email sent successfully!")
except Exception as e:
print(f"Failed to send email: {e}")
debug_print(f"Email Exception: {str(e)}")
def job():
print(f"\n--- Starting log analysis for {JOURNAL_UNIT} ---")
logs = get_yesterdays_logs()
total_traffic, errors = process_logs(logs)
summary = get_ai_summary(errors)
send_email_report(total_traffic, summary)
print("--- Analysis complete. ---")
if __name__ == "__main__":
print("Log Analyzer Container starting up...")
if DEBUG:
print("[DEBUG] Debug mode is ENABLED.")
# 1. Register the permanent daily schedule first
schedule.every().day.at(REPORT_TIME).do(job)
print(f"Job permanently scheduled to run daily at {REPORT_TIME}.")
# 2. Handle the immediate run if requested
if RUN_NOW:
print("RUN_NOW is enabled. Executing immediate startup run...")
job()
print("--- Immediate run finished. Returning to standard schedule. ---")
else:
print("RUN_NOW is disabled. Skipping immediate run.")
# 3. Enter the permanent background loop
print(f"Entering background scheduler loop. Waiting for {REPORT_TIME}...")
while True:
schedule.run_pending()
time.sleep(60)

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
requests
schedule

View File

@@ -0,0 +1,34 @@
[Unit]
Description=daily-ai-summary (virt-daily-ai-summary)
Requires=docker.service
After=docker.service
DefaultDependencies=no
[Service]
Type=simple
Environment="HOME=/root"
ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-daily-ai-summary 2>/dev/null || true'
ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-daily-ai-summary 2>/dev/null || true'
ExecStart=/usr/bin/env docker run \
--rm \
--name=virt-daily-ai-summary \
--log-driver=none \
--network=host \
--env-file=/virt/daily-ai-summary/env \
--mount type=bind,src=/var/log/journal,dst=/var/log/journal,ro \
--mount type=bind,src=/run/log/journal,dst=/run/log/journal,ro \
--mount type=bind,src=/etc/machine-id,dst=/etc/machine-id,ro \
--mount type=bind,src=/etc/localtime,dst=/etc/localtime,ro \
--mount type=bind,src=/etc/timezone,dst=/etc/timezone,ro \
virt-daily-ai-summary
ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-daily-ai-summary 2>/dev/null || true'
ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-daily-ai-summary 2>/dev/null || true'
Restart=always
RestartSec=30
SyslogIdentifier=virt-daily-ai-summary
[Install]
WantedBy=multi-user.target