675 lines
19 KiB
Bash
Executable File
675 lines
19 KiB
Bash
Executable File
#!/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
|