#!/usr/bin/env bash # # ═══════════════════════════════════════════════════════════════════════════ # FILE: installers/linux/install.sh # VERSION: 1.0.0 # LAST UPDATED: 2026-01-26 # AUTHOR: NOVA AI Integration Team # ═══════════════════════════════════════════════════════════════════════════ # # PURPOSE: # Idempotent installer for NOVA AI agent on Linux. # Detects existing nova-ai-agent service and provides management actions. # # RESPONSIBILITIES: # - Detect existing nova-ai-agent installation (systemd service, binary) # - Route to appropriate action based on NOVA_ACTION env var # - Perform fresh installation if agent not found # - Create systemd service unit (nova-ai-agent.service) # - Configure agent via /etc/nova/agent.yaml # - Optionally register node with NOVA backend # # USAGE: # # Fresh install (if not exists): # curl -fsSL https://install.novaai.com/linux.sh | bash # # # Show status of existing installation: # NOVA_ACTION=status ./install.sh # # # Restart service: # NOVA_ACTION=restart ./install.sh # # # Upgrade to latest version: # NOVA_ACTION=upgrade ./install.sh # # # Repair systemd unit: # NOVA_ACTION=repair ./install.sh # # # Update configuration: # NOVA_ACTION=configure NOVA_ORG_ID=my-org ./install.sh # # ENVIRONMENT VARIABLES: # NOVA_ACTION - Action to perform: status|restart|configure|upgrade|repair # NOVA_AGENT_BASE_URL - Download base URL (default: https://downloads.novasrai.com/agent) # NOVA_AGENT_VERSION - Version to install (default: latest) # NOVA_ENDPOINT - Backend API endpoint for registration # NOVA_AUTH_TOKEN - Authentication token # NOVA_ORG_ID - Organization ID # NOVA_NODE_NAME - Node name (default: hostname) # NOVA_TAGS - Comma-separated tags # NOVA_NON_INTERACTIVE - Set to 1 for CI installs # NOVA_DEBUG - Set to 1 for verbose output # # ═══════════════════════════════════════════════════════════════════════════ set -euo pipefail # ═══════════════════════════════════════════════════════════════════════════ # CONFIGURATION & GLOBALS # ═══════════════════════════════════════════════════════════════════════════ # Source UI library SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" UI_LIB="${SCRIPT_DIR}/../common/nova_cli_ui.sh" if [[ ! -f "$UI_LIB" ]]; then echo "ERROR: UI library not found at $UI_LIB" echo "Please ensure the installer package is complete." exit 1 fi # shellcheck source=../common/nova_cli_ui.sh source "$UI_LIB" # Agent configuration NOVA_AGENT_BASE_URL="${NOVA_AGENT_BASE_URL:-https://novaaiops.com/downloads/agent}" NOVA_AGENT_VERSION="${NOVA_AGENT_VERSION:-latest}" NOVA_ACTION="${NOVA_ACTION:-auto}" # auto, status, restart, configure, upgrade, repair # Paths SERVICE_NAME="nova-ai-agent" BINARY_PATH="/usr/local/bin/nova-ai-agent" CONFIG_DIR="/etc/nova" CONFIG_FILE="${CONFIG_DIR}/agent.yaml" SYSTEMD_UNIT_FILE="/etc/systemd/system/${SERVICE_NAME}.service" DATA_DIR="/var/lib/nova" LOG_DIR="/var/log/nova" # Node configuration NOVA_ORG_ID="${NOVA_ORG_ID:-}" NOVA_NODE_NAME="${NOVA_NODE_NAME:-$(hostname)}" NOVA_TAGS="${NOVA_TAGS:-}" NOVA_ENDPOINT="${NOVA_ENDPOINT:-}" NOVA_AUTH_TOKEN="${NOVA_AUTH_TOKEN:-}" # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: detect_os_arch # ═══════════════════════════════════════════════════════════════════════════ detect_os_arch() { local os="linux" local arch case "$(uname -m)" in x86_64) arch="amd64" ;; aarch64) arch="arm64" ;; armv7l) arch="armv7" ;; *) error "Unsupported architecture: $(uname -m)" exit 1 ;; esac echo "${os}_${arch}" } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: check_prerequisites # ═══════════════════════════════════════════════════════════════════════════ check_prerequisites() { step "Checking prerequisites" # Check for root/sudo if [[ $EUID -ne 0 ]]; then error "This installer must be run as root or with sudo" exit 1 fi # Check for systemd if ! command -v systemctl &> /dev/null; then error "systemd is required but not found" exit 1 fi # Check for required tools local missing=() for cmd in curl tar; do if ! command -v "$cmd" &> /dev/null; then missing+=("$cmd") fi done if [[ ${#missing[@]} -gt 0 ]]; then error "Missing required tools: ${missing[*]}" info "Please install: ${missing[*]}" exit 1 fi success "Prerequisites satisfied" } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: detect_existing_installation # ═══════════════════════════════════════════════════════════════════════════ # # PURPOSE: # Detect if nova-ai-agent is already installed on the system. # Checks for: systemd service, binary in PATH, systemd unit file. # # RETURNS: # 0 if installation exists, 1 if not found # # SIDE EFFECTS: # Sets global variables: EXISTING_INSTALL, SERVICE_RUNNING, BINARY_EXISTS # # ─────────────────────────────────────────────────────────────────────────── detect_existing_installation() { debug_log "Detecting existing nova-ai-agent installation" EXISTING_INSTALL=0 SERVICE_RUNNING=0 BINARY_EXISTS=0 # Check if systemd service exists and is running if systemctl status "$SERVICE_NAME" &> /dev/null; then EXISTING_INSTALL=1 if systemctl is-active --quiet "$SERVICE_NAME"; then SERVICE_RUNNING=1 fi debug_log "Found systemd service: $SERVICE_NAME (running: $SERVICE_RUNNING)" fi # Check if binary exists if [[ -f "$BINARY_PATH" ]]; then EXISTING_INSTALL=1 BINARY_EXISTS=1 debug_log "Found binary at: $BINARY_PATH" fi # Check if systemd unit file exists if [[ -f "$SYSTEMD_UNIT_FILE" ]]; then EXISTING_INSTALL=1 debug_log "Found systemd unit file: $SYSTEMD_UNIT_FILE" fi return $EXISTING_INSTALL } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: show_status # ═══════════════════════════════════════════════════════════════════════════ show_status() { step "NOVA AI Agent Status" echo "" # Service status if systemctl is-active --quiet "$SERVICE_NAME"; then success "Service: $SERVICE_NAME (running)" else warn "Service: $SERVICE_NAME (stopped)" fi # Binary version if [[ -f "$BINARY_PATH" ]]; then local version if version=$("$BINARY_PATH" --version 2>/dev/null | head -n1); then info "Version: $version" else info "Binary: $BINARY_PATH" fi else warn "Binary: Not found at $BINARY_PATH" fi # Configuration if [[ -f "$CONFIG_FILE" ]]; then success "Config: $CONFIG_FILE" if [[ -r "$CONFIG_FILE" ]]; then substep "Organization ID: $(grep -E '^org_id:' "$CONFIG_FILE" | awk '{print $2}')" substep "Node Name: $(grep -E '^node_name:' "$CONFIG_FILE" | awk '{print $2}')" fi else warn "Config: Not found at $CONFIG_FILE" fi # Systemd unit if [[ -f "$SYSTEMD_UNIT_FILE" ]]; then success "Systemd Unit: $SYSTEMD_UNIT_FILE" else warn "Systemd Unit: Not found" fi # Log file echo "" info "View logs: journalctl -u $SERVICE_NAME -f" info "Service control: systemctl {start|stop|restart|status} $SERVICE_NAME" # Available actions echo "" divider info "Available actions (set NOVA_ACTION env var):" substep "NOVA_ACTION=restart - Restart the service" substep "NOVA_ACTION=configure - Update configuration" substep "NOVA_ACTION=upgrade - Upgrade to latest version" substep "NOVA_ACTION=repair - Repair systemd unit" divider } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: action_restart # ═══════════════════════════════════════════════════════════════════════════ action_restart() { step "Restarting nova-ai-agent service" if spinner "Restarting service" systemctl restart "$SERVICE_NAME"; then success "Service restarted successfully" # Wait for service to stabilize sleep 2 if systemctl is-active --quiet "$SERVICE_NAME"; then success "Service is running" else error "Service failed to start" info "Check logs: journalctl -u $SERVICE_NAME -n 50" exit 1 fi else error "Failed to restart service" exit 1 fi } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: action_configure # ═══════════════════════════════════════════════════════════════════════════ action_configure() { step "Updating configuration" # Load existing config if it exists local existing_org_id="" local existing_node_name="" local existing_endpoint="" if [[ -f "$CONFIG_FILE" ]]; then existing_org_id=$(grep -E '^org_id:' "$CONFIG_FILE" 2>/dev/null | awk '{print $2}' || echo "") existing_node_name=$(grep -E '^node_name:' "$CONFIG_FILE" 2>/dev/null | awk '{print $2}' || echo "") existing_endpoint=$(grep -E '^endpoint_url:' "$CONFIG_FILE" 2>/dev/null | awk '{print $2}' || echo "") fi # Prompt for values (or use env vars) local org_id="${NOVA_ORG_ID:-${existing_org_id}}" local node_name="${NOVA_NODE_NAME:-${existing_node_name:-$(hostname)}}" local endpoint="${NOVA_ENDPOINT:-${existing_endpoint}}" if [[ "${NOVA_NON_INTERACTIVE:-0}" != "1" ]]; then safe_prompt "Organization ID" "$org_id" org_id safe_prompt "Node Name" "$node_name" node_name safe_prompt "Endpoint URL (optional)" "$endpoint" endpoint fi # Update config file write_config_file "$org_id" "$node_name" "$endpoint" success "Configuration updated" # Offer to restart if confirm "Restart service to apply changes?" "y"; then action_restart else info "Remember to restart the service: systemctl restart $SERVICE_NAME" fi } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: action_upgrade # ═══════════════════════════════════════════════════════════════════════════ action_upgrade() { step "Upgrading nova-ai-agent" # Backup existing binary if [[ -f "$BINARY_PATH" ]]; then local backup="${BINARY_PATH}.backup.$(date +%s)" substep "Backing up current binary to: $backup" cp "$BINARY_PATH" "$backup" fi # Download new binary download_agent # Restart service action_restart success "Upgrade complete" } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: action_repair # ═══════════════════════════════════════════════════════════════════════════ action_repair() { step "Repairing nova-ai-agent installation" # Recreate systemd unit substep "Recreating systemd unit file" create_systemd_unit # Reload systemd daemon substep "Reloading systemd daemon" systemctl daemon-reload # Enable service substep "Enabling service" systemctl enable "$SERVICE_NAME" # Restart service action_restart success "Repair complete" } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: download_agent # ═══════════════════════════════════════════════════════════════════════════ download_agent() { step "Downloading NOVA agent binary" local os_arch os_arch=$(detect_os_arch) local download_url="${NOVA_AGENT_BASE_URL}/${NOVA_AGENT_VERSION}/nova-ai-agent-${os_arch}" local checksum_url="${NOVA_AGENT_BASE_URL}/${NOVA_AGENT_VERSION}/checksums.txt" substep "Platform: $os_arch" substep "Version: $NOVA_AGENT_VERSION" substep "URL: $download_url" local temp_dir temp_dir=$(mktemp -d) trap "rm -rf '$temp_dir'" EXIT local temp_binary="${temp_dir}/nova-ai-agent" # Download binary if ! spinner "Downloading binary" curl -fsSL "$download_url" -o "$temp_binary"; then error "Failed to download agent binary" info "URL: $download_url" exit 1 fi # Verify checksum if available if curl -fsSL "$checksum_url" -o "${temp_dir}/checksums.txt" 2>/dev/null; then substep "Verifying checksum" local expected_checksum expected_checksum=$(grep "nova-ai-agent-${os_arch}" "${temp_dir}/checksums.txt" | awk '{print $1}') if [[ -n "$expected_checksum" ]]; then local actual_checksum actual_checksum=$(sha256sum "$temp_binary" | awk '{print $1}') if [[ "$expected_checksum" != "$actual_checksum" ]]; then error "Checksum verification failed" info "Expected: $expected_checksum" info "Actual: $actual_checksum" exit 1 fi success "Checksum verified" fi fi # Install binary substep "Installing to $BINARY_PATH" install -m 755 "$temp_binary" "$BINARY_PATH" success "Binary installed" } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: write_config_file # ═══════════════════════════════════════════════════════════════════════════ write_config_file() { local org_id="$1" local node_name="$2" local endpoint="$3" step "Creating configuration file" # Create config directory mkdir -p "$CONFIG_DIR" # Write config file (YAML format) cat > "$CONFIG_FILE" < "$SYSTEMD_UNIT_FILE" </dev/null | head -n1 || echo "unknown") fi local payload payload=$(cat <&1); then success "Node registered successfully" debug_log "Registration response: $response" else warn "Node registration failed (continuing anyway)" debug_log "Registration error: $response" fi } # ═══════════════════════════════════════════════════════════════════════════ # FUNCTION: fresh_install # ═══════════════════════════════════════════════════════════════════════════ fresh_install() { step "Performing fresh installation" echo "" # Create directories step "Creating directories" mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR" success "Directories created" # Download agent binary download_agent # Write configuration local org_id="${NOVA_ORG_ID}" local node_name="${NOVA_NODE_NAME:-$(hostname)}" local endpoint="${NOVA_ENDPOINT}" if [[ "${NOVA_NON_INTERACTIVE:-0}" != "1" ]]; then echo "" divider info "Agent Configuration" divider safe_prompt "Organization ID" "${org_id:-default-org}" org_id safe_prompt "Node Name" "$node_name" node_name safe_prompt "Backend Endpoint (optional)" "$endpoint" endpoint fi write_config_file "$org_id" "$node_name" "$endpoint" # Create systemd unit create_systemd_unit # Reload systemd and enable service step "Enabling service" systemctl daemon-reload systemctl enable "$SERVICE_NAME" success "Service enabled" # Start service step "Starting service" if systemctl start "$SERVICE_NAME"; then success "Service started" else error "Failed to start service" info "Check logs: journalctl -u $SERVICE_NAME -n 50" exit 1 fi # Register node (optional) register_node # Print summary print_summary \ "Service: $SERVICE_NAME" \ "Status: Running" \ "Binary: $BINARY_PATH" \ "Config: $CONFIG_FILE" \ "Logs: journalctl -u $SERVICE_NAME -f" \ "" \ "Next steps:" \ " 1. Verify agent is reporting: systemctl status $SERVICE_NAME" \ " 2. View logs: journalctl -u $SERVICE_NAME -f" \ " 3. Access dashboard: https://novaaiops.com" } # ═══════════════════════════════════════════════════════════════════════════ # MAIN EXECUTION # ═══════════════════════════════════════════════════════════════════════════ main() { print_logo divider "═" echo -e "${NOVA_PURPLE}${BOLD} NOVA AI AGENT INSTALLER FOR LINUX${RESET}" divider "═" echo "" check_prerequisites # Detect existing installation if detect_existing_installation; then # Installation exists if [[ "$SERVICE_RUNNING" == "1" ]]; then info "Detected existing nova-ai-agent installation (running)" else info "Detected existing nova-ai-agent installation (stopped)" fi echo "" # Determine action if [[ "$NOVA_ACTION" == "auto" ]]; then # Default to showing status NOVA_ACTION="status" # If service is stopped, offer to start it if [[ "$SERVICE_RUNNING" == "0" ]]; then if confirm "Service is stopped. Start it now?" "y"; then NOVA_ACTION="restart" fi fi fi # Execute action case "$NOVA_ACTION" in status) show_status ;; restart) action_restart show_status ;; configure) action_configure ;; upgrade) action_upgrade show_status ;; repair) action_repair show_status ;; *) error "Unknown action: $NOVA_ACTION" info "Valid actions: status, restart, configure, upgrade, repair" exit 1 ;; esac else # No existing installation - perform fresh install info "No existing nova-ai-agent installation detected" echo "" if [[ "${NOVA_NON_INTERACTIVE:-0}" != "1" ]]; then if ! confirm "Proceed with fresh installation?" "y"; then info "Installation cancelled" exit 0 fi fi fresh_install fi echo "" } # Run main function main "$@"