From f492147b531510b5c44732a3d89b25e0b893b620 Mon Sep 17 00:00:00 2001 From: cpu Date: Sat, 11 Jul 2026 12:14:59 +0000 Subject: [PATCH] first commit --- .dockerignore | 25 +++++ .env.example | 26 ++++++ .gitignore | 1 + Dockerfile | 18 ++++ README.md | 0 daily_summary.py | 166 ++++++++++++++++++++++++++++++++++ requirements.txt | 2 + virt-daily-ai-summary.service | 34 +++++++ 8 files changed, 272 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 daily_summary.py create mode 100644 requirements.txt create mode 100644 virt-daily-ai-summary.service diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ab67e6d --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..70218c6 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b51e155 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/daily_summary.py b/daily_summary.py new file mode 100644 index 0000000..210f32e --- /dev/null +++ b/daily_summary.py @@ -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: {'' if SMTP_USER else ''}") + + 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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2d6ef25 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +schedule diff --git a/virt-daily-ai-summary.service b/virt-daily-ai-summary.service new file mode 100644 index 0000000..1166f10 --- /dev/null +++ b/virt-daily-ai-summary.service @@ -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