Commit All

This commit is contained in:
2025-10-30 13:33:08 +01:00
commit 3678efed07
31 changed files with 5536 additions and 0 deletions

674
deploy_rpi.sh Executable file
View File

@@ -0,0 +1,674 @@
#!/bin/bash
# Turmli Bar Calendar Tool - Raspberry Pi Zero Deployment Script
# Deploys the application as a systemd service without Docker
# Optimized for low resource usage on Raspberry Pi Zero
set -e # Exit on error
# ============================================================================
# Configuration
# ============================================================================
APP_NAME="turmli-calendar"
APP_DIR="/opt/turmli-calendar"
SERVICE_NAME="turmli-calendar.service"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}"
APP_USER="pi"
APP_PORT="8000"
PYTHON_VERSION="3"
REPO_DIR="$(dirname "$(readlink -f "$0")")"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# ============================================================================
# Helper Functions
# ============================================================================
print_header() {
echo
echo -e "${MAGENTA}╔════════════════════════════════════════════╗${NC}"
echo -e "${MAGENTA}║ Turmli Calendar - Raspberry Pi Deployment ║${NC}"
echo -e "${MAGENTA}╚════════════════════════════════════════════╝${NC}"
echo
}
print_section() {
echo
echo -e "${CYAN}━━━ $1 ━━━${NC}"
echo
}
print_success() {
echo -e "${GREEN}${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1"
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
}
print_info() {
echo -e "${BLUE}${NC} $1"
}
check_root() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root (use sudo)"
exit 1
fi
}
check_os() {
if ! grep -q "Raspbian\|Raspberry Pi OS" /etc/os-release 2>/dev/null; then
print_warning "This script is optimized for Raspberry Pi OS"
read -p "Continue anyway? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
}
# ============================================================================
# System Checks and Prerequisites
# ============================================================================
check_system() {
print_section "System Check"
# Check if running as root
check_root
# Check OS
check_os
# Check available memory
local mem_total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
if [ "$mem_total" -lt 400000 ]; then
print_warning "Low memory detected ($(($mem_total/1024))MB). This is expected for Pi Zero."
fi
# Check available disk space
local disk_free=$(df /opt 2>/dev/null | tail -1 | awk '{print $4}')
if [ "$disk_free" -lt 500000 ]; then
print_warning "Low disk space available. Need at least 500MB free."
print_info "Current free space: $(($disk_free/1024))MB"
fi
# Check /tmp space
local tmp_free=$(df /tmp 2>/dev/null | tail -1 | awk '{print $4}')
if [ "$tmp_free" -lt 200000 ]; then
print_warning "/tmp has limited space. Will use alternative temp directory."
fi
# Check Python
if ! command -v python${PYTHON_VERSION} &> /dev/null; then
print_error "Python ${PYTHON_VERSION} is not installed"
return 1
fi
print_success "Python $(python${PYTHON_VERSION} --version 2>&1 | cut -d' ' -f2) installed"
# Check pip
if ! python${PYTHON_VERSION} -m pip --version &> /dev/null; then
print_warning "pip not found, installing..."
apt-get update
apt-get install -y python3-pip
fi
print_success "pip installed"
# Check git (optional, for updates)
if command -v git &> /dev/null; then
print_success "git installed (optional)"
else
print_info "git not installed (optional, needed for updates)"
fi
return 0
}
install_dependencies() {
print_section "Installing System Dependencies"
# Update package list
print_info "Updating package list..."
apt-get update
# Install system dependencies
print_info "Installing system packages..."
apt-get install -y \
python3-dev \
python3-venv \
python3-pip \
build-essential \
libffi-dev \
libssl-dev \
libxml2-dev \
libxslt1-dev \
curl \
systemd
# Clean apt cache to free up space
apt-get clean
apt-get autoremove -y
print_success "System dependencies installed"
}
# ============================================================================
# Application Setup
# ============================================================================
create_app_directory() {
print_section "Creating Application Directory"
# Create directory
if [ -d "$APP_DIR" ]; then
print_warning "Directory $APP_DIR already exists"
read -p "Remove existing installation? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
print_info "Removing existing installation..."
systemctl stop ${SERVICE_NAME} 2>/dev/null || true
rm -rf "$APP_DIR"
else
print_error "Installation cancelled"
exit 1
fi
fi
mkdir -p "$APP_DIR"
print_success "Created $APP_DIR"
}
copy_application_files() {
print_section "Copying Application Files"
# Copy necessary files
print_info "Copying application files..."
cp -v "$REPO_DIR/main.py" "$APP_DIR/"
# Use RPi-optimized requirements if available, otherwise fallback to standard
if [ -f "$REPO_DIR/requirements-rpi.txt" ]; then
print_info "Using Raspberry Pi optimized requirements"
cp -v "$REPO_DIR/requirements-rpi.txt" "$APP_DIR/requirements.txt"
else
cp -v "$REPO_DIR/requirements.txt" "$APP_DIR/"
fi
# Copy logo if exists
if [ -f "$REPO_DIR/Vektor-Logo.svg" ]; then
cp -v "$REPO_DIR/Vektor-Logo.svg" "$APP_DIR/"
fi
# Create static directory
mkdir -p "$APP_DIR/static"
if [ -d "$REPO_DIR/static" ]; then
cp -r "$REPO_DIR/static/"* "$APP_DIR/static/" 2>/dev/null || true
fi
# Copy logo to static directory
if [ -f "$APP_DIR/Vektor-Logo.svg" ]; then
cp "$APP_DIR/Vektor-Logo.svg" "$APP_DIR/static/logo.svg"
fi
# Create empty cache file
touch "$APP_DIR/calendar_cache.json"
# Set ownership
chown -R ${APP_USER}:${APP_USER} "$APP_DIR"
print_success "Application files copied"
}
setup_python_environment() {
print_section "Setting Up Python Environment"
# Create virtual environment to isolate dependencies
print_info "Creating Python virtual environment..."
sudo -u ${APP_USER} python${PYTHON_VERSION} -m venv "$APP_DIR/venv"
# Upgrade pip in virtual environment
print_info "Upgrading pip..."
sudo -u ${APP_USER} "$APP_DIR/venv/bin/pip" install --upgrade pip wheel setuptools
# Set temporary directory to avoid filling up /tmp (which is in RAM on Pi)
export TMPDIR="$APP_DIR/tmp"
mkdir -p "$TMPDIR"
chown ${APP_USER}:${APP_USER} "$TMPDIR"
# Install requirements
print_info "Installing Python dependencies (this may take a while on Pi Zero)..."
print_warning "This process might take 10-20 minutes on a Raspberry Pi Zero"
# Install with optimizations for low memory and ARM architecture
sudo -u ${APP_USER} TMPDIR="$TMPDIR" "$APP_DIR/venv/bin/pip" install \
--no-cache-dir \
--prefer-binary \
--no-build-isolation \
--no-deps \
-r "$APP_DIR/requirements.txt"
# Install dependencies separately to handle failures better
sudo -u ${APP_USER} TMPDIR="$TMPDIR" "$APP_DIR/venv/bin/pip" install \
--no-cache-dir \
--prefer-binary \
--no-build-isolation \
-r "$APP_DIR/requirements.txt"
# Clean up temp directory
rm -rf "$TMPDIR"
print_success "Python environment set up"
}
# ============================================================================
# Systemd Service Setup
# ============================================================================
create_systemd_service() {
print_section "Creating Systemd Service"
# Create service file
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Turmli Bar Calendar Tool
Documentation=https://github.com/turmli/bar-calendar-tool
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${APP_DIR}
Environment="PATH=/opt/turmli-calendar/venv/bin:/home/turmli/.local/bin:/usr/local/bin:/usr/bin:/bin"
Environment="PYTHONPATH=${APP_DIR}"
Environment="TZ=Europe/Berlin"
# Use virtual environment Python with uvicorn
ExecStart=${APP_DIR}/venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port ${APP_PORT} --workers 1 --log-level info
# Restart policy
Restart=always
RestartSec=10
StartLimitInterval=200
StartLimitBurst=5
# Resource limits for Raspberry Pi Zero
MemoryMax=256M
CPUQuota=75%
# Security hardening
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=${APP_DIR}/calendar_cache.json ${APP_DIR}/static
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${APP_NAME}
[Install]
WantedBy=multi-user.target
EOF
print_success "Service file created at $SERVICE_FILE"
# Reload systemd
systemctl daemon-reload
print_success "Systemd configuration reloaded"
}
# ============================================================================
# Service Management Functions
# ============================================================================
start_service() {
print_section "Starting Service"
systemctl enable ${SERVICE_NAME}
systemctl start ${SERVICE_NAME}
# Wait for service to start
sleep 3
if systemctl is-active --quiet ${SERVICE_NAME}; then
print_success "Service started successfully"
# Test if application is responding
sleep 2
if curl -s -f "http://localhost:${APP_PORT}/api/events" > /dev/null 2>&1; then
print_success "Application is responding on port ${APP_PORT}"
else
print_warning "Service is running but not responding yet (may still be starting)"
fi
else
print_error "Failed to start service"
print_info "Check logs with: journalctl -u ${SERVICE_NAME} -n 50"
return 1
fi
}
stop_service() {
print_section "Stopping Service"
if systemctl is-active --quiet ${SERVICE_NAME}; then
systemctl stop ${SERVICE_NAME}
print_success "Service stopped"
else
print_info "Service is not running"
fi
}
restart_service() {
print_section "Restarting Service"
systemctl restart ${SERVICE_NAME}
sleep 2
if systemctl is-active --quiet ${SERVICE_NAME}; then
print_success "Service restarted successfully"
else
print_error "Failed to restart service"
return 1
fi
}
# ============================================================================
# Status and Monitoring
# ============================================================================
show_status() {
print_section "Service Status"
# Show service status
systemctl status ${SERVICE_NAME} --no-pager
echo
print_section "Application Health"
# Check if responding
if curl -s -f "http://localhost:${APP_PORT}/api/events" > /dev/null 2>&1; then
print_success "Application is healthy and responding"
# Get event count
local events=$(curl -s "http://localhost:${APP_PORT}/api/events" | python3 -c "import sys, json; data=json.load(sys.stdin); print(len(data.get('events', [])))" 2>/dev/null || echo "unknown")
print_info "Calendar has $events events"
else
print_error "Application is not responding"
fi
# Show resource usage
echo
print_section "Resource Usage"
local pid=$(systemctl show ${SERVICE_NAME} -p MainPID --value)
if [ "$pid" != "0" ]; then
if [ -f "/proc/$pid/status" ]; then
local mem_usage=$(grep VmRSS /proc/$pid/status | awk '{print $2/1024 " MB"}')
print_info "Memory usage: $mem_usage"
fi
fi
# Show recent logs
echo
print_section "Recent Logs"
journalctl -u ${SERVICE_NAME} -n 20 --no-pager
}
show_logs() {
print_section "Application Logs"
journalctl -u ${SERVICE_NAME} -f
}
# ============================================================================
# Update Function
# ============================================================================
update_application() {
print_section "Updating Application"
# Stop service
stop_service
# Backup current installation
print_info "Creating backup..."
cp -r "$APP_DIR" "${APP_DIR}.backup.$(date +%Y%m%d_%H%M%S)"
# Copy new files
print_info "Copying updated files..."
cp -v "$REPO_DIR/main.py" "$APP_DIR/"
# Use RPi-optimized requirements if available
if [ -f "$REPO_DIR/requirements-rpi.txt" ]; then
cp -v "$REPO_DIR/requirements-rpi.txt" "$APP_DIR/requirements.txt"
else
cp -v "$REPO_DIR/requirements.txt" "$APP_DIR/"
fi
if [ -f "$REPO_DIR/Vektor-Logo.svg" ]; then
cp -v "$REPO_DIR/Vektor-Logo.svg" "$APP_DIR/"
cp "$APP_DIR/Vektor-Logo.svg" "$APP_DIR/static/logo.svg"
fi
# Update dependencies with temp directory
print_info "Updating Python dependencies..."
export TMPDIR="$APP_DIR/tmp"
mkdir -p "$TMPDIR"
chown ${APP_USER}:${APP_USER} "$TMPDIR"
sudo -u ${APP_USER} TMPDIR="$TMPDIR" "$APP_DIR/venv/bin/pip" install \
--no-cache-dir \
--prefer-binary \
--no-build-isolation \
-r "$APP_DIR/requirements.txt"
# Clean up temp directory
rm -rf "$TMPDIR"
# Set ownership
chown -R ${APP_USER}:${APP_USER} "$APP_DIR"
# Restart service
start_service
print_success "Application updated successfully"
}
# ============================================================================
# Uninstall Function
# ============================================================================
uninstall() {
print_section "Uninstalling Application"
print_warning "This will remove the Turmli Calendar application"
read -p "Are you sure? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "Uninstall cancelled"
return
fi
# Stop and disable service
systemctl stop ${SERVICE_NAME} 2>/dev/null || true
systemctl disable ${SERVICE_NAME} 2>/dev/null || true
# Remove service file
rm -f "$SERVICE_FILE"
systemctl daemon-reload
# Remove application directory
rm -rf "$APP_DIR"
print_success "Application uninstalled"
}
# ============================================================================
# Performance Optimization for Pi Zero
# ============================================================================
optimize_for_pi_zero() {
print_section "Optimizing for Raspberry Pi Zero"
# Disable unnecessary services to free up resources
print_info "Checking for unnecessary services..."
local services_to_check="bluetooth cups avahi-daemon triggerhappy"
for service in $services_to_check; do
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
print_info "Consider disabling $service to free resources"
fi
done
# Configure swap if needed
local swap_total=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
if [ "$swap_total" -lt 100000 ]; then
print_warning "Low swap space detected. Consider increasing swap size."
print_info "Edit /etc/dphys-swapfile and set CONF_SWAPSIZE=256"
fi
# Set CPU governor for better performance
if [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ]; then
local governor=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor)
if [ "$governor" != "performance" ]; then
print_info "Current CPU governor: $governor"
print_info "Consider setting to 'performance' for better response times"
fi
fi
print_success "Optimization check complete"
}
# ============================================================================
# Main Installation Function
# ============================================================================
install() {
print_header
# Run checks
check_system || exit 1
# Install dependencies
install_dependencies
# Setup application
create_app_directory
copy_application_files
setup_python_environment
# Setup service
create_systemd_service
# Optimize for Pi Zero
optimize_for_pi_zero
# Start service
start_service
# Show final status
print_section "Installation Complete!"
print_success "Turmli Calendar is now running"
echo
print_info "Access the calendar at:"
print_info " http://$(hostname -I | cut -d' ' -f1):${APP_PORT}"
print_info " http://localhost:${APP_PORT}"
echo
print_info "Useful commands:"
print_info " sudo systemctl status ${SERVICE_NAME} - Check status"
print_info " sudo systemctl restart ${SERVICE_NAME} - Restart service"
print_info " sudo journalctl -u ${SERVICE_NAME} -f - View logs"
print_info " sudo $0 status - Show detailed status"
echo
}
# ============================================================================
# Command Line Interface
# ============================================================================
print_usage() {
echo "Usage: sudo $0 {install|start|stop|restart|status|logs|update|uninstall|optimize}"
echo
echo "Commands:"
echo " install - Full installation (first time setup)"
echo " start - Start the service"
echo " stop - Stop the service"
echo " restart - Restart the service"
echo " status - Show service status and health"
echo " logs - Follow application logs"
echo " update - Update application from current directory"
echo " uninstall - Remove application completely"
echo " optimize - Check and suggest optimizations for Pi Zero"
echo
echo "Examples:"
echo " sudo $0 install # Initial installation"
echo " sudo $0 status # Check if running properly"
echo " sudo $0 logs # View real-time logs"
echo
echo "Configuration:"
echo " Port: ${APP_PORT}"
echo " Directory: ${APP_DIR}"
echo " Service: ${SERVICE_NAME}"
}
# ============================================================================
# Main Script Entry Point
# ============================================================================
case "$1" in
install)
check_root
install
;;
start)
check_root
start_service
;;
stop)
check_root
stop_service
;;
restart)
check_root
restart_service
;;
status)
show_status
;;
logs)
show_logs
;;
update)
check_root
update_application
;;
uninstall)
check_root
uninstall
;;
optimize)
check_root
optimize_for_pi_zero
;;
*)
print_header
print_usage
exit 1
;;
esac
exit 0