#!/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