diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..b458d06 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# RemoteRig — Pi-side deploy script +# Deploys a new binary with backup, health-check, and automatic rollback. +# +# Usage: +# sudo ./deploy.sh [BINARY_PATH] [DEPLOY_PATH] [SERVICE_NAME] +# +# Defaults: +# BINARY_PATH = ./remoterig (new binary to deploy) +# DEPLOY_PATH = /opt/remoterig/remoterig +# SERVICE_NAME = remoterig +# +# Examples: +# # Deploy locally-built binary with defaults +# sudo ./deploy.sh ./remoterig +# +# # Custom paths +# sudo ./deploy.sh /tmp/remoterig-arm64 /opt/remoterig/remoterig remoterig + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Args +# --------------------------------------------------------------------------- +BINARY="${1:-remoterig}" +DEPLOY_PATH="${2:-/opt/remoterig/remoterig}" +SERVICE="${3:-remoterig}" +TIMESTAMP="$(date +%Y%m%d%H%M%S)" +BACKUP="${DEPLOY_PATH}.${TIMESTAMP}.bak" +MAX_BACKUPS=3 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +info() { echo "[INFO] $*"; } +ok() { echo "[OK] $*"; } +fail() { echo "[FAIL] $*" >&2; } + +# --------------------------------------------------------------------------- +# Pre-flight checks +# --------------------------------------------------------------------------- +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: must run as root (sudo ./deploy.sh ...)" >&2 + exit 1 +fi + +if [ ! -f "${BINARY}" ]; then + fail "Binary not found: ${BINARY}" + exit 1 +fi + +echo "==============================================" +echo " RemoteRig Deploy" +echo " Binary: ${BINARY}" +echo " Deploy path: ${DEPLOY_PATH}" +echo " Service: ${SERVICE}" +echo " Timestamp: ${TIMESTAMP}" +echo "==============================================" + +# --------------------------------------------------------------------------- +# 1. Backup existing binary +# --------------------------------------------------------------------------- +info "Backing up current binary..." +if [ -f "${DEPLOY_PATH}" ]; then + cp "${DEPLOY_PATH}" "${BACKUP}" + ok "Backed up to ${BACKUP}" +else + info "No existing binary at ${DEPLOY_PATH} — fresh install" +fi + +# --------------------------------------------------------------------------- +# 2. Deploy new binary +# --------------------------------------------------------------------------- +info "Deploying new binary..." +cp "${BINARY}" "${DEPLOY_PATH}" +chmod +x "${DEPLOY_PATH}" +ok "Binary installed at ${DEPLOY_PATH}" + +# --------------------------------------------------------------------------- +# 3. Reload systemd and restart service +# --------------------------------------------------------------------------- +info "Reloading systemd and restarting ${SERVICE}..." +systemctl daemon-reload + +# Restart (or start if not running) +if systemctl is-active --quiet "${SERVICE}" 2>/dev/null; then + systemctl restart "${SERVICE}" +else + systemctl start "${SERVICE}" +fi +ok "Service restart issued" + +# --------------------------------------------------------------------------- +# 4. Health check +# --------------------------------------------------------------------------- +info "Waiting 3s for service to stabilize..." +sleep 3 + +if systemctl is-active --quiet "${SERVICE}"; then + ok "${SERVICE} is active — deploy successful" + + # Optional: curl health endpoint + if command -v curl >/dev/null 2>&1; then + HEALTH_URL="http://localhost:8080/health" + if curl -sf --max-time 3 "${HEALTH_URL}" >/dev/null 2>&1; then + ok "Health check passed: ${HEALTH_URL}" + else + info "Health endpoint not reachable (may need more startup time)" + fi + fi +else + fail "${SERVICE} is NOT active — rolling back" + + # ----------------------------------------------------------------------- + # 5. Rollback on failure + # ----------------------------------------------------------------------- + if [ -f "${BACKUP}" ]; then + info "Restoring backup: ${BACKUP}" + cp "${BACKUP}" "${DEPLOY_PATH}" + chmod +x "${DEPLOY_PATH}" + + systemctl restart "${SERVICE}" 2>/dev/null || true + + sleep 2 + if systemctl is-active --quiet "${SERVICE}"; then + ok "Rollback successful — previous binary restored and service is active" + else + fail "Rollback failed — service still not active" + echo "Check logs: journalctl -u ${SERVICE} -n 50" >&2 + exit 1 + fi + else + fail "No backup available — cannot roll back" + echo "Check logs: journalctl -u ${SERVICE} -n 50" >&2 + exit 1 + fi +fi + +# --------------------------------------------------------------------------- +# 6. Cleanup old backups (keep last N) +# --------------------------------------------------------------------------- +info "Cleaning up old backups (keeping last ${MAX_BACKUPS})..." +DEPLOY_DIR="$(dirname "${DEPLOY_PATH}")" +BASE_NAME="$(basename "${DEPLOY_PATH}")" + +# List backups, skip current, keep last MAX_BACKUPS, delete the rest +ls -1t "${DEPLOY_DIR}/${BASE_NAME}."*.bak 2>/dev/null | \ + tail -n +$((MAX_BACKUPS + 1)) | \ + while IFS= read -r old_backup; do + rm -f "${old_backup}" + info "Removed old backup: $(basename "${old_backup}")" + done + +ok "Deploy complete — ${MAX_BACKUPS} backups retained" +echo "" diff --git a/scripts/remoterig.service b/scripts/remoterig.service new file mode 100644 index 0000000..cab5d1b --- /dev/null +++ b/scripts/remoterig.service @@ -0,0 +1,29 @@ +[Unit] +Description=RemoteRig Central Hub +Documentation=https://github.com/CubeCraft-Creations/remote-rig +After=network.target mosquitto.service +Wants=mosquitto.service + +[Service] +Type=simple +User=pi +WorkingDirectory=/opt/remoterig +ExecStart=/opt/remoterig/remoterig +Restart=on-failure +RestartSec=5s +StandardOutput=journal +StandardError=journal + +# Security hardening (optional, uncomment to enable) +# NoNewPrivileges=yes +# ProtectSystem=strict +# ProtectHome=yes +# ReadWritePaths=/opt/remoterig + +# Allow graceful shutdown +TimeoutStopSec=10s +KillMode=mixed +KillSignal=SIGTERM + +[Install] +WantedBy=multi-user.target diff --git a/scripts/setup-pi.sh b/scripts/setup-pi.sh new file mode 100755 index 0000000..650152a --- /dev/null +++ b/scripts/setup-pi.sh @@ -0,0 +1,336 @@ +#!/usr/bin/env bash +# RemoteRig — First-Time Raspberry Pi Zero 2 W Setup +# Target: Debian/Raspberry Pi OS (bookworm) +# Idempotent: safe to run multiple times +# +# Usage: +# sudo ./setup-pi.sh [--config PATH] [--service-user USER] +# +# Options: +# --config PATH Path to config.yaml template to copy to /opt/remoterig/ +# --service-user USER Systemd service user (default: pi) +# --static-ip IP Static IP for wlan0 (default: 192.168.4.10/24) +# --gateway IP Gateway for wlan0 (default: 192.168.4.1) +# --help Show this help + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Defaults +# --------------------------------------------------------------------------- +CONFIG_TEMPLATE="" +SERVICE_USER="pi" +STATIC_IP="192.168.4.10/24" +GATEWAY="192.168.4.1" +MOSQUITTO_PKG="mosquitto mosquitto-clients" +DEPLOY_DIR="/opt/remoterig" +SERVICE_NAME="remoterig" +SERVICE_FILE="scripts/remoterig.service" +MOSQUITTO_CONF="/etc/mosquitto/conf.d/remoterig.conf" + +# --------------------------------------------------------------------------- +# Help +# --------------------------------------------------------------------------- +usage() { + sed -n '/^# Usage:/,/^$/p' "$0" | sed 's/^# //' + exit 0 +} + +# --------------------------------------------------------------------------- +# Parse args +# --------------------------------------------------------------------------- +while [ $# -gt 0 ]; do + case "$1" in + --config) + CONFIG_TEMPLATE="$2" + shift 2 + ;; + --service-user) + SERVICE_USER="$2" + shift 2 + ;; + --static-ip) + STATIC_IP="$2" + shift 2 + ;; + --gateway) + GATEWAY="$2" + shift 2 + ;; + --help|-h) + usage + ;; + *) + echo "ERROR: unknown option: $1" >&2 + usage + ;; + esac +done + +# --------------------------------------------------------------------------- +# Pre-flight checks +# --------------------------------------------------------------------------- +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: must run as root (sudo ./setup-pi.sh)" >&2 + exit 1 +fi + +info() { echo "[INFO] $*"; } +ok() { echo "[OK] $*"; } +skip() { echo "[SKIP] $*"; } +warn() { echo "[WARN] $*" >&2; } + +echo "==============================================" +echo " RemoteRig Pi Zero 2 W Setup" +echo " Target: ${STATIC_IP} via ${GATEWAY}" +echo " Service user: ${SERVICE_USER}" +echo "==============================================" + +# --------------------------------------------------------------------------- +# 1. Update package list (always safe) +# --------------------------------------------------------------------------- +info "Updating package list..." +apt-get update -qq + +# --------------------------------------------------------------------------- +# 2. Install Mosquitto MQTT broker + clients +# --------------------------------------------------------------------------- +info "Installing Mosquitto..." +if dpkg -l mosquitto mosquitto-clients >/dev/null 2>&1; then + # Already installed — ensure latest + apt-get install -y -qq ${MOSQUITTO_PKG} 2>/dev/null && \ + ok "Mosquitto packages up to date" || \ + warn "Mosquitto package update had warnings (non-fatal)" +else + apt-get install -y -qq ${MOSQUITTO_PKG} + ok "Mosquitto installed" +fi + +# --------------------------------------------------------------------------- +# 3. Configure Mosquitto — anonymous on localhost, listener on 0.0.0.0:1883 +# --------------------------------------------------------------------------- +info "Configuring Mosquitto..." +mkdir -p /etc/mosquitto/conf.d + +# Write idempotent config +cat > "${MOSQUITTO_CONF}" <<'MQTTEOF' +# RemoteRig Mosquitto configuration +# Closed travel-router LAN — anonymous access is intentional + +# Listen on all interfaces (LAN + localhost) +listener 1883 0.0.0.0 + +# No authentication (closed network, no internet access) +allow_anonymous true +MQTTEOF + +ok "Mosquitto config written: ${MOSQUITTO_CONF}" + +# --------------------------------------------------------------------------- +# 4. Create /opt/remoterig directory +# --------------------------------------------------------------------------- +info "Creating deploy directory..." +if [ -d "${DEPLOY_DIR}" ]; then + skip "${DEPLOY_DIR} already exists" +else + mkdir -p "${DEPLOY_DIR}" + ok "Created ${DEPLOY_DIR}" +fi + +# --------------------------------------------------------------------------- +# 5. Copy config.yaml template (if provided) +# --------------------------------------------------------------------------- +if [ -n "${CONFIG_TEMPLATE}" ] && [ -f "${CONFIG_TEMPLATE}" ]; then + info "Copying config.yaml template..." + if [ -f "${DEPLOY_DIR}/config.yaml" ]; then + skip "${DEPLOY_DIR}/config.yaml already exists (not overwriting)" + else + cp "${CONFIG_TEMPLATE}" "${DEPLOY_DIR}/config.yaml" + ok "Copied config.yaml to ${DEPLOY_DIR}/config.yaml" + fi +elif [ -n "${CONFIG_TEMPLATE}" ]; then + warn "Config template '${CONFIG_TEMPLATE}' not found — skipping" +else + info "No config template provided — skipping" +fi + +# Ensure service user owns the deploy directory +chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DEPLOY_DIR}" 2>/dev/null || \ + warn "Could not chown ${DEPLOY_DIR} to ${SERVICE_USER} (user may not exist yet)" + +# --------------------------------------------------------------------------- +# 6. Install and enable systemd service +# --------------------------------------------------------------------------- +info "Installing systemd service..." + +# Locate the service file relative to this script's directory +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SRC_SERVICE="${SCRIPT_DIR}/remoterig.service" + +if [ ! -f "${SRC_SERVICE}" ]; then + warn "Service file not found at ${SRC_SERVICE} — skipping service install" + warn "Run this script from the repository root (scripts/setup-pi.sh)" +else + DST_SERVICE="/etc/systemd/system/${SERVICE_NAME}.service" + + # Copy if different + if [ -f "${DST_SERVICE}" ]; then + if cmp -s "${SRC_SERVICE}" "${DST_SERVICE}"; then + skip "systemd service already installed and up to date" + else + cp "${SRC_SERVICE}" "${DST_SERVICE}" + ok "systemd service updated" + RELOAD_SYSTEMD=1 + fi + else + cp "${SRC_SERVICE}" "${DST_SERVICE}" + ok "systemd service installed" + RELOAD_SYSTEMD=1 + fi + + # Substitute the service user + sed -i "s/^User=.*/User=${SERVICE_USER}/" "${DST_SERVICE}" + + if [ "${RELOAD_SYSTEMD:-0}" -eq 1 ]; then + systemctl daemon-reload + fi + + # Enable (idempotent) + if systemctl is-enabled --quiet "${SERVICE_NAME}" 2>/dev/null; then + skip "systemd service already enabled" + else + systemctl enable "${SERVICE_NAME}" + ok "systemd service enabled" + fi +fi + +# --------------------------------------------------------------------------- +# 7. Set static IP on wlan0 +# --------------------------------------------------------------------------- +info "Configuring static IP on wlan0..." + +# Check if wlan0 exists +if ! ip link show wlan0 >/dev/null 2>&1; then + warn "wlan0 interface not found — skipping static IP configuration" + warn "Connect Wi-Fi first (raspi-config), then re-run this script" +else + STATIC_IP_SET=0 + + # --- Method A: NetworkManager (default on bookworm) --- + if command -v nmcli >/dev/null 2>&1; then + info "Using NetworkManager (nmcli)..." + + # Find the Wi-Fi connection profile + WIFI_CON=$(nmcli -t -f NAME,TYPE con show 2>/dev/null | grep ':802-11-wireless' | cut -d: -f1 | head -1) + + if [ -n "${WIFI_CON}" ]; then + CURRENT_IP=$(nmcli -t -f IP4.ADDRESS con show "${WIFI_CON}" 2>/dev/null | cut -d: -f2 | head -1 || true) + + if [ "${CURRENT_IP}" = "${STATIC_IP}" ]; then + skip "wlan0 already set to ${STATIC_IP} via nmcli" + STATIC_IP_SET=1 + else + nmcli con mod "${WIFI_CON}" ipv4.addresses "${STATIC_IP}" + nmcli con mod "${WIFI_CON}" ipv4.gateway "${GATEWAY}" + nmcli con mod "${WIFI_CON}" ipv4.dns "${GATEWAY}" + nmcli con mod "${WIFI_CON}" ipv4.method manual + nmcli con up "${WIFI_CON}" 2>/dev/null || true + ok "wlan0 set to ${STATIC_IP} via nmcli (connection: ${WIFI_CON})" + STATIC_IP_SET=1 + fi + else + warn "No Wi-Fi connection profile found in NetworkManager" + fi + fi + + # --- Method B: dhcpcd (fallback for older PiOS) --- + if [ ${STATIC_IP_SET} -eq 0 ] && command -v dhcpcd >/dev/null 2>&1; then + info "Using dhcpcd..." + + DHCPCD_CONF="/etc/dhcpcd.conf" + + if grep -q "interface wlan0" "${DHCPCD_CONF}" 2>/dev/null; then + skip "dhcpcd already has wlan0 config" + else + cat >> "${DHCPCD_CONF}" </dev/null; then + cat >> "${INTERFACES_FILE}" </dev/null; then + skip "Mosquitto already running" +else + systemctl enable mosquitto 2>/dev/null || true + systemctl restart mosquitto + ok "Mosquitto started" +fi + +# Verify Mosquitto is listening +sleep 1 +if systemctl is-active --quiet mosquitto 2>/dev/null; then + ok "Mosquitto is running and listening on :1883" +else + warn "Mosquitto may not have started — check: sudo systemctl status mosquitto" +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo "==============================================" +echo " Setup complete!" +echo "==============================================" +echo " Mosquitto: $(systemctl is-active mosquitto 2>/dev/null || echo 'unknown')" +echo " Service: ${SERVICE_NAME} (systemctl status ${SERVICE_NAME})" +echo " Deploy dir: ${DEPLOY_DIR}" +echo " Static IP: ${STATIC_IP} on wlan0" +echo "" +echo " Next steps:" +echo " 1. Build the remoterig binary for ARM64:" +echo " GOOS=linux GOARCH=arm64 go build -o remoterig ./cmd/server" +echo " 2. Copy binary to Pi:" +echo " scp remoterig pi@192.168.4.10:/opt/remoterig/" +echo " 3. Copy config if needed:" +echo " scp config.yaml pi@192.168.4.10:/opt/remoterig/" +echo " 4. Start the service:" +echo " sudo systemctl start remoterig" +echo " 5. Check health:" +echo " curl http://192.168.4.10:8080/health" +echo "" +echo " To deploy updates, use: scripts/deploy.sh" +echo "=============================================="