#!/bin/bash
# NebuleAir Pro 4G - Comprehensive Update Script
# This script performs a complete system update including git pull,
# config initialization, and service management
# Non-interactive version for WebUI
echo "======================================"
echo "NebuleAir Pro 4G - Firmware Update"
echo "======================================"
echo "Started at: $(date)"
echo ""
# Set working directory
cd /var/www/nebuleair_pro_4g
# Ensure this script is executable
chmod +x /var/www/nebuleair_pro_4g/update_firmware.sh
# Function to print status messages
print_status() {
echo "[$(date '+%H:%M:%S')] $1"
}
# Function to check command success
check_status() {
if [ $? -eq 0 ]; then
print_status "✓ $1 completed successfully"
else
print_status "✗ $1 failed"
return 1
fi
}
# Step 1: Git operations
print_status "Step 1: Updating firmware from repository..."
# Disable filemode to prevent permission issues
git -C /var/www/nebuleair_pro_4g config core.fileMode false
check_status "Git fileMode disabled"
# Fetch latest changes
git fetch origin
check_status "Git fetch"
# Show current branch
print_status "Current branch: $(git branch --show-current)"
# Check for local changes
if [ -n "$(git status --porcelain)" ]; then
print_status "Warning: Local changes detected, stashing..."
git stash push -m "Auto-stash before update $(date)"
check_status "Git stash"
fi
# Pull latest changes
git pull origin $(git branch --show-current)
check_status "Git pull"
# Display firmware version
if [ -f "/var/www/nebuleair_pro_4g/VERSION" ]; then
print_status "Firmware version: $(cat /var/www/nebuleair_pro_4g/VERSION)"
fi
# Step 2: Update database configuration
print_status ""
print_status "Step 2: Updating database configuration..."
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/set_config.py
check_status "Database configuration update"
# Step 3: Check and fix file permissions
print_status ""
print_status "Step 3: Checking file permissions..."
sudo chmod +x /var/www/nebuleair_pro_4g/update_firmware.sh
sudo chmod 755 /var/www/nebuleair_pro_4g/sqlite/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/NPM/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/BME280/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/SARA/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/envea/*.py
sudo chmod 755 /var/www/nebuleair_pro_4g/MPPT/*.py 2>/dev/null
sudo chmod 755 /var/www/nebuleair_pro_4g/wifi/*.py 2>/dev/null
sudo chmod 755 /var/www/nebuleair_pro_4g/services/*.sh 2>/dev/null
check_status "File permissions update"
# Step 3b: Ensure Apache/PHP config allows file uploads
APACHE_MAIN_CONF="/etc/apache2/apache2.conf"
if grep -q '' "$APACHE_MAIN_CONF"; then
if ! sed -n '//,/<\/Directory>/p' "$APACHE_MAIN_CONF" | grep -q "AllowOverride All"; then
sed -i '//,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' "$APACHE_MAIN_CONF"
print_status "✓ AllowOverride All enabled for Apache"
APACHE_CHANGED=true
fi
fi
PHP_INI=$(php -r "echo php_ini_loaded_file();" 2>/dev/null)
if [ -n "$PHP_INI" ]; then
CURRENT_UPLOAD=$(grep -oP 'upload_max_filesize = \K\d+' "$PHP_INI" 2>/dev/null)
if [ -n "$CURRENT_UPLOAD" ] && [ "$CURRENT_UPLOAD" -lt 50 ]; then
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 50M/' "$PHP_INI"
sed -i 's/post_max_size = .*/post_max_size = 55M/' "$PHP_INI"
print_status "✓ PHP upload limits set to 50M"
APACHE_CHANGED=true
fi
fi
if [ "${APACHE_CHANGED:-false}" = true ]; then
systemctl reload apache2 2>/dev/null
print_status "✓ Apache reloaded"
fi
# Step 3c: Reconcile systemd services with the repo (self-heal)
# Re-running setup_services.sh ensures any service that was removed,
# masked, or never installed gets recreated from the canonical source.
# Idempotent: rewriting an existing service file with the same content
# is a no-op for already-running services.
# Note: git config core.fileMode=false strips the executable bit, so we
# chmod +x explicitly (same pattern as installation_part2.sh).
print_status ""
print_status "Step 3c: Reconciling systemd services with setup_services.sh..."
SETUP_SERVICES=/var/www/nebuleair_pro_4g/services/setup_services.sh
if [ -f "$SETUP_SERVICES" ]; then
sudo chmod +x "$SETUP_SERVICES"
sudo "$SETUP_SERVICES"
check_status "Setup services reconciliation"
else
print_status "⚠ setup_services.sh not found, skipping"
fi
# Step 3d: Bootstrap Tailscale (install + fetch authkey + enroll on tailnet)
# Self-heal for sensors deployed before v1.9.0 that never had Tailscale.
# - Installs the tailscale binary if missing.
# - Drops a sudoers rule in /etc/sudoers.d/ (safer than touching /etc/sudoers).
# - Fetches the preauth key from data.nebuleair.fr if not already on disk.
# HTTPS → HTTP fallback because data.nebuleair.fr may not have TLS yet.
# TODO: drop the HTTP fallback once the cert is in place.
# - Triggers the bootstrap script to enroll on the tailnet.
print_status ""
print_status "Step 3d: Bootstrap Tailscale..."
# 3d.1: Install tailscale binary if absent
if ! command -v tailscale >/dev/null 2>&1; then
print_status "Installing Tailscale..."
curl -fsSL https://tailscale.com/install.sh | sh
check_status "Tailscale install"
else
print_status "ℹ Tailscale already installed ($(tailscale version | head -1))"
fi
# 3d.2: Self-heal sudoers rule (for sensors installed pre-v1.9.0)
TAILSCALE_SUDOERS=/etc/sudoers.d/nebuleair-tailscale
if [ ! -f "$TAILSCALE_SUDOERS" ]; then
echo "www-data ALL=(ALL) NOPASSWD: /usr/bin/tailscale *" | sudo tee "$TAILSCALE_SUDOERS" > /dev/null
sudo chmod 0440 "$TAILSCALE_SUDOERS"
if sudo visudo -c -f "$TAILSCALE_SUDOERS" >/dev/null 2>&1; then
print_status "✓ Sudoers rule for tailscale added"
else
sudo rm -f "$TAILSCALE_SUDOERS"
print_status "⚠ Tailscale sudoers rule failed validation, removed"
fi
fi
# 3d.3: Fetch preauth key from data.nebuleair.fr if not already present
AUTHKEY_FILE=/etc/tailscale/authkey
if [ ! -s "$AUTHKEY_FILE" ]; then
deviceID=$(sqlite3 /var/www/nebuleair_pro_4g/sqlite/sensors.db \
"SELECT value FROM config_table WHERE key='deviceID'" 2>/dev/null)
if [ -z "$deviceID" ]; then
print_status "⚠ deviceID empty, cannot fetch tailscale authkey"
else
print_status "Fetching tailscale authkey for deviceID=$deviceID..."
URL_HTTPS="https://data.nebuleair.fr/pro_4G/get_tailscale_key.php?deviceID=${deviceID}"
URL_HTTP="http://data.nebuleair.fr/pro_4G/get_tailscale_key.php?deviceID=${deviceID}"
authkey=$(curl -fsS --max-time 30 "$URL_HTTPS" 2>/dev/null || \
curl -fsS --max-time 30 "$URL_HTTP" 2>/dev/null || true)
if [[ "$authkey" == hskey-auth-* ]]; then
sudo mkdir -p /etc/tailscale
echo "$authkey" | sudo tee "$AUTHKEY_FILE" > /dev/null
sudo chmod 600 "$AUTHKEY_FILE"
sudo chown root:root "$AUTHKEY_FILE"
print_status "✓ Tailscale authkey stored at $AUTHKEY_FILE"
else
print_status "⚠ Could not fetch tailscale authkey (server unreachable or invalid response)"
fi
fi
else
print_status "ℹ Tailscale authkey already present"
fi
# 3d.4: Trigger the bootstrap (idempotent, no-op if already enrolled)
BOOTSTRAP=/var/www/nebuleair_pro_4g/services/tailscale_bootstrap.sh
if [ -x "$BOOTSTRAP" ]; then
sudo "$BOOTSTRAP" || print_status "⚠ Tailscale bootstrap returned non-zero (see logs/tailscale_bootstrap.log)"
elif [ -f "$BOOTSTRAP" ]; then
sudo chmod +x "$BOOTSTRAP"
sudo "$BOOTSTRAP" || print_status "⚠ Tailscale bootstrap returned non-zero (see logs/tailscale_bootstrap.log)"
else
print_status "⚠ Bootstrap script not found at $BOOTSTRAP"
fi
# Step 4: Restart critical services if they exist
print_status ""
print_status "Step 4: Managing system services..."
# List of services to check and restart
services=(
"nebuleair-npm-data.timer"
"nebuleair-envea-data.timer"
"nebuleair-sara-data.timer"
"nebuleair-bme280-data.timer"
"nebuleair-mppt-data.timer"
"nebuleair-noise-data.timer"
"nebuleair-wifi-powersave.timer"
)
for service in "${services[@]}"; do
if systemctl list-unit-files | grep -q "$service"; then
# Check if service is enabled before restarting
if systemctl is-enabled --quiet "$service" 2>/dev/null; then
print_status "Restarting enabled service: $service"
sudo systemctl restart "$service"
if systemctl is-active --quiet "$service"; then
print_status "✓ $service is running"
else
print_status "⚠ $service failed to start"
fi
else
print_status "ℹ Service $service is disabled, skipping restart"
fi
else
print_status "ℹ Service $service not found (may not be installed)"
fi
done
# Step 5: System health check
print_status ""
print_status "Step 5: System health check..."
# Check disk space
disk_usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$disk_usage" -gt 90 ]; then
print_status "⚠ Warning: Disk usage is high ($disk_usage%)"
else
print_status "✓ Disk usage is acceptable ($disk_usage%)"
fi
# Check if database is accessible
if [ -f "/var/www/nebuleair_pro_4g/sqlite/sensors.db" ]; then
print_status "✓ Database file exists"
else
print_status "⚠ Warning: Database file not found"
fi
# Step 6: Final cleanup
print_status ""
print_status "Step 6: Cleaning up..."
sudo find /var/www/nebuleair_pro_4g/logs -name "*.log" -size +10M -exec truncate -s 0 {} \;
check_status "Log cleanup"
print_status ""
print_status "======================================"
print_status "Update completed successfully!"
print_status "======================================"
exit 0