Files
turmlibar-calendar/deploy_rpi.sh
2025-10-30 13:33:08 +01:00

675 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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