Files
nebuleair_pro_4g/update_firmware.sh
PaulVua 46c73acb7e v1.10.1: OTA installe les deps pip + filtre lectures parasites CCS811
Découvert en vérif SSH sur nebuleair-pro100 : le timer CCS811 échouait en
ModuleNotFoundError car l'OTA fait git pull mais ne réinstallait jamais les
dépendances pip (installation_part1.sh ne tourne qu'à l'install neuve).

- requirements.txt: source unique de vérité des deps Python
- installation_part1.sh: install via requirements.txt (chemin relatif au script,
  le repo n'est pas encore cloné dans /var/www à cette étape)
- update_firmware.sh: nouvelle étape 2a, pip install -r requirements.txt
  (idempotent) -> les capteurs déjà déployés récupèrent les libs manquantes à l'OTA
- CCS811/write_data.py + get_data.py: skip des lectures eCO2 < 400 ppm
  (échantillon 0/0 parasite juste après init du driver, plancher physique = 400)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:51:58 +02:00

286 lines
11 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
# 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 2a: Install/update Python dependencies (self-heal)
# OTA does a git pull but historically never (re)installed pip deps, so a new
# sensor lib introduced by an update (e.g. adafruit-circuitpython-ccs811) was
# missing on already-deployed sensors and the timer failed with ModuleNotFound.
# requirements.txt is the single source of truth; pip skips already-satisfied
# packages so this is idempotent and only pulls newly-added libs.
print_status ""
print_status "Step 2a: Installing/updating Python dependencies..."
if [ -f "/var/www/nebuleair_pro_4g/requirements.txt" ]; then
sudo pip3 install -r /var/www/nebuleair_pro_4g/requirements.txt --break-system-packages
check_status "Python dependencies install"
else
print_status "⚠ requirements.txt not found, skipping dependency install"
fi
# Step 2: Update database (schema migration + config keys)
# create_db.py is idempotent (CREATE TABLE IF NOT EXISTS + ALTER TABLE ADD COLUMN
# wrapped in try/except). Required to add tables introduced after the sensor was
# initially provisioned (e.g. data_S88, data_NOISE.noise_status, ...).
# Without this step, OTA updates that add a new sensor table leave the timer running
# but every write fails with "no such table".
print_status ""
print_status "Step 2: Updating database (schema migration + config keys)..."
/usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/create_db.py
check_status "Database schema migration"
/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 '<Directory /var/www/>' "$APACHE_MAIN_CONF"; then
if ! sed -n '/<Directory \/var\/www\/>/,/<\/Directory>/p' "$APACHE_MAIN_CONF" | grep -q "AllowOverride All"; then
sed -i '/<Directory \/var\/www\/>/,/<\/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