#!/bin/bash # Smartbotic Service Installer # Installs Smartbotic service packages with config preservation and proper security # # Usage: # sudo ./install.sh smartbotic-*.tar.gz # sudo ./install.sh --dry-run smartbotic-database-*.tar.gz smartbotic-microbit-*.tar.gz set -e # Default paths PREFIX="/opt/smartbotic" BINDIR="$PREFIX/bin" CONFDIR="/etc/smartbotic" DATADIR="/var/lib/smartbotic" LOGDIR="/var/log/smartbotic" SYSTEMD_DIR="/etc/systemd/system" # Smartbotic system user SMARTBOTIC_USER="smartbotic" SMARTBOTIC_GROUP="smartbotic" # Log file (set after PREFIX is finalized) INSTALL_LOG="" # Output control VERBOSE=false QUIET=false # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # Initialize logging init_logging() { mkdir -p "$PREFIX" INSTALL_LOG="$PREFIX/install-$(date +%Y%m%d-%H%M%S).log" echo "Smartbotic Install Log - $(date)" > "$INSTALL_LOG" echo "Command: $0 $ORIGINAL_ARGS" >> "$INSTALL_LOG" echo "========================================" >> "$INSTALL_LOG" } # Log to file only log() { echo "[$(date +%H:%M:%S)] $1" >> "$INSTALL_LOG" } # Print to console (respects quiet mode) print_info() { log "INFO: $1" if [ "$QUIET" = false ]; then if [ "$VERBOSE" = true ]; then echo -e "${GREEN}[INFO]${NC} $1" fi fi } print_warn() { log "WARN: $1" if [ "$QUIET" = false ]; then echo -e "${YELLOW}[WARN]${NC} $1" fi } print_error() { log "ERROR: $1" echo -e "${RED}[ERROR]${NC} $1" >&2 } # Always show steps (progress indicator) print_step() { log "STEP: $1" if [ "$QUIET" = false ]; then echo -e "${BLUE}▶${NC} $1" fi } # Summary line (always shown unless quiet) print_summary() { log "SUMMARY: $1" if [ "$QUIET" = false ]; then echo -e " ${GREEN}✓${NC} $1" fi } usage() { cat <&1); then if [ -n "$output" ]; then echo "$output" >> "$INSTALL_LOG" fi else local exit_code=$? if [ -n "$output" ]; then echo "$output" >> "$INSTALL_LOG" fi return $exit_code fi fi } # Collect dependencies from package collect_deps() { local deps="$1" if [ -n "$deps" ]; then for dep in $deps; do if [[ ! " $ALL_DEPS " =~ " $dep " ]]; then ALL_DEPS="$ALL_DEPS $dep" fi done fi } # Install all collected dependencies install_deps() { if [ "$NO_DEPS" = true ]; then print_info "Skipping dependency installation (--no-deps)" return fi ALL_DEPS=$(echo "$ALL_DEPS" | xargs) if [ -z "$ALL_DEPS" ]; then print_info "No dependencies to install" return fi print_step "Installing dependencies..." log "Dependencies: $ALL_DEPS" if [ "$DRY_RUN" = true ]; then log "[DRY-RUN] apt-get update && apt-get install -y $ALL_DEPS" else apt-get update -qq >> "$INSTALL_LOG" 2>&1 apt-get install -y --no-install-recommends $ALL_DEPS >> "$INSTALL_LOG" 2>&1 fi print_summary "Dependencies installed" } # Create system user and group create_user() { if [ "$NO_USER" = true ]; then print_info "Skipping user creation (--no-user)" return fi print_step "Setting up system user..." if ! getent group "$SMARTBOTIC_GROUP" > /dev/null 2>&1; then log "Creating group: $SMARTBOTIC_GROUP" run_cmd groupadd --system "$SMARTBOTIC_GROUP" fi if ! getent passwd "$SMARTBOTIC_USER" > /dev/null 2>&1; then log "Creating user: $SMARTBOTIC_USER" run_cmd useradd --system \ --gid "$SMARTBOTIC_GROUP" \ --home-dir "$DATADIR" \ --shell /usr/sbin/nologin \ --comment "Smartbotic Service User" \ "$SMARTBOTIC_USER" fi print_summary "User $SMARTBOTIC_USER ready" } # Create directory structure create_directories() { print_step "Creating directories..." local dirs=( "$PREFIX" "$BINDIR" "$PREFIX/share/smartbotic/nginx" "$CONFDIR" "$DATADIR" "$DATADIR/database" "$DATADIR/database/storage" "$DATADIR/database/migrations" "$DATADIR/microbit" "$DATADIR/microbit/webui" "$LOGDIR" ) for dir in "${dirs[@]}"; do if [ ! -d "$dir" ]; then log "Creating: $dir" run_cmd mkdir -p "$dir" fi done if [ "$DRY_RUN" = false ]; then chown -R "$SMARTBOTIC_USER:$SMARTBOTIC_GROUP" "$DATADIR" "$LOGDIR" chown root:"$SMARTBOTIC_GROUP" "$CONFDIR" chmod 750 "$CONFDIR" fi print_summary "Directories created" } # Install a single package install_package() { local pkg="$1" local pkg_basename=$(basename "$pkg") local pkg_name="${pkg_basename%.tar.gz}" # Extract short name (e.g., "database" from "smartbotic-database-0.1.0-debian13") local short_name=$(echo "$pkg_name" | sed -E 's/smartbotic-([^-]+).*/\1/') print_step "Installing $short_name..." local tmpdir tmpdir=$(mktemp -d) trap "rm -rf '$tmpdir'" RETURN tar -xzf "$pkg" -C "$tmpdir" local pkg_root="$tmpdir/$pkg_name" if [ ! -d "$pkg_root" ]; then pkg_root=$(find "$tmpdir" -maxdepth 1 -type d ! -path "$tmpdir" | head -1) fi if [ ! -d "$pkg_root" ]; then print_error "Could not find package root in archive" return 1 fi # Collect dependencies if [ -f "$pkg_root/DEPS" ]; then collect_deps "$(cat "$pkg_root/DEPS")" fi local installed_items=() # Install binaries if [ -d "$pkg_root/bin" ]; then for binary in "$pkg_root/bin"/*; do if [ -f "$binary" ]; then local bin_name=$(basename "$binary") local dest="$BINDIR/$bin_name" if [ -f "$dest" ] && [ "$FORCE" = false ] && [ "$PRESERVE_ALL" = true ]; then log "Skipping existing: $dest" else log "Installing binary: $dest" run_cmd install -m 755 "$binary" "$dest" installed_items+=("binary") fi fi done fi # Install configs (never overwrite existing) if [ -d "$pkg_root/etc/smartbotic" ]; then for cfg in "$pkg_root/etc/smartbotic"/*; do if [ -f "$cfg" ]; then local cfg_name=$(basename "$cfg") local dest_name="${cfg_name%.default}" local dest="$CONFDIR/$dest_name" if [ -f "$dest" ]; then log "Config exists, preserving: $dest" if [ "$DRY_RUN" = false ]; then cp "$cfg" "$dest.new" chown root:"$SMARTBOTIC_GROUP" "$dest.new" chmod 640 "$dest.new" fi else log "Installing config: $dest" run_cmd install -m 640 -o root -g "$SMARTBOTIC_GROUP" "$cfg" "$dest" installed_items+=("config") fi fi done fi # Install systemd services if [ -d "$pkg_root/etc/systemd/system" ] && [ "$NO_SYSTEMD" = false ]; then for service in "$pkg_root/etc/systemd/system"/*.service; do if [ -f "$service" ]; then local svc_name=$(basename "$service") log "Installing systemd service: $svc_name" run_cmd install -m 644 "$service" "$SYSTEMD_DIR/$svc_name" installed_items+=("service") fi done fi # Install data files if [ -d "$pkg_root/share/smartbotic" ]; then log "Installing data files..." if [ "$DRY_RUN" = false ]; then # WebUI files if [ -d "$pkg_root/share/smartbotic/webui" ]; then rm -rf "$DATADIR/microbit/webui"/* cp -a "$pkg_root/share/smartbotic/webui"/* "$DATADIR/microbit/webui/" 2>/dev/null || true installed_items+=("webui") fi # Database migrations if [ -d "$pkg_root/share/smartbotic/database/migrations" ]; then cp -a "$pkg_root/share/smartbotic/database/migrations"/* "$DATADIR/database/migrations/" 2>/dev/null || true installed_items+=("migrations") fi # Nginx template if [ -d "$pkg_root/share/smartbotic/nginx" ]; then cp -a "$pkg_root/share/smartbotic/nginx"/* "$PREFIX/share/smartbotic/nginx/" 2>/dev/null || true installed_items+=("nginx") fi chown -R "$SMARTBOTIC_USER:$SMARTBOTIC_GROUP" "$DATADIR" fi fi print_summary "$short_name installed" } # Main installation main() { # Initialize logging first init_logging if [ "$QUIET" = false ]; then echo "" echo -e "${GREEN}Smartbotic Installer${NC}" echo "====================" fi log "PREFIX=$PREFIX" log "CONFDIR=$CONFDIR" log "DATADIR=$DATADIR" log "USER=$SMARTBOTIC_USER" log "PACKAGES=${#PACKAGES[@]}" if [ "$DRY_RUN" = true ]; then print_warn "DRY-RUN MODE - no changes will be made" fi # Create user and directories create_user create_directories # Sort packages: database first, then microbit, then webui, then others local sorted_packages=() local database_pkg="" local microbit_pkg="" local webui_pkg="" local other_packages=() for pkg in "${PACKAGES[@]}"; do case "$pkg" in *smartbotic-database*) database_pkg="$pkg" ;; *smartbotic-microbit*) microbit_pkg="$pkg" ;; *smartbotic-webui*) webui_pkg="$pkg" ;; *) other_packages+=("$pkg") ;; esac done [ -n "$database_pkg" ] && sorted_packages+=("$database_pkg") [ -n "$microbit_pkg" ] && sorted_packages+=("$microbit_pkg") [ -n "$webui_pkg" ] && sorted_packages+=("$webui_pkg") sorted_packages+=("${other_packages[@]}") # First pass: collect dependencies for pkg in "${sorted_packages[@]}"; do if [ -f "$pkg" ]; then local tmpdir=$(mktemp -d) tar -xzf "$pkg" -C "$tmpdir" 2>/dev/null local pkg_root=$(find "$tmpdir" -maxdepth 1 -type d ! -path "$tmpdir" | head -1) if [ -f "$pkg_root/DEPS" ]; then collect_deps "$(cat "$pkg_root/DEPS")" fi rm -rf "$tmpdir" fi done # Install dependencies install_deps # Install packages for pkg in "${sorted_packages[@]}"; do if [ ! -f "$pkg" ]; then print_error "Package not found: $pkg" continue fi install_package "$pkg" done # Post-installation print_step "Finalizing..." if [ "$NO_SYSTEMD" = false ] && [ "$DRY_RUN" = false ]; then log "Reloading systemd daemon..." systemctl daemon-reload >> "$INSTALL_LOG" 2>&1 fi print_summary "Installation complete" if [ "$QUIET" = false ]; then echo "" echo -e "${GREEN}Done!${NC} Log: $INSTALL_LOG" echo "" echo "Next steps:" echo " 1. Configure: $CONFDIR/smartbotic.env" echo " (copy from $CONFDIR/smartbotic.env.default if not exists)" echo " Set JWT_SECRET, SMTP, and CallerAI API settings" echo " 2. Review configs: $CONFDIR/database.json, $CONFDIR/microbit.json" echo " 3. Enable services:" echo " systemctl enable smartbotic-database smartbotic-microbit" echo " 4. Start services:" echo " systemctl start smartbotic-database" echo " systemctl start smartbotic-microbit" echo " 5. Setup nginx (optional):" echo " $BINDIR/smartbotic-setup-nginx --domain example.com --email admin@example.com" echo "" fi log "Installation completed successfully" } main