Refactor installation and uninstallation scripts; enhance logging and error handling

- Removed Docker installation instructions from README.md as they are not yet functional.
- Simplified the installation script by removing the upgrade process and directly checking for existing installations.
- Added checks for required dependencies (python3, curl, python3-venv) during installation.
- Improved logging throughout the application, replacing print statements with logger calls.
- Enhanced email validation using regex and added error handling for invalid email addresses.
- Updated the uninstall script to provide options for complete or partial removal of Vigil.
- Created a logger service to handle logging to both console and log files.
- Updated requirements.txt to use newer versions of dependencies.
- Added versioning to the main application and provided a version option in the command line interface.
This commit is contained in:
Uthman Fatih 2025-11-14 22:58:55 +00:00
parent a08228f7ef
commit 312d3e9674
10 changed files with 382 additions and 448 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"idf.pythonInstallPath": "/usr/bin/python3"
}

View File

@ -13,8 +13,6 @@ Vigil is designed for stability and security, avoiding keeping credentials in th
- [Prerequisites](#prerequisites) - [Prerequisites](#prerequisites)
- [Installation and Running](#installation-and-running) - [Installation and Running](#installation-and-running)
- [Installation](#installation) - [Installation](#installation)
- [Using the install script (recommended)](#using-the-install-script-recommended)
- [Using Docker](#using-docker)
- [Usage](#usage) - [Usage](#usage)
- [Running Vigil](#running-vigil) - [Running Vigil](#running-vigil)
- [1. Scheduled Mode (serve)](#1-scheduled-mode-serve) - [1. Scheduled Mode (serve)](#1-scheduled-mode-serve)
@ -91,8 +89,6 @@ This will check for updates every hour and send an email to the provided email a
## Installation ## Installation
### Using the install script (recommended)
The easiest way to install Vigil is by using the interactive install script. The easiest way to install Vigil is by using the interactive install script.
This script automatically checks for required dependencies, offers to install any that are missing, and can optionally configure Vigil to start automatically on boot via **systemd**. This script automatically checks for required dependencies, offers to install any that are missing, and can optionally configure Vigil to start automatically on boot via **systemd**.
@ -126,24 +122,6 @@ sudo bash uninstall.sh
rm uninstall.sh rm uninstall.sh
``` ```
### Using docker
> [!WARNING]
> The docker run command will not work yet and needs to be updated when released.
> The docker section will also be updated to include compose examples.
If you prefer to use Docker, you can download the latest Docker image from the [Docker Hub](https://hub.docker.com/r/uthmn/vigil)
```bash
docker pull uthmn/vigil
```
You can now run Vigil using the following command:
```bash
docker run -it --rm uthmn/vigil
```
## Usage ## Usage
Once installed, Vigil can be run in two modes: Once installed, Vigil can be run in two modes:

View File

@ -10,75 +10,23 @@ fi
ACTUAL_USER=${SUDO_USER:-$USER} ACTUAL_USER=${SUDO_USER:-$USER}
USER_HOME=$(eval echo ~$ACTUAL_USER) USER_HOME=$(eval echo ~$ACTUAL_USER)
# Now check if we already have vigil installed # /opt/vigil = static files/code
if [[ -d "/opt/vigil" ]]; then # /etc/vigil = configurations
echo "Vigil is already installed." # /var/log/vigil = logs
echo "Would you like to upgrade? This will preserve your .env and users.json files. (y/n)"
read -r answer
if [[ "${answer,,}" == "y" ]]; then # Check if we are running on a supported system
echo "Backing up configuration files..."
# Create temporary backup directory
BACKUP_DIR="/tmp/vigil_backup_$(date +%s)"
mkdir -p "$BACKUP_DIR"
# Backup .env and users.json if they exist
if [[ -f "/opt/vigil/.env" ]]; then
cp /opt/vigil/.env "$BACKUP_DIR/.env"
echo "Backed up .env"
fi
if [[ -f "/opt/vigil/users.json" ]]; then
cp /opt/vigil/users.json "$BACKUP_DIR/users.json"
echo "Backed up users.json"
fi
# Stop the service if it's running
if systemctl is-active --quiet vigil.service; then
echo "Stopping vigil service..."
systemctl stop vigil.service
fi
# Remove the old installation
echo "Removing old installation..."
rm -rf /opt/vigil
echo "Upgrade preparation complete. Proceeding with fresh installation..."
echo ""
# Set flag to restore config later
UPGRADE_MODE=true
else
echo "Installation cancelled. Please remove /opt/vigil manually if you want a fresh install."
exit 1
fi
fi
# Now check if we are running on a supported system
if [[ "$OSTYPE" != "linux-gnu"* ]]; then if [[ "$OSTYPE" != "linux-gnu"* ]]; then
echo "This script is only supported on Linux" 1>&2 echo "This script is only supported on Linux" 1>&2
exit 1 exit 1
fi fi
# Check if we have apt installed # Now check if we have apt installed
if ! command -v apt &> /dev/null; then if ! command -v apt &> /dev/null; then
echo "Apt is not installed. Please install it before running this script." 1>&2 echo "Apt is not installed. Please install it before running this script." 1>&2
exit 1 exit 1
fi fi
# Now check if we have git and python3 installed, if not offer to install them # Now check if we have python3 installed, if not offer to install it
if ! command -v git &> /dev/null; then
echo "Git is not installed. Do you want to install it now? (y/n)"
read -r answer
if [[ "${answer,,}" == "y" ]]; then
apt install git -y
else
echo "Please install git before running this script." 1>&2
exit 1
fi
fi
if ! command -v python3 &> /dev/null; then if ! command -v python3 &> /dev/null; then
echo "Python 3 is not installed. Do you want to install it now? (y/n)" echo "Python 3 is not installed. Do you want to install it now? (y/n)"
read -r answer read -r answer
@ -90,7 +38,19 @@ if ! command -v python3 &> /dev/null; then
fi fi
fi fi
# Now check if we have python3 -m pip installed, if not offer to install it # Now check if we have curl installed, if not offer to install it
if ! command -v curl &> /dev/null; then
echo "Curl is not installed. Do you want to install it now? (y/n)"
read -r answer
if [[ "${answer,,}" == "y" ]]; then
apt install curl -y
else
echo "Please install curl before running this script." 1>&2
exit 1
fi
fi
# Now check if we have python3-pip installed, if not offer to install it
if ! python3 -m pip --version &> /dev/null; then if ! python3 -m pip --version &> /dev/null; then
echo "pip is not installed. Do you want to install it now? (y/n)" echo "pip is not installed. Do you want to install it now? (y/n)"
read -r answer read -r answer
@ -102,12 +62,76 @@ if ! python3 -m pip --version &> /dev/null; then
fi fi
fi fi
# Now git clone the repository to /opt/vigil # Now check if we have python3-venv installed, if not offer to install it
echo "" if ! python3 -m venv &> /dev/null; then
echo "Cloning Vigil repository..." echo "Python3-venv is not installed. Do you want to install it now? (y/n)"
git clone https://git.uthmn.com/ufatih/vigil.git /opt/vigil read -r answer
if [[ "${answer,,}" == "y" ]]; then
apt install python3-venv -y
else
echo "Please install python3-venv before running this script." 1>&2
exit 1
fi
fi
# Now create a virtual environment and install the required packages if [[ -d "/opt/vigil" ]]; then
echo "Vigil is already installed."
echo "Would you like to upgrade? This will preserve your configuration. (y/n)"
read -r answer
if [[ "${answer,,}" == "y" ]]; then
echo "Stopping vigil service..."
systemctl stop vigil 2>/dev/null
echo "Removing old installation..."
rm -rf /opt/vigil
# Continue with installation
else
echo "Installation cancelled."
exit 0
fi
fi
# First create the required directories
echo ""
# Creating the /opt/vigil directory
echo "Creating /opt/vigil directory..."
mkdir -p /opt/vigil
# Create the /opt/vigil/services directory
echo "Creating /opt/vigil/services directory..."
mkdir -p /opt/vigil/services
# Create the /etc/vigil directory
echo "Creating /etc/vigil directory..."
mkdir -p /etc/vigil
# Create the /var/log/vigil directory
echo "Creating /var/log/vigil directory..."
mkdir -p /var/log/vigil
# Now curl -fsSL each file into the /opt/vigil directory
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/LICENSE -o /opt/vigil/LICENSE || { echo "Failed to download LICENSE file."; exit 1; }
echo " ✓ LICENSE"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/README.md -o /opt/vigil/README.md || { echo "Failed to download README.md file."; exit 1; }
echo " ✓ README.md"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/install.sh -o /opt/vigil/install.sh || { echo "Failed to download install.sh file."; exit 1; }
echo " ✓ install.sh"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/uninstall.sh -o /opt/vigil/uninstall.sh || { echo "Failed to download uninstall.sh file."; exit 1; }
echo " ✓ uninstall.sh"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/requirements.txt -o /opt/vigil/requirements.txt || { echo "Failed to download requirements.txt file."; exit 1; }
echo " ✓ requirements.txt"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/main.py -o /opt/vigil/main.py || { echo "Failed to download main.py file."; exit 1; }
echo " ✓ main.py"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/services/apt.py -o /opt/vigil/services/apt.py || { echo "Failed to download services/apt.py file."; exit 1; }
echo " ✓ services/apt.py"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/services/mail.py -o /opt/vigil/services/mail.py || { echo "Failed to download services/mail.py file."; exit 1; }
echo " ✓ services/mail.py"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/services/logger.py -o /opt/vigil/services/logger.py || { echo "Failed to download services/logger.py file."; exit 1; }
echo " ✓ services/logger.py"
curl -fsSL https://git.uthmn.com/ufatih/vigil/raw/branch/main/services/__init__.py -o /opt/vigil/services/__init__.py || { echo "Failed to download services/__init__.py file."; exit 1; }
echo " ✓ services/__init__.py"
echo "✓ All files downloaded successfully"
# Now create a venv and install the required packages
echo "" echo ""
echo "Setting up virtual environment and installing dependencies..." echo "Setting up virtual environment and installing dependencies..."
python3 -m venv /opt/vigil/.venv python3 -m venv /opt/vigil/.venv
@ -115,221 +139,18 @@ source /opt/vigil/.venv/bin/activate
pip install -r /opt/vigil/requirements.txt pip install -r /opt/vigil/requirements.txt
deactivate deactivate
# Now create a .env file and request the user to fill in the required values # Create a systemd service
echo ""
echo "=== Email Configuration ==="
echo ""
# Check if we're in upgrade mode and have a backup echo "Creating systemd service..."
if [[ "$UPGRADE_MODE" == true ]] && [[ -f "$BACKUP_DIR/.env" ]]; then cat > /etc/systemd/system/vigil.service << 'EOF'
echo "Found existing .env configuration. Would you like to:"
echo "1) Keep existing configuration"
echo "2) Enter new configuration"
read -r config_choice
if [[ "$config_choice" == "1" ]]; then
cp "$BACKUP_DIR/.env" /opt/vigil/.env
echo "Restored existing .env configuration"
chmod 600 /opt/vigil/.env
# Skip to connection testing
SKIP_ENV_INPUT=true
fi
fi
if [[ "$SKIP_ENV_INPUT" != true ]]; then
touch /opt/vigil/.env
# SMTP settings
echo "Please enter SMTP server address:"
read -r SMTP_SERVER
echo "Please enter SMTP port (default: 587 for TLS, 465 for SSL):"
read -r SMTP_PORT
echo "Please enter SMTP username:"
read -r SMTP_USERNAME
echo "Please enter SMTP password:"
read -rs SMTP_PASSWORD
echo ""
# IMAP settings
echo "Please enter IMAP server address:"
read -r IMAP_SERVER
echo "Please enter IMAP port (default: 993 for SSL):"
read -r IMAP_PORT
echo "Please enter IMAP username:"
read -r IMAP_USERNAME
echo "Please enter IMAP password:"
read -rs IMAP_PASSWORD
echo ""
# Write to .env file
cat > /opt/vigil/.env << EOF
SMTP_SERVER=$SMTP_SERVER
SMTP_PORT=$SMTP_PORT
SMTP_USERNAME=$SMTP_USERNAME
SMTP_PASSWORD=$SMTP_PASSWORD
IMAP_SERVER=$IMAP_SERVER
IMAP_PORT=$IMAP_PORT
IMAP_USERNAME=$IMAP_USERNAME
IMAP_PASSWORD=$IMAP_PASSWORD
EOF
echo "Configuration saved to /opt/vigil/.env"
# Set secure permissions on .env file (contains passwords)
chmod 600 /opt/vigil/.env
fi
# Test if the inputs work/are valid, else tell the user to fill in later and continue
if [[ "$SKIP_ENV_INPUT" != true ]]; then
echo ""
echo "Testing connections..."
# Check if required commands are available
if command -v nc >/dev/null 2>&1; then
# Test SMTP connection with netcat (timeout after 5 seconds)
echo "Testing SMTP connection to $SMTP_SERVER:$SMTP_PORT..."
if timeout 5 nc -zv "$SMTP_SERVER" "$SMTP_PORT" 2>&1 | grep -q succeeded; then
echo "[OK] SMTP connection successful"
SMTP_VALID=true
else
echo "[FAILED] SMTP connection failed - could not reach $SMTP_SERVER:$SMTP_PORT"
SMTP_VALID=false
fi
echo ""
# Test IMAP connection
echo "Testing IMAP connection to $IMAP_SERVER:$IMAP_PORT..."
if timeout 5 nc -zv "$IMAP_SERVER" "$IMAP_PORT" 2>&1 | grep -q succeeded; then
echo "[OK] IMAP connection successful"
IMAP_VALID=true
else
echo "[FAILED] IMAP connection failed - could not reach $IMAP_SERVER:$IMAP_PORT"
IMAP_VALID=false
fi
else
echo "[WARNING] Cannot test connections (nc not installed). Skipping validation."
SMTP_VALID=unknown
IMAP_VALID=unknown
fi
# Summary and continue
echo ""
if [ "$SMTP_VALID" = false ] || [ "$IMAP_VALID" = false ]; then
echo "[WARNING] Some connections failed."
echo "Configuration has been saved to /opt/vigil/.env"
echo "Please verify your settings and update the file manually if needed."
echo ""
read -p "Press Enter to continue..." -r
else
echo "[OK] All connections successful!"
echo "Configuration saved to /opt/vigil/.env"
fi
else
echo ""
echo "[OK] Using existing email configuration"
fi
# Now create a users.json file and request the user to fill in the required values (or leave empty to skip)
echo ""
echo "=== Notification Recipients ==="
echo ""
# Check if we're in upgrade mode and have a backup
if [[ "$UPGRADE_MODE" == true ]] && [[ -f "$BACKUP_DIR/users.json" ]]; then
echo "Found existing users.json configuration. Would you like to:"
echo "1) Keep existing recipients"
echo "2) Enter new recipients"
read -r users_choice
if [[ "$users_choice" == "1" ]]; then
cp "$BACKUP_DIR/users.json" /opt/vigil/users.json
echo "Restored existing notification recipients"
# Clean up backup directory
rm -rf "$BACKUP_DIR"
# Skip to next section
SKIP_USERS_INPUT=true
fi
fi
if [[ "$SKIP_USERS_INPUT" != true ]]; then
echo "Please enter the email addresses vigil will send notifications to (comma separated):"
echo "(Leave empty to skip)"
read -r EMAILS
# Strip whitespace and convert to JSON array
if [ -n "$EMAILS" ]; then
# Remove all whitespace, split by comma, and build JSON array
EMAILS_CLEAN=$(echo "$EMAILS" | tr -d '[:space:]')
# Convert to JSON array format
EMAILS_JSON="["
IFS=',' read -ra EMAIL_ARRAY <<< "$EMAILS_CLEAN"
for i in "${!EMAIL_ARRAY[@]}"; do
if [ $i -gt 0 ]; then
EMAILS_JSON="$EMAILS_JSON,"
fi
EMAILS_JSON="$EMAILS_JSON\"${EMAIL_ARRAY[$i]}\""
done
EMAILS_JSON="$EMAILS_JSON]"
echo "$EMAILS_JSON" > /opt/vigil/users.json
echo "Notification recipients saved to /opt/vigil/users.json"
else
echo "[]" > /opt/vigil/users.json
echo "No recipients configured. You can add them later in /opt/vigil/users.json"
fi
# Clean up backup directory if it exists
if [[ -n "$BACKUP_DIR" ]] && [[ -d "$BACKUP_DIR" ]]; then
rm -rf "$BACKUP_DIR" # Don't want to hit a steam
fi
fi
# Set ownership of vigil directory to actual user
chown -R "$ACTUAL_USER:$ACTUAL_USER" /opt/vigil
# Create the alias to the command
echo ""
echo "Adding 'vigil' command alias..."
ALIAS_LINE="alias vigil='source /opt/vigil/.venv/bin/activate && python3 /opt/vigil/main.py && deactivate'"
# Add to user's bashrc if not already present
if ! grep -q "alias vigil=" "$USER_HOME/.bashrc" 2>/dev/null; then
echo "$ALIAS_LINE" >> "$USER_HOME/.bashrc"
echo "Alias added to $USER_HOME/.bashrc"
else
echo "Alias already exists in $USER_HOME/.bashrc"
fi
# Suggest to add vigil to systemctl to autostart (after network is up)
echo ""
echo "Would you like to add vigil to autostart on boot? (y/n)"
read -r answer
if [[ "${answer,,}" == "y" ]]; then
# Create systemd service file
cat > /etc/systemd/system/vigil.service << EOF
[Unit] [Unit]
Description=Vigil Monitoring Service Description=Vigil - APT Update Monitor
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
[Service] [Service]
Type=simple Type=simple
User=$ACTUAL_USER ExecStart=/opt/vigil/.venv/bin/python /opt/vigil/main.py serve --check-daily 18
WorkingDirectory=/opt/vigil
Environment="PATH=/opt/vigil/.venv/bin"
ExecStart=/opt/vigil/.venv/bin/python3 /opt/vigil/main.py
Restart=on-failure Restart=on-failure
RestartSec=10 RestartSec=10
@ -337,26 +158,53 @@ RestartSec=10
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
# Reload systemd, enable and start the service systemctl daemon-reload
systemctl daemon-reload echo "✓ Systemd service created (not enabled or started)"
systemctl enable vigil.service
systemctl start vigil.service
echo "[OK] Vigil service has been created and started" # Create a command to run vigil
echo "You can check its status with: systemctl status vigil" echo "Creating vigil command..."
cat > /usr/local/bin/vigil << 'EOF'
#!/bin/bash
exec /opt/vigil/.venv/bin/python /opt/vigil/main.py "$@"
EOF
chmod +x /usr/local/bin/vigil
echo "✓ Command 'vigil' available system-wide"
echo ""
echo "✓ Vigil installed successfully!"
echo ""
# Check what needs to be configured
NEEDS_CONFIG=false
if [[ ! -f /etc/vigil/.env ]]; then
echo "⚠️ Missing: /etc/vigil/.env"
echo " Create this file with your SMTP credentials"
echo " See README for template: /opt/vigil/README.md"
echo ""
NEEDS_CONFIG=true
fi
if [[ ! -f /etc/vigil/users.json ]]; then
echo "⚠️ Missing: /etc/vigil/users.json"
echo " Create this file with recipient email addresses"
echo " Example: [\"admin@example.com\"]"
echo ""
NEEDS_CONFIG=true
fi
if [[ "$NEEDS_CONFIG" == "true" ]]; then
echo "Next steps:"
echo " 1. Configure the files above"
echo " 2. Test: sudo vigil now"
echo " 3. Enable service: sudo systemctl enable --now vigil"
else else
echo "Skipping autostart configuration" echo "✓ Configuration files found"
echo ""
echo "Next steps:"
echo " 1. Test: sudo vigil now"
echo " 2. Enable service: sudo systemctl enable --now vigil"
fi fi
echo "" echo ""
echo "========================================"
echo "Vigil has been installed successfully!"
echo "========================================"
echo ""
echo "To use vigil, run: source ~/.bashrc && vigil"
echo "Or simply open a new terminal and run: vigil"
echo ""
if [[ "${answer,,}" == "y" ]]; then
echo "Vigil is now running as a system service."
echo "Use 'systemctl status vigil' to check its status"
fi

160
main.py
View File

@ -11,27 +11,58 @@ from typing import Union
import services.apt import services.apt
import services.mail import services.mail
import html as hypertext
import re
from socket import gethostname from socket import gethostname
import json import json
from os.path import dirname, join, exists from os.path import exists
from services.logger import logger
import typer import typer
import signal
# Its beautiful ;)
EMAIL_REGEX = re.compile(r"""
(?:[a-z0-9!#$%&'*+\x2f=?^_`\x7b-\x7d~\x2d]+(?:\.[a-z0-9!#$%&'*+\x2f=?^_`\x7b-\x7d~\x2d]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9\x2d]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9\x2d]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9\x2d]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
""", re.VERBOSE | re.IGNORECASE)
# Change for version of Vigil
__version__ = "1.0.0"
shutdown_flag = False
def handle_shutdown(signum, frame):
global shutdown_flag
logger.info("Shutdown signal received; finishing current cycle and exiting...")
shutdown_flag = True
signal.signal(signal.SIGINT, handle_shutdown)
signal.signal(signal.SIGTERM, handle_shutdown)
app = typer.Typer() app = typer.Typer()
def wait_until(target_time): def wait_until_interruptible(target_time):
now = datetime.now() global shutdown_flag
seconds_to_wait = (target_time - now).total_seconds() while not shutdown_flag:
if seconds_to_wait > 0: now = datetime.now()
time.sleep(seconds_to_wait) remaining = (target_time - now).total_seconds()
if remaining <= 0:
return
# sleep in small chunks so shutdown is responsive
time.sleep(min(remaining, 0.5))
def schedule(mode, hour, minute=0, receiver_email=None): def schedule(mode, hour, minute=0, receiver_email=None):
print(f"Scheduler started at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Mode: {mode}") global shutdown_flag
while True: logger.info(f"Scheduler started at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Mode: {mode}")
while not shutdown_flag:
try: try:
now = datetime.now() now = datetime.now()
if mode == "daily": if mode == "daily":
target = now.replace(hour=hour, minute=minute, second=0, microsecond=0) target = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
if target <= now: if target <= now:
@ -43,11 +74,19 @@ def schedule(mode, hour, minute=0, receiver_email=None):
else: else:
raise ValueError("Mode must be 'daily' or 'hourly'.") raise ValueError("Mode must be 'daily' or 'hourly'.")
wait_until(target) logger.info(f"Next check scheduled for {target.strftime('%Y-%m-%d %H:%M:%S')}")
wait_until_interruptible(target)
if shutdown_flag:
break
generate_email(receiver_email) generate_email(receiver_email)
except Exception as e: except Exception as e:
print(f"Error in scheduled task: {e}") logger.error(f"Error in scheduled task: {e}", exc_info=True)
time.sleep(60) # Wait before retrying time.sleep(60)
logger.info("Scheduler exited cleanly.")
@app.command() @app.command()
def serve(receiver_email: str = None, check_hourly: bool = False, check_daily: str = "18"): def serve(receiver_email: str = None, check_hourly: bool = False, check_daily: str = "18"):
@ -58,7 +97,7 @@ def serve(receiver_email: str = None, check_hourly: bool = False, check_daily: s
- check_daily=int hour of day for daily email - check_daily=int hour of day for daily email
""" """
if check_hourly and check_daily: if check_hourly and check_daily not in [False, "false", "False", None]:
check_daily = False check_daily = False
if check_hourly: if check_hourly:
@ -71,11 +110,11 @@ def serve(receiver_email: str = None, check_hourly: bool = False, check_daily: s
try: try:
hour = int(check_daily) hour = int(check_daily)
except ValueError: except ValueError:
print(f"Invalid check_daily value: {check_daily}") logger.error(f"Invalid check_daily value: {check_daily}")
exit(1) raise typer.Exit(1)
schedule("daily", hour, receiver_email=receiver_email) schedule("daily", hour, receiver_email=receiver_email)
else: else:
print("No schedule selected.") logger.error("No schedule selected.")
@app.command() @app.command()
def now(receiver_email: str = None): def now(receiver_email: str = None):
@ -87,25 +126,13 @@ def now(receiver_email: str = None):
def generate_email(receiver_emails: Union[list, None]): def generate_email(receiver_emails: Union[list, None]):
services.apt.require_root() services.apt.require_root()
if not services.apt.detect_apt(): if not services.apt.detect_apt():
print("Apt not found on this system.") logger.error("Apt not found on this system.")
exit(1) raise typer.Exit(1)
updates = services.apt.check_updates() try:
updates = services.apt.check_updates()
# For testing except Exception as e:
#updates = { logger.error(f"Failed to check for updates: {e}", exc_info=True)
# "vim": { raise typer.Exit(1)
# "installed_version": "8.2.3400",
# "latest_version": "8.2.3400",
# "repo": "universe",
# "security": True
# },
# "vim-gtk3": {
# "installed_version": "8.2.3400",
# "latest_version": "8.2.3400",
# "repo": "universe",
# "security": False
# }
#}
receiver_email = [] receiver_email = []
if receiver_emails: if receiver_emails:
@ -116,13 +143,27 @@ def generate_email(receiver_emails: Union[list, None]):
# If no CLI emails are provided # If no CLI emails are provided
if not receiver_email: if not receiver_email:
users_json_path = join(dirname(__file__), "users.json") users_json_path = "/etc/vigil/users.json"
if exists(users_json_path): if exists(users_json_path):
with open(users_json_path, "r") as f: try:
receiver_email = json.load(f) with open(users_json_path, "r") as f:
data = json.load(f)
if not isinstance(data, list):
logger.error("users.json must contain a list of email addresses")
raise typer.Exit(1)
receiver_email = [str(email).strip() for email in data]
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON in users.json: {e}")
raise typer.Exit(1)
else: else:
print("No email address provided and users.json not found.") logger.error("No email address provided and /etc/vigil/users.json not found.")
exit(1) raise typer.Exit(1)
# Validate email address(es)
for email in receiver_email:
if not EMAIL_REGEX.match(email):
logger.error(f"Invalid email address: {email}")
raise typer.Exit(1)
# Get how many security updates are available # Get how many security updates are available
security_updates = 0 security_updates = 0
@ -137,7 +178,7 @@ def generate_email(receiver_emails: Union[list, None]):
# Check if there are any updates at all # Check if there are any updates at all
if total_updates == 0: if total_updates == 0:
print("No updates available.") logger.info("No updates available.")
return return
# Get system hostname # Get system hostname
@ -154,10 +195,10 @@ def generate_email(receiver_emails: Union[list, None]):
continue continue
chunk = f''' chunk = f'''
<tr> <tr>
<td style="border: 1px solid #ddd; padding: 8px;">{package}</td> <td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(package)}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["installed_version"]}</td> <td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["installed_version"])}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["latest_version"]}</td> <td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["latest_version"])}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["repo"]}</td> <td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["repo"])}</td>
</tr> </tr>
''' '''
security_chunks += chunk security_chunks += chunk
@ -185,10 +226,10 @@ def generate_email(receiver_emails: Union[list, None]):
continue continue
chunk = f''' chunk = f'''
<tr> <tr>
<td style="border: 1px solid #ddd; padding: 8px;">{package}</td> <td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(package)}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["installed_version"]}</td> <td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["installed_version"])}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["latest_version"]}</td> <td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["latest_version"])}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{updates[package]["repo"]}</td> <td style="border: 1px solid #ddd; padding: 8px;">{hypertext.escape(updates[package]["repo"])}</td>
</tr> </tr>
''' '''
general_chunks += chunk general_chunks += chunk
@ -210,10 +251,29 @@ def generate_email(receiver_emails: Union[list, None]):
html = security + general html = security + general
services.mail.send_email(receiver_email, subject, html) try:
services.mail.send_email(receiver_email, subject, html)
logger.info(f"Update notification sent: {security_updates} security, {general_updates} general updates to {len(receiver_email)} recipient(s)")
except Exception as e:
logger.error(f"Failed to send email notification: {e}", exc_info=True)
raise typer.Exit(1)
def version_callback(value: bool):
if value:
typer.echo(f"Vigil {__version__}")
raise typer.Exit()
@app.callback() @app.callback()
def callback(): def callback(
version: bool = typer.Option(
None,
"--version",
"-v",
callback=version_callback,
is_eager=True,
help="Show version and exit"
)
):
""" """
Checks apt for upgrades and emails them Checks apt for upgrades and emails them
""" """

View File

@ -3,8 +3,8 @@ dotenv==0.9.9
markdown-it-py==4.0.0 markdown-it-py==4.0.0
mdurl==0.1.2 mdurl==0.1.2
Pygments==2.19.2 Pygments==2.19.2
python-dotenv==1.1.1 python-dotenv==1.2.1
rich==14.2.0 rich==14.2.0
shellingham==1.5.4 shellingham==1.5.4
typer==0.19.2 typer==0.20.0
typing_extensions==4.15.0 typing_extensions==4.15.0

0
services/__init__.py Normal file
View File

View File

@ -6,13 +6,15 @@
# © Uthmn 2025 under MIT license # © Uthmn 2025 under MIT license
import subprocess import subprocess
import os from os import geteuid
import sys import sys
import re import re
from services.logger import logger
def require_root(): def require_root():
if os.geteuid() != 0: if geteuid() != 0:
print("This script requires root privileges. Please run with sudo.") logger.error("This script requires root privileges. Please run with sudo.")
sys.exit(1) sys.exit(1)
def detect_apt(): def detect_apt():
@ -74,4 +76,4 @@ if __name__ == "__main__":
require_root() require_root()
if detect_apt(): if detect_apt():
updates = check_updates() updates = check_updates()
print(updates) logger.info(updates)

46
services/logger.py Normal file
View File

@ -0,0 +1,46 @@
# _ __________________
# | | / / _/ ____/ _/ /
# | | / // // / __ / // /
# | |/ // // /_/ // // /___
# |___/___/\____/___/_____/
# © Uthmn 2025 under MIT license
import logging
from logging.handlers import RotatingFileHandler
from os import makedirs
from os.path import exists
# Setup logging once, globally
logger = logging.getLogger('vigil')
# Prevent duplicate setup
if not logger.handlers:
logger.setLevel(logging.INFO)
# Create log directory if it doesn't exist
log_dir = '/var/log/vigil'
if not exists(log_dir):
makedirs(log_dir, exist_ok=True)
# File handler with rotation
file_handler = RotatingFileHandler(
'/var/log/vigil/vigil.log',
maxBytes=10*1024*1024, # 10MB
backupCount=3
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
# Console handler (for systemd journal)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
logger.addHandler(file_handler)
logger.addHandler(console_handler)

View File

@ -13,16 +13,18 @@ from email import message_from_bytes
from dotenv import load_dotenv from dotenv import load_dotenv
from os import getenv from os import getenv
from os.path import dirname, join, exists from os.path import exists
import time import time
from services.logger import logger
# Exit if .env does not exist # Exit if .env does not exist
if not exists(join(dirname(__file__), "../.env")): if not exists("/etc/vigil/.env"):
print("Please create a .env file in the root directory.") logger.error("Please create a .env file in /etc/vigil.")
exit(1) exit(1)
# Load environment variables from .env file # Load environment variables from .env file
dotenv_path = join(dirname(__file__), "../.env") dotenv_path = "/etc/vigil/.env"
load_dotenv(dotenv_path) load_dotenv(dotenv_path)
# SMTP server settings # SMTP server settings
@ -43,7 +45,7 @@ RETRY_DELAY = 2 # in seconds
# Check if all environment variables are set # Check if all environment variables are set
if not SMTP_SERVER or not SMTP_PORT or not SMTP_USERNAME or not SMTP_PASSWORD or not IMAP_SERVER or not IMAP_PORT or not IMAP_USERNAME or not IMAP_PASSWORD: if not SMTP_SERVER or not SMTP_PORT or not SMTP_USERNAME or not SMTP_PASSWORD or not IMAP_SERVER or not IMAP_PORT or not IMAP_USERNAME or not IMAP_PASSWORD:
print("Please set all environment variables in .env file.") logger.error("Please set all environment variables in .env file.")
exit(1) exit(1)
# Make sure SMTP and IMAP ports are integers # Make sure SMTP and IMAP ports are integers
@ -60,11 +62,11 @@ def connect_smtp_with_retry():
except Exception as e: except Exception as e:
if attempt < MAX_RETRIES - 1: if attempt < MAX_RETRIES - 1:
wait_time = RETRY_DELAY * (2 ** attempt) # Exponential backoff wait_time = RETRY_DELAY * (2 ** attempt) # Exponential backoff
print(f"SMTP connection attempt {attempt + 1} failed: {e}") logger.error(f"SMTP connection attempt {attempt + 1} failed: {e}")
print(f"Retrying in {wait_time} seconds...") logger.info(f"Retrying in {wait_time} seconds...")
time.sleep(wait_time) time.sleep(wait_time)
else: else:
print(f"Failed to connect to SMTP after {MAX_RETRIES} attempts: {e}") logger.error(f"Failed to connect to SMTP after {MAX_RETRIES} attempts: {e}")
raise raise
def connect_imap_with_retry(): def connect_imap_with_retry():
@ -77,11 +79,11 @@ def connect_imap_with_retry():
except Exception as e: except Exception as e:
if attempt < MAX_RETRIES - 1: if attempt < MAX_RETRIES - 1:
wait_time = RETRY_DELAY * (2 ** attempt) # Exponential backoff wait_time = RETRY_DELAY * (2 ** attempt) # Exponential backoff
print(f"IMAP connection attempt {attempt + 1} failed: {e}") logger.error(f"IMAP connection attempt {attempt + 1} failed: {e}")
print(f"Retrying in {wait_time} seconds...") logger.info(f"Retrying in {wait_time} seconds...")
time.sleep(wait_time) time.sleep(wait_time)
else: else:
print(f"Failed to connect to IMAP after {MAX_RETRIES} attempts: {e}") logger.error(f"Failed to connect to IMAP after {MAX_RETRIES} attempts: {e}")
raise raise
# Test all connections # Test all connections
@ -89,14 +91,14 @@ try:
server = connect_smtp_with_retry() server = connect_smtp_with_retry()
server.quit() server.quit()
except Exception as e: except Exception as e:
print(f"Initial SMTP connection test failed: {e}") logger.error(f"Initial SMTP connection test failed: {e}")
exit(1) exit(1)
try: try:
M = connect_imap_with_retry() M = connect_imap_with_retry()
M.logout() M.logout()
except Exception as e: except Exception as e:
print(f"Initial IMAP connection test failed: {e}") logger.error(f"Initial IMAP connection test failed: {e}")
exit(1) exit(1)
def send_email(receiver_emails, subject, body): def send_email(receiver_emails, subject, body):
@ -113,11 +115,12 @@ def send_email(receiver_emails, subject, body):
server = connect_smtp_with_retry() server = connect_smtp_with_retry()
try: try:
server.sendmail(sender_email, receiver_emails, message.as_string()) server.sendmail(sender_email, receiver_emails, message.as_string())
print("HTML email sent successfully!") logger.info("HTML email sent successfully!")
finally: finally:
server.quit() server.quit()
except Exception as e: except Exception as e:
print(f"Failed to send email: {e}") logger.error(f"Failed to send email: {e}")
raise
def receive_emails(folder="INBOX", limit=50): def receive_emails(folder="INBOX", limit=50):
emails = {} emails = {}
@ -131,7 +134,7 @@ def receive_emails(folder="INBOX", limit=50):
# Get all message IDs # Get all message IDs
typ, data = M.search(None, 'ALL') typ, data = M.search(None, 'ALL')
if typ != 'OK' or not data[0]: if typ != 'OK' or not data[0]:
print("No messages found.") logger.info("No messages found.")
return emails return emails
all_ids = data[0].split() all_ids = data[0].split()
@ -140,7 +143,7 @@ def receive_emails(folder="INBOX", limit=50):
for num in last_ids: for num in last_ids:
typ, msg_data = M.fetch(num, '(RFC822)') typ, msg_data = M.fetch(num, '(RFC822)')
if typ != 'OK': if typ != 'OK':
print(f"Failed to fetch message {num}") logger.error(f"Failed to fetch message {num}")
continue continue
# Parse the email # Parse the email
@ -185,6 +188,6 @@ def receive_emails(folder="INBOX", limit=50):
M.logout() M.logout()
except Exception as e: except Exception as e:
print(f"Error receiving emails: {e}") logger.error(f"Error receiving emails: {e}")
return emails return emails

View File

@ -1,76 +1,70 @@
#!/bin/bash #!/bin/bash
# TODO: Add uninstall to readme # Check if running as root
# Just double check, check /opt/vigil exists, then remove it, also check+remove systemd service, remove aliases from bashrc and also dont offer but notify that dependencies not uninstalled
# Check if vigil is installed, if not then exit
if [[ ! -d "/opt/vigil" ]]; then
echo "Vigil is not installed. Exiting." 1>&2
exit 1
fi
# Check if we are running as root
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root" 1>&2 echo "This script must be run as root" 1>&2
exit 1 exit 1
fi fi
# Get the actual user (not root) for later use echo "This will uninstall Vigil from your system."
ACTUAL_USER=${SUDO_USER:-$USER}
USER_HOME=$(eval echo ~$ACTUAL_USER)
# Now check if we are running on a supported system
if [[ "$OSTYPE" != "linux-gnu"* ]]; then
echo "This script is only supported on Linux" 1>&2
exit 1
fi
# Warning and countdown
echo "WARNING: This will remove /opt/vigil and any related data."
echo "Press Ctrl+C to cancel..."
echo ""
for i in 5 4 3 2 1; do
echo -n "$i..."
sleep 1
done
echo "" echo ""
echo "What would you like to remove?"
echo " 1) Everything (code, config, and logs)"
echo " 2) Just the code (preserve /etc/vigil and /var/log/vigil)"
echo " 3) Cancel"
echo "" echo ""
read -p "Enter choice [1-3]: " choice
# Final confirmation case $choice in
echo "Continue with uninstallation? (y/n)" 1)
read -r answer REMOVE_ALL=true
if [[ "${answer,,}" != "y" ]]; then ;;
echo "Uninstallation cancelled." 2)
exit 0 REMOVE_ALL=false
fi ;;
3)
echo "Uninstall cancelled."
exit 0
;;
*)
echo "Invalid choice. Uninstall cancelled."
exit 1
;;
esac
# Now delete the vigil directory # Stop and disable service
echo "" echo ""
echo "Removing Vigil installation..." echo "Stopping vigil service..."
systemctl stop vigil 2>/dev/null
systemctl disable vigil 2>/dev/null
# Remove systemd service
echo "Removing systemd service..."
rm -f /etc/systemd/system/vigil.service
systemctl daemon-reload
# Remove command
echo "Removing vigil command..."
rm -f /usr/local/bin/vigil
# Remove code
echo "Removing code from /opt/vigil..."
rm -rf /opt/vigil rm -rf /opt/vigil
# Now delete the systemd service (if it exists) if [[ "$REMOVE_ALL" == "true" ]]; then
if [[ -f /etc/systemd/system/vigil.service ]]; then echo "Removing configuration from /etc/vigil..."
echo "Removing Vigil systemd service..." rm -rf /etc/vigil
systemctl stop vigil.service 2>/dev/null echo "Removing logs from /var/log/vigil..."
systemctl disable vigil.service 2>/dev/null rm -rf /var/log/vigil
rm /etc/systemd/system/vigil.service echo ""
systemctl daemon-reload echo "✓ Vigil completely removed from your system"
else else
echo "No systemd service found (skipping)" echo ""
fi echo "✓ Vigil code removed"
echo "✓ Configuration preserved in /etc/vigil"
# Now delete the aliases from bashrc echo "✓ Logs preserved in /var/log/vigil"
echo "Removing Vigil aliases from bashrc..." echo ""
if grep -q "alias vigil=" "$USER_HOME/.bashrc" 2>/dev/null; then echo "To reinstall: curl ... | sudo bash"
sed -i '/alias vigil=/d' "$USER_HOME/.bashrc"
echo "Aliases removed from $USER_HOME/.bashrc"
else
echo "No aliases found in bashrc"
fi fi
echo "" echo ""
echo "Dependencies not uninstalled. Please uninstall them manually if needed: python3, git, pip"
echo "Vigil has been uninstalled successfully."