Files
patchmon.net/manage-patchmon.sh
Muhammad Ibrahim 4576781900 Improve agent version logic in manage-patchmon.sh
- Added priority-based version detection (agent script > codebase > package.json > database)
- Always update agent script content during updates, even if version exists
- Improved logging and fallback to 1.2.5
- Consistent behavior with manage-patchmon-dev.sh
2025-09-20 13:07:02 +01:00

3052 lines
110 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
# PatchMon Unified Management Script
# Usage: ./manage-patchmon.sh <command> [options]
# Commands: deploy, update, delete, list, status
# Options: public-repo (for deploy command)
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
SCRIPT_VERSION="manage-patchmon.sh v1.2.4-ops-2025-09-18-4"
DEFAULT_GITHUB_REPO="git@github.com:9technologygroup/patchmon.net.git"
FQDN=""
GITHUB_REPO=""
DB_SAFE_NAME=""
DB_NAME=""
DB_USER=""
DB_PASS=""
JWT_SECRET=""
BACKEND_PORT=""
APP_DIR=""
SERVICE_NAME=""
USE_LETSENCRYPT="false"
SERVER_PROTOCOL_SEL="http"
SERVER_PORT_SEL=80
PUBLIC_REPO_MODE="false"
SETUP_NGINX="true"
# Functions
print_status() {
echo -e "${GREEN}$1${NC}"
}
# Unified version detection function
get_instance_version() {
local app_dir=$1
local fqdn=$2
local status=$3
local port=$4
local version="N/A"
# Priority 1: Get version from git tags (most accurate for deployed versions)
if [ -d "$app_dir/.git" ]; then
cd "$app_dir" 2>/dev/null && {
# Try different git tag methods
version=$(git describe --tags --exact-match HEAD 2>/dev/null | sed 's/^v//' || \
git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || \
git tag --sort=-version:refname | head -1 | sed 's/^v//' 2>/dev/null || \
echo "")
} && cd - >/dev/null
fi
# Priority 2: Get version from database agent version (if service is running)
if [ "$version" = "N/A" ] || [ -z "$version" ]; then
if [ "$status" = "active" ] && [ -n "$port" ] && [ "$port" != "N/A" ]; then
# Try to get version from the API
version=$(curl -s --connect-timeout 3 "http://localhost:$port/api/v1/hosts/agent/version" 2>/dev/null | \
grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4 2>/dev/null || echo "")
fi
fi
# Priority 3: Get version from package.json (fallback)
if [ "$version" = "N/A" ] || [ -z "$version" ]; then
if [ -f "$app_dir/package.json" ]; then
version=$(grep '"version"' "$app_dir/package.json" | head -1 | sed 's/.*"version":[[:space:]]*"\([^"]*\)".*/\1/')
elif [ -f "$app_dir/backend/package.json" ]; then
version=$(grep '"version"' "$app_dir/backend/package.json" | head -1 | sed 's/.*"version":[[:space:]]*"\([^"]*\)".*/\1/')
fi
fi
# Ensure version is not empty
if [ -z "$version" ] || [ "$version" = '""' ] || [ "$version" = "N/A" ]; then
version="N/A"
fi
echo "$version"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
print_banner() {
echo -e "${BLUE}================ PatchMon Manager =================${NC}"
echo -e "${BLUE}Running: ${SCRIPT_VERSION}${NC}"
echo -e "${BLUE}====================================================${NC}"
}
# Check if system component is already installed
check_system_component() {
local component=$1
case $component in
"postgresql")
systemctl is-active postgresql >/dev/null 2>&1 && return 0 || return 1
;;
"nginx")
systemctl is-active nginx >/dev/null 2>&1 && return 0 || return 1
;;
"nodejs")
command -v node >/dev/null 2>&1 && return 0 || return 1
;;
"certbot")
command -v certbot >/dev/null 2>&1 && return 0 || return 1
;;
*)
return 1
;;
esac
}
# Function to find available port
find_available_port() {
local start_port=3001
local port=$start_port
while true; do
# Check if port is in use using multiple methods for reliability
if ! netstat -tuln 2>/dev/null | grep -q ":$port " && \
! ss -tuln 2>/dev/null | grep -q ":$port " && \
! lsof -i :$port 2>/dev/null | grep -q ":$port"; then
echo $port
return 0
fi
port=$((port + 1))
# Safety check to prevent infinite loop
if [ $port -gt 3100 ]; then
print_error "Could not find available port between 3001-3100"
exit 1
fi
done
}
# Initialize instance variables
init_instance_vars() {
DB_SAFE_NAME=$(echo $FQDN | tr '[:upper:]' '[:lower:]' | tr '.-' '__')
DB_NAME="patchmon_${DB_SAFE_NAME}"
DB_USER="patchmon_${DB_SAFE_NAME}_user"
DB_PASS=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25)
JWT_SECRET=$(openssl rand -base64 64 | tr -d "=+/" | cut -c1-50)
# Show currently used ports for debugging
echo -e "${BLUE}🔍 Checking currently used ports...${NC}"
echo "Ports 3000-3010 in use:"
for p in {3000..3010}; do
if netstat -tuln 2>/dev/null | grep -q ":$p " || \
ss -tuln 2>/dev/null | grep -q ":$p " || \
lsof -i :$p 2>/dev/null | grep -q ":$p"; then
echo " Port $p: IN USE"
fi
done
BACKEND_PORT=$(find_available_port)
FRONTEND_PORT=$((BACKEND_PORT + 1))
APP_DIR="/opt/patchmon-$FQDN"
SERVICE_NAME="patchmon-$FQDN"
}
# Ask whether to enable Let's Encrypt / HTTPS
choose_ssl_option() {
echo -e "${BLUE}🔒 SSL/HTTPS Configuration${NC}"
echo "This installer can configure Let's Encrypt for HTTPS (public FQDN required)."
echo "If you plan to run PatchMon internally (behind NAT) or without public DNS, choose 'N'."
read -p "Enable Let's Encrypt HTTPS? (Y/n): " ENABLE_SSL
ENABLE_SSL=${ENABLE_SSL:-Y}
if [[ "$ENABLE_SSL" =~ ^[Yy]$ ]]; then
USE_LETSENCRYPT="true"
SERVER_PROTOCOL_SEL="https"
SERVER_PORT_SEL=443
else
USE_LETSENCRYPT="false"
SERVER_PROTOCOL_SEL="http"
SERVER_PORT_SEL=80
fi
export USE_LETSENCRYPT SERVER_PROTOCOL_SEL SERVER_PORT_SEL
print_status "SSL option selected: ${USE_LETSENCRYPT} (protocol=${SERVER_PROTOCOL_SEL}, port=${SERVER_PORT_SEL})"
}
# Configure timezone and time sync
configure_timezone() {
echo -e "${BLUE}🕒 Checking current time and timezone...${NC}"
echo "Current time: $(date)"
echo "Current timezone: $(timedatectl show -p Timezone --value 2>/dev/null || echo 'unknown')"
echo
read -p "Would you like to change the timezone? (y/N): " change_tz
change_tz=${change_tz:-N}
if [[ "$change_tz" =~ ^[Yy]$ ]]; then
echo -e "${BLUE}🌍 Available timezones example: Europe/London, UTC, America/New_York${NC}"
read -p "Enter timezone (e.g., Europe/London): " NEW_TZ
if [ ! -z "$NEW_TZ" ]; then
timedatectl set-timezone "$NEW_TZ" || print_warning "Failed to set timezone"
print_status "Timezone set to: $NEW_TZ"
fi
fi
# Enable NTP sync
if ! timedatectl show | grep -q "NTPSynchronized=yes"; then
echo -e "${BLUE}🕐 Enabling NTP time synchronization...${NC}"
timedatectl set-ntp true || print_warning "Failed to enable NTP"
print_status "NTP synchronization enabled"
else
print_info "NTP synchronization already enabled"
fi
}
# Update system (only if not recently updated)
update_system() {
if [ ! -f /var/cache/apt/pkgcache.bin ] || [ $(find /var/cache/apt/pkgcache.bin -mtime +1) ]; then
echo -e "${BLUE}📦 Updating system packages...${NC}"
apt-get update
apt-get upgrade -y
print_status "System updated"
else
print_info "System packages recently updated, skipping"
fi
# Install essential tools if not present
if ! command -v curl >/dev/null 2>&1 || ! command -v nc >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1; then
echo -e "${BLUE}📦 Installing essential tools...${NC}"
apt-get install -y curl netcat-openbsd git
print_status "Essential tools installed"
fi
}
# Install Node.js (if not already installed)
install_nodejs() {
# Force PATH refresh to ensure we get the latest Node.js
export PATH="/usr/bin:/usr/local/bin:$PATH"
hash -r # Clear bash command cache
NODE_VERSION=""
if command -v node >/dev/null 2>&1; then
NODE_VERSION=$(node --version)
NODE_MAJOR=$(echo $NODE_VERSION | cut -d'.' -f1 | sed 's/v//')
NODE_MINOR=$(echo $NODE_VERSION | cut -d'.' -f2)
echo -e "${BLUE}🔍 Detected Node.js version: $NODE_VERSION${NC}"
# Check if Node.js version is sufficient (need 20.19+ or 22.12+)
if [ "$NODE_MAJOR" -gt 22 ] || [ "$NODE_MAJOR" -eq 22 ] || ([ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -ge 19 ]); then
print_info "Node.js $NODE_VERSION is compatible (need 20.19+ or 22.12+)"
# Check if npm is available
if ! command -v npm >/dev/null 2>&1; then
echo -e "${BLUE}📦 Installing npm...${NC}"
apt-get install -y npm
fi
# Update npm to compatible version
echo -e "${BLUE}🔧 Ensuring npm compatibility...${NC}"
if [ "$NODE_MAJOR" -ge 22 ]; then
npm install -g npm@latest
else
npm install -g npm@10
fi
print_status "Node.js and npm ready"
return
else
print_warning "Node.js $NODE_VERSION is too old (need 20.19+ or 22.12+), upgrading..."
fi
else
echo -e "${BLUE}📦 Node.js not found, installing...${NC}"
fi
echo -e "${BLUE}📦 Installing Node.js 20...${NC}"
# Remove old Node.js if present
if command -v node >/dev/null 2>&1; then
echo -e "${YELLOW}🗑️ Removing old Node.js installation...${NC}"
apt-get remove -y nodejs npm || true
apt-get autoremove -y || true
# Clear alternatives
update-alternatives --remove-all node 2>/dev/null || true
update-alternatives --remove-all npm 2>/dev/null || true
fi
# Clean up old NodeSource repo if present
rm -f /etc/apt/sources.list.d/nodesource.list
rm -f /usr/share/keyrings/nodesource.gpg
# Install Node.js 20 from NodeSource
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
# Force PATH refresh again
export PATH="/usr/bin:/usr/local/bin:$PATH"
hash -r
# Verify installation
if ! command -v node >/dev/null 2>&1; then
print_error "Node.js installation failed - command not found"
exit 1
fi
if ! command -v npm >/dev/null 2>&1; then
print_error "npm installation failed - command not found"
exit 1
fi
# Update npm to compatible version for Node.js 20
echo -e "${BLUE}🔧 Updating npm to compatible version...${NC}"
npm install -g npm@10
NODE_VERSION=$(node --version)
NPM_VERSION=$(npm --version)
# Verify version is now correct
NODE_MAJOR=$(echo $NODE_VERSION | cut -d'.' -f1 | sed 's/v//')
if [ "$NODE_MAJOR" -lt 20 ]; then
print_error "Node.js upgrade failed - still showing version $NODE_VERSION"
exit 1
fi
print_status "Node.js $NODE_VERSION and npm $NPM_VERSION installed and verified"
}
# Install PostgreSQL (if not already installed)
install_postgresql() {
if check_system_component "postgresql"; then
print_info "PostgreSQL already installed and running"
return
fi
echo -e "${BLUE}🗄️ Installing PostgreSQL...${NC}"
apt-get install -y postgresql postgresql-contrib
systemctl enable postgresql
systemctl start postgresql
print_status "PostgreSQL installed and started"
}
# Install Nginx (if not already installed)
install_nginx() {
if check_system_component "nginx"; then
print_info "Nginx already installed and running"
return
fi
echo -e "${BLUE}🌐 Installing Nginx...${NC}"
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
# Configure firewall if ufw is available
if command -v ufw >/dev/null 2>&1; then
ufw allow 'Nginx Full' || true
fi
print_status "Nginx installed and started"
}
# Install Certbot (if SSL is enabled and not already installed)
install_certbot() {
if [ "$USE_LETSENCRYPT" != "true" ]; then
return
fi
if check_system_component "certbot"; then
print_info "Certbot already installed"
return
fi
echo -e "${BLUE}🔒 Installing Certbot...${NC}"
apt-get install -y certbot python3-certbot-nginx
print_status "Certbot installed"
}
# Setup database for instance
setup_database() {
echo -e "${BLUE}📋 Creating database: $DB_NAME${NC}"
# Drop and recreate database and user for clean state
sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;" || true
sudo -u postgres psql -c "DROP USER IF EXISTS $DB_USER;" || true
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME;"
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';"
# Grant comprehensive permissions
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;"
sudo -u postgres psql -c "ALTER USER $DB_USER CREATEDB;"
# Set schema permissions
sudo -u postgres psql -d $DB_NAME -c "GRANT USAGE ON SCHEMA public TO $DB_USER;"
sudo -u postgres psql -d $DB_NAME -c "GRANT CREATE ON SCHEMA public TO $DB_USER;"
sudo -u postgres psql -d $DB_NAME -c "ALTER SCHEMA public OWNER TO $DB_USER;"
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $DB_USER;"
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $DB_USER;"
# Test database connection
echo -e "${BLUE}🔍 Testing database connection...${NC}"
if PGPASSWORD="$DB_PASS" psql -h localhost -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" >/dev/null 2>&1; then
print_status "Database connection successful"
else
print_error "Database connection failed"
echo "Debug information:"
sudo -u postgres psql -c "\l" | grep "$DB_NAME" || echo "Database not found"
sudo -u postgres psql -c "\du" | grep "$DB_USER" || echo "User not found"
exit 1
fi
print_status "Database $DB_NAME and user $DB_USER created and tested"
}
# Clone application code
clone_application() {
echo -e "${BLUE}📥 Cloning PatchMon application...${NC}"
# Remove existing directory if it exists
rm -rf $APP_DIR
if [ "$PUBLIC_REPO_MODE" = "true" ]; then
# Public repo mode - use HTTPS clone directly
print_info "Public repository mode - using HTTPS clone"
# Convert SSH URL to HTTPS if needed
HTTPS_REPO=$(echo $GITHUB_REPO | sed 's|git@github.com:|https://github.com/|' | sed 's|\.git$|.git|')
git clone -b "$DEPLOYMENT_BRANCH" $HTTPS_REPO $APP_DIR
print_status "Repository cloned via HTTPS (public repo mode) from branch: $DEPLOYMENT_BRANCH"
else
# Original behavior - Try SSH first, fallback to HTTPS
SSH_REPO=$(echo $GITHUB_REPO | sed 's|https://github.com/|git@github.com:|')
if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"; then
print_info "GitHub SSH key detected, using SSH clone"
if git clone -b "$DEPLOYMENT_BRANCH" $SSH_REPO $APP_DIR 2>/dev/null; then
print_status "Repository cloned via SSH from branch: $DEPLOYMENT_BRANCH"
else
print_warning "SSH clone failed, trying HTTPS..."
git clone -b "$DEPLOYMENT_BRANCH" $GITHUB_REPO $APP_DIR
fi
else
git clone -b "$DEPLOYMENT_BRANCH" $GITHUB_REPO $APP_DIR
fi
fi
cd $APP_DIR
# Set initial ownership and create required directories
echo -e "${BLUE}🔐 Setting initial ownership and creating directories...${NC}"
chown -R www-data:www-data $APP_DIR
# Create logs directory immediately to prevent permission errors
mkdir -p $APP_DIR/backend/logs
chown -R www-data:www-data $APP_DIR/backend/logs
chmod 755 $APP_DIR/backend/logs
print_status "Repository cloned to $APP_DIR with correct ownership"
}
# Setup Node.js environment for instance
setup_node_environment() {
echo -e "${BLUE}📦 Setting up Node.js environment for instance...${NC}"
# Force PATH refresh to ensure we get the latest Node.js
export PATH="/usr/bin:/usr/local/bin:$PATH"
hash -r
# Verify Node.js and npm are available
if ! command -v node >/dev/null 2>&1; then
print_error "Node.js not found after installation. PATH issue detected."
echo "Current PATH: $PATH"
echo "Available node binaries:"
find /usr -name "node" 2>/dev/null || echo "No node binaries found"
exit 1
fi
if ! command -v npm >/dev/null 2>&1; then
print_error "npm not found after installation. PATH issue detected."
echo "Current PATH: $PATH"
echo "Available npm binaries:"
find /usr -name "npm" 2>/dev/null || echo "No npm binaries found"
exit 1
fi
NODE_VERSION=$(node --version)
NPM_VERSION=$(npm --version)
NODE_MAJOR=$(echo $NODE_VERSION | cut -d'.' -f1 | sed 's/v//')
NODE_MINOR=$(echo $NODE_VERSION | cut -d'.' -f2)
# Verify Node.js version is compatible
if [ "$NODE_MAJOR" -lt 20 ] || ([ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -lt 19 ]); then
print_error "Node.js version $NODE_VERSION is incompatible with Vite 7.1.5 (need 20.19+ or 22.12+)"
echo -e "${YELLOW}This suggests the Node.js upgrade failed. Please check the installation.${NC}"
exit 1
fi
print_status "Node.js environment ready: Node $NODE_VERSION, npm $NPM_VERSION (compatible)"
}
# Install application dependencies
install_dependencies() {
echo -e "${BLUE}📦 Installing application dependencies...${NC}"
# Root dependencies
if [ -f "$APP_DIR/package.json" ]; then
cd $APP_DIR
npm install
fi
# Backend dependencies
if [ -f "$APP_DIR/backend/package.json" ]; then
cd $APP_DIR/backend
npm install
fi
# Frontend dependencies
if [ -f "$APP_DIR/frontend/package.json" ]; then
cd $APP_DIR/frontend
npm install
fi
# Copy frontend server.js if nginx is disabled
if [ "$SETUP_NGINX" = "false" ]; then
echo -e "${BLUE}📁 Copying frontend server.js...${NC}"
cp $SCRIPT_DIR/frontend/server.js $APP_DIR/frontend/
print_status "Frontend server.js copied"
fi
print_status "Dependencies installed"
}
# Create environment files
create_env_files() {
echo -e "${BLUE}⚙️ Creating environment files...${NC}"
# Backend .env
cat > $APP_DIR/backend/.env << EOF
NODE_ENV=production
PORT=$BACKEND_PORT
API_VERSION=v1
# Database
DATABASE_URL=postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME?schema=public
# Security
CORS_ORIGINS=${SERVER_PROTOCOL_SEL}://$FQDN
TRUST_PROXY=1
ENABLE_HSTS=$([ "$USE_LETSENCRYPT" = "true" ] && echo "true" || echo "false")
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
AUTH_RATE_LIMIT_MAX=5
HOST_RATE_LIMIT_MAX=50
# Body Parsing
JSON_BODY_LIMIT=5mb
# Logging
ENABLE_LOGGING=true
# JWT Secret
JWT_SECRET=$JWT_SECRET
EOF
# Frontend .env (NODE_ENV not needed - Vite handles this automatically)
if [ "$SETUP_NGINX" = "true" ]; then
# With nginx - use FQDN
cat > $APP_DIR/frontend/.env << EOF
VITE_API_URL=${SERVER_PROTOCOL_SEL}://$FQDN/api/v1
VITE_FRONTEND_URL=${SERVER_PROTOCOL_SEL}://$FQDN
VITE_FRONTEND_PORT=$FRONTEND_PORT
VITE_BACKEND_PORT=$BACKEND_PORT
EOF
else
# Without nginx - use separate frontend and backend ports
cat > $APP_DIR/frontend/.env << EOF
VITE_API_URL=${SERVER_PROTOCOL_SEL}://$FQDN/api/v1
VITE_FRONTEND_URL=${SERVER_PROTOCOL_SEL}://$FQDN
VITE_FRONTEND_PORT=$FRONTEND_PORT
VITE_BACKEND_PORT=$BACKEND_PORT
EOF
fi
print_status "Environment files created"
}
# Run database migrations
run_migrations() {
echo -e "${BLUE}🗃️ Running database migrations...${NC}"
cd $APP_DIR/backend
# Test connection before migrations
if ! PGPASSWORD="$DB_PASS" psql -h localhost -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" >/dev/null 2>&1; then
print_error "Cannot connect to database before migrations"
echo "Debug information:"
sudo -u postgres psql -c "\l" | grep "$DB_NAME" || echo "Database not found"
sudo -u postgres psql -c "\du" | grep "$DB_USER" || echo "User not found"
exit 1
fi
# Generate Prisma client
npx prisma generate
# Run migrations
npx prisma migrate deploy
print_status "Database migrations completed"
}
# Seed default roles
seed_default_roles() {
echo -e "${BLUE}🛡️ Seeding default roles...${NC}"
cd $APP_DIR/backend
# Test connection before seeding
if ! PGPASSWORD="$DB_PASS" psql -h localhost -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" >/dev/null 2>&1; then
print_error "Cannot connect to database before seeding roles"
exit 1
fi
node -e "
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function seedRoles() {
try {
// Create admin role with full permissions
await prisma.rolePermissions.upsert({
where: { role: 'admin' },
update: {
canViewDashboard: true,
canViewHosts: true,
canManageHosts: true,
canViewPackages: true,
canManagePackages: true,
canViewUsers: true,
canManageUsers: true,
canViewReports: true,
canExportData: true,
canManageSettings: true
},
create: {
role: 'admin',
canViewDashboard: true,
canViewHosts: true,
canManageHosts: true,
canViewPackages: true,
canManagePackages: true,
canViewUsers: true,
canManageUsers: true,
canViewReports: true,
canExportData: true,
canManageSettings: true
}
});
// Create user role with read-only permissions
await prisma.rolePermissions.upsert({
where: { role: 'user' },
update: {
canViewDashboard: true,
canViewHosts: true,
canManageHosts: false,
canViewPackages: true,
canManagePackages: false,
canViewUsers: false,
canManageUsers: false,
canViewReports: true,
canExportData: false,
canManageSettings: false
},
create: {
role: 'user',
canViewDashboard: true,
canViewHosts: true,
canManageHosts: false,
canViewPackages: true,
canManagePackages: false,
canViewUsers: false,
canManageUsers: false,
canViewReports: true,
canExportData: false,
canManageSettings: false
}
});
console.log('✅ Default roles seeded successfully');
} catch (error) {
console.error('❌ Error seeding roles:', error.message);
process.exit(1);
} finally {
await prisma.\$disconnect();
}
}
seedRoles();
"
print_status "Default roles seeded"
}
# Build frontend
build_frontend() {
echo -e "${BLUE}🏗️ Building frontend...${NC}"
cd $APP_DIR/frontend
npm run build
print_status "Frontend built successfully"
}
# Fix permissions for the application
fix_permissions() {
echo -e "${BLUE}🔐 Setting final permissions...${NC}"
# Ensure entire directory is owned by www-data
chown -R www-data:www-data $APP_DIR
# Set directory permissions (755 = rwxr-xr-x)
find $APP_DIR -type d -exec chmod 755 {} \;
# Set file permissions (644 = rw-r--r--)
find $APP_DIR -type f -exec chmod 644 {} \;
# Make scripts executable
if [ -f "$APP_DIR/manage.sh" ]; then
chmod +x $APP_DIR/manage.sh
fi
# Ensure logs directory exists with correct permissions
mkdir -p $APP_DIR/backend/logs
chown -R www-data:www-data $APP_DIR/backend/logs
chmod 755 $APP_DIR/backend/logs
# Make sure node_modules have correct permissions for npm operations
if [ -d "$APP_DIR/node_modules" ]; then
chown -R www-data:www-data $APP_DIR/node_modules
fi
if [ -d "$APP_DIR/backend/node_modules" ]; then
chown -R www-data:www-data $APP_DIR/backend/node_modules
fi
if [ -d "$APP_DIR/frontend/node_modules" ]; then
chown -R www-data:www-data $APP_DIR/frontend/node_modules
fi
print_status "Final permissions set correctly - entire directory owned by www-data"
}
# Setup Nginx configuration
setup_nginx() {
echo -e "${BLUE}🌐 Setting up Nginx configuration...${NC}"
if [ "$USE_LETSENCRYPT" = "true" ]; then
# HTTP-only config first for Certbot challenge
cat > /etc/nginx/sites-available/$FQDN << EOF
server {
listen 80;
server_name $FQDN;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://\$server_name\$request_uri;
}
}
EOF
else
# HTTP-only configuration
cat > /etc/nginx/sites-available/$FQDN << EOF
server {
listen 80;
server_name $FQDN;
# Frontend
location / {
root $APP_DIR/frontend/dist;
try_files \$uri \$uri/ /index.html;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
}
# API routes
location /api/ {
proxy_pass http://localhost:$BACKEND_PORT;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Health check
location /health {
proxy_pass http://localhost:$BACKEND_PORT/health;
access_log off;
}
}
EOF
fi
# Enable site
ln -sf /etc/nginx/sites-available/$FQDN /etc/nginx/sites-enabled/$FQDN
# Test configuration
nginx -t
nginx -s reload
print_status "Nginx configuration created"
}
# Setup SSL with Let's Encrypt
setup_ssl() {
if [ "$USE_LETSENCRYPT" != "true" ]; then
return
fi
echo -e "${BLUE}🔒 Setting up SSL certificate...${NC}"
# Try to get SSL certificate
if ! certbot --nginx -d $FQDN --non-interactive --agree-tos --email admin@$FQDN --redirect; then
print_error "SSL certificate generation failed"
print_warning "This could be due to:"
print_warning " - Domain not pointing to this server"
print_warning " - Firewall blocking port 80/443"
print_warning " - DNS propagation issues"
print_warning " - Server not accessible from internet"
return 1
fi
# Update Nginx config with full HTTPS configuration
cat > /etc/nginx/sites-available/$FQDN << EOF
server {
listen 80;
server_name $FQDN;
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name $FQDN;
ssl_certificate /etc/letsencrypt/live/$FQDN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$FQDN/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Frontend
location / {
root $APP_DIR/frontend/dist;
try_files \$uri \$uri/ /index.html;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
# API routes
location /api/ {
proxy_pass http://localhost:$BACKEND_PORT;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Health check
location /health {
proxy_pass http://localhost:$BACKEND_PORT/health;
access_log off;
}
}
EOF
nginx -t
nginx -s reload
print_status "SSL certificate installed and Nginx updated"
return 0
}
# Setup systemd service
setup_service() {
echo -e "${BLUE}🔧 Setting up systemd service...${NC}"
# Create backend service file
cat > /etc/systemd/system/$SERVICE_NAME.service << EOF
[Unit]
Description=PatchMon Backend for $FQDN
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=$APP_DIR/backend
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=$BACKEND_PORT
Environment=DATABASE_URL=postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME?schema=public
# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=$APP_DIR
ProtectHome=true
ProtectKernelTunables=true
ProtectControlGroups=true
SyslogIdentifier=$SERVICE_NAME
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and enable service
systemctl daemon-reload
systemctl enable $SERVICE_NAME
systemctl start $SERVICE_NAME
# Wait a moment for service to start
sleep 3
if systemctl is-active --quiet $SERVICE_NAME; then
print_status "Backend service $SERVICE_NAME started successfully"
else
print_error "Backend service $SERVICE_NAME failed to start"
systemctl status $SERVICE_NAME
exit 1
fi
# Create frontend service if nginx is disabled
if [ "$SETUP_NGINX" = "false" ]; then
echo -e "${BLUE}🔧 Setting up frontend service...${NC}"
# Create frontend service file
cat > /etc/systemd/system/${SERVICE_NAME}-frontend.service << EOF
[Unit]
Description=PatchMon Frontend for $FQDN
After=network.target
Wants=$SERVICE_NAME.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=$APP_DIR/frontend
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=$FRONTEND_PORT
Environment=CORS_ORIGIN=${SERVER_PROTOCOL_SEL}://$FQDN
# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=$APP_DIR
ProtectHome=true
ProtectKernelTunables=true
ProtectControlGroups=true
SyslogIdentifier=${SERVICE_NAME}-frontend
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and enable frontend service
systemctl daemon-reload
systemctl enable ${SERVICE_NAME}-frontend
systemctl start ${SERVICE_NAME}-frontend
# Wait a moment for service to start
sleep 3
if systemctl is-active --quiet ${SERVICE_NAME}-frontend; then
print_status "Frontend service ${SERVICE_NAME}-frontend started successfully"
else
print_error "Frontend service ${SERVICE_NAME}-frontend failed to start"
systemctl status ${SERVICE_NAME}-frontend
exit 1
fi
fi
}
# Update database settings
update_database_settings() {
echo -e "${BLUE}⚙️ Updating database settings...${NC}"
cd $APP_DIR/backend
# Test connection before updating settings
if ! PGPASSWORD="$DB_PASS" psql -h localhost -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" >/dev/null 2>&1; then
print_error "Cannot connect to database before updating settings"
exit 1
fi
node -e "
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function updateSettings() {
try {
// Check if settings record exists, create or update
const existingSettings = await prisma.settings.findFirst();
const settingsData = {
serverUrl: '${SERVER_PROTOCOL_SEL}://$FQDN',
serverProtocol: '${SERVER_PROTOCOL_SEL}',
serverHost: '$FQDN',
serverPort: $SERVER_PORT_SEL,
frontendUrl: '${SERVER_PROTOCOL_SEL}://$FQDN',
updateInterval: 60,
autoUpdate: true
};
if (existingSettings) {
// Update existing settings
await prisma.settings.update({
where: { id: existingSettings.id },
data: settingsData
});
} else {
// Create new settings record
await prisma.settings.create({
data: settingsData
});
}
console.log('✅ Database settings updated successfully');
} catch (error) {
console.error('❌ Error updating settings:', error.message);
process.exit(1);
} finally {
await prisma.\$disconnect();
}
}
updateSettings();
"
print_status "Database settings updated"
}
# Create agent version
create_agent_version() {
echo -e "${BLUE}🤖 Creating agent version...${NC}"
cd $APP_DIR/backend
# Priority 1: Get version from agent script (most accurate for agent versions)
local current_version="N/A"
if [ -f "$APP_DIR/agents/patchmon-agent.sh" ]; then
current_version=$(grep '^AGENT_VERSION=' "$APP_DIR/agents/patchmon-agent.sh" | cut -d'"' -f2 2>/dev/null || echo "N/A")
if [ "$current_version" != "N/A" ] && [ -n "$current_version" ]; then
print_info "Detected agent version from script: $current_version"
fi
fi
# Priority 2: Get version from the codebase (if agent script version not found)
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
current_version=$(get_instance_version "$APP_DIR" "" "" "")
if [ "$current_version" != "N/A" ] && [ -n "$current_version" ]; then
print_info "Detected version from codebase: $current_version"
fi
fi
# Priority 3: Fallback to package.json version
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
if [ -f "package.json" ]; then
current_version=$(grep '"version"' "package.json" | head -1 | sed 's/.*"version":[[:space:]]*"\([^"]*\)".*/\1/')
if [ -n "$current_version" ]; then
print_info "Detected version from package.json: $current_version"
fi
fi
fi
# Check if we're updating an existing instance - if so, check what version is currently in the database
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
print_info "Checking existing agent versions in database..."
local db_version=$(node -e "
require('dotenv').config();
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
prisma.agentVersion.findFirst({
where: { isCurrent: true },
select: { version: true }
}).then(version => {
if (version) {
console.log(version.version);
} else {
console.log('N/A');
}
prisma.\$disconnect();
}).catch(() => {
console.log('N/A');
prisma.\$disconnect();
});
" 2>/dev/null || echo "N/A")
if [ "$db_version" != "N/A" ] && [ -n "$db_version" ]; then
current_version="$db_version"
print_info "Using existing database version: $current_version"
else
# Final fallback to 1.2.5 if still not found
current_version="1.2.5"
print_warning "Could not determine version, using fallback: $current_version"
fi
fi
print_info "Creating/updating agent version: $current_version"
print_info "This will ensure the latest agent script is available in the database"
# Test connection before creating agent version
if ! PGPASSWORD="$DB_PASS" psql -h localhost -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" >/dev/null 2>&1; then
print_error "Cannot connect to database before creating agent version"
exit 1
fi
# Copy agent script to backend directory
if [ -f "$APP_DIR/agents/patchmon-agent.sh" ]; then
cp "$APP_DIR/agents/patchmon-agent.sh" "$APP_DIR/backend/"
node -e "
const fs = require('fs');
require('dotenv').config({ path: './.env' });
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const currentVersion = '$current_version';
// Simple version comparison function
function compareVersions(version1, version2) {
const v1parts = version1.split('.').map(Number);
const v2parts = version2.split('.').map(Number);
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
const v1part = v1parts[i] || 0;
const v2part = v2parts[i] || 0;
if (v1part > v2part) return 1;
if (v1part < v2part) return -1;
}
return 0;
}
async function createAgentVersion() {
try {
const agentScript = fs.readFileSync('./patchmon-agent.sh', 'utf8');
// Check if current version already exists
const existingVersion = await prisma.agentVersion.findUnique({
where: { version: currentVersion }
});
if (existingVersion) {
// Version exists, always update the script content during updates
console.log('📝 Updating existing agent version ' + currentVersion + ' with latest script content...');
await prisma.agentVersion.update({
where: { version: currentVersion },
data: {
scriptContent: agentScript,
isCurrent: true,
releaseNotes: 'Version ' + currentVersion + ' - Updated Agent Script\\n\\nThis version contains the latest agent script from the codebase update.'
}
});
console.log('✅ Agent version ' + currentVersion + ' updated successfully with latest script');
} else {
// Version doesn't exist, create it
console.log('🆕 Creating new agent version ' + currentVersion + '...');
await prisma.agentVersion.create({
data: {
version: currentVersion,
scriptContent: agentScript,
isCurrent: true,
isDefault: false, // Don't set as default during updates
releaseNotes: 'Version ' + currentVersion + ' - Updated Agent Script\\n\\nThis version contains the latest agent script from the codebase update.'
}
});
console.log('✅ Agent version ' + currentVersion + ' created successfully');
}
// Set all other versions to not be current
await prisma.agentVersion.updateMany({
where: { version: { not: currentVersion } },
data: { isCurrent: false }
});
// Check if we should update older versions with the new script
const allVersions = await prisma.agentVersion.findMany({
orderBy: { version: 'desc' }
});
for (const version of allVersions) {
if (version.version !== currentVersion && compareVersions(currentVersion, version.version) > 0) {
console.log('🔄 Updating older version ' + version.version + ' with new script content...');
await prisma.agentVersion.update({
where: { id: version.id },
data: {
scriptContent: agentScript,
releaseNotes: 'Version ' + version.version + ' - Updated with latest script from ' + currentVersion + '\\n\\nThis version has been updated with the latest agent script content.'
}
});
}
}
console.log('✅ Agent version management completed successfully');
} catch (error) {
console.error('❌ Error creating agent version:', error.message);
process.exit(1);
} finally {
await prisma.\$disconnect();
}
}
createAgentVersion();
"
# Clean up
rm -f "$APP_DIR/backend/patchmon-agent.sh"
print_status "Agent version created"
else
print_warning "Agent script not found, skipping agent version creation"
fi
}
# Setup admin user interactively
setup_admin_user() {
echo -e "${BLUE}👤 Setting up admin user...${NC}"
cd $APP_DIR/backend
# Wait for service to be ready and test connection
echo -e "${BLUE}⏳ Waiting for backend service to be ready...${NC}"
# First, verify the service is actually running
if ! systemctl is-active $SERVICE_NAME >/dev/null 2>&1; then
print_error "Service $SERVICE_NAME is not running"
systemctl status $SERVICE_NAME
exit 1
fi
# Wait for the health endpoint to respond
local max_attempts=30
local attempt=1
local health_url="http://localhost:$BACKEND_PORT/health"
while [ $attempt -le $max_attempts ]; do
# Try multiple methods to check if service is ready
if curl -s --connect-timeout 5 --max-time 10 "$health_url" >/dev/null 2>&1; then
echo -e "${GREEN}✅ Backend service is ready${NC}"
break
elif nc -z localhost $BACKEND_PORT 2>/dev/null; then
# Port is open, but health endpoint might not be ready yet
echo -e "${YELLOW}⏳ Port $BACKEND_PORT is open, waiting for health endpoint... (attempt $attempt/$max_attempts)${NC}"
else
echo -e "${YELLOW}⏳ Waiting for backend service on port $BACKEND_PORT... (attempt $attempt/$max_attempts)${NC}"
fi
sleep 3
attempt=$((attempt + 1))
if [ $attempt -gt $max_attempts ]; then
print_error "Backend service failed to become ready after $max_attempts attempts"
echo -e "${BLUE}🔍 Debugging information:${NC}"
echo "Service status:"
systemctl status $SERVICE_NAME --no-pager
echo ""
echo "Port check:"
netstat -tuln | grep ":$BACKEND_PORT" || echo "Port $BACKEND_PORT not listening"
echo ""
echo "Recent logs:"
journalctl -u $SERVICE_NAME -n 20 --no-pager
exit 1
fi
done
# Test database connection
if ! PGPASSWORD="$DB_PASS" psql -h localhost -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" >/dev/null 2>&1; then
print_error "Cannot connect to database before admin setup"
exit 1
fi
# Copy and run admin setup script
cp $APP_DIR/setup-admin-user.js $APP_DIR/backend/
echo -e "${BLUE}🔧 Creating admin user interactively...${NC}"
echo -e "${YELLOW}Please follow the prompts to create your admin user:${NC}"
# Run admin setup with FQDN environment variable
FQDN="$FQDN" node $APP_DIR/backend/setup-admin-user.js
# Clean up
rm -f $APP_DIR/backend/setup-admin-user.js
print_status "Admin user setup completed"
}
# Setup log rotation
setup_log_rotation() {
echo -e "${BLUE}📋 Setting up log rotation...${NC}"
cat > /etc/logrotate.d/$SERVICE_NAME << EOF
/var/log/$SERVICE_NAME.log {
daily
missingok
rotate 52
compress
delaycompress
notifempty
create 0644 www-data www-data
postrotate
systemctl reload $SERVICE_NAME
endscript
}
EOF
print_status "Log rotation configured"
}
# Save credentials
save_credentials() {
echo -e "${BLUE}💾 Saving instance credentials...${NC}"
cat > $APP_DIR/credentials.txt << EOF
# PatchMon Instance Credentials for $FQDN
# Generated on: $(date)
#
# IMPORTANT: Keep this file secure and delete it after noting the credentials
## Database Credentials
Database Name: $DB_NAME
Database User: $DB_USER
Database Password: $DB_PASS
## JWT Secret
JWT Secret: $JWT_SECRET
## Application URLs
Frontend URL: https://$FQDN
Backend API: https://$FQDN/api/v1
Backend Port: $BACKEND_PORT
Frontend Port: $FRONTEND_PORT
## Default Admin Login
Username: admin
Password: admin123
(Please change this password after first login)
## Service Management
Service Name: $SERVICE_NAME
App Directory: $APP_DIR
Management Script: $APP_DIR/manage.sh
## Database Connection String
DATABASE_URL=postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME?schema=public
EOF
chmod 600 $APP_DIR/credentials.txt
chown www-data:www-data $APP_DIR/credentials.txt
print_status "Credentials saved to $APP_DIR/credentials.txt"
}
# Create management script for instance
create_management_script() {
echo -e "${BLUE}📝 Creating instance management script...${NC}"
cat > $APP_DIR/manage.sh << EOF
#!/bin/bash
# Management script for $FQDN
case \$1 in
"status")
systemctl status $SERVICE_NAME
;;
"start")
systemctl start $SERVICE_NAME
;;
"stop")
systemctl stop $SERVICE_NAME
;;
"restart")
systemctl restart $SERVICE_NAME
;;
"logs")
journalctl -u $SERVICE_NAME -f
;;
"update")
cd $APP_DIR
git config --global --add safe.directory $APP_DIR 2>/dev/null || true
git pull origin $DEPLOYMENT_BRANCH
# Clean reinstall ALL dependencies (fixes workspace issues)
echo "Clean reinstalling ALL dependencies..."
# Remove root node_modules first
echo "Removing root node_modules..."
rm -rf node_modules package-lock.json 2>/dev/null || true
# Clean reinstall root dependencies
echo "Installing root dependencies..."
npm install
# Install backend dependencies
echo "Installing backend dependencies..."
cd backend && npm install
if [ -f ".env" ]; then
export \$(grep -v '^#' .env | xargs)
# Debug: Check environment variables
echo "Environment variables loaded from .env"
echo "DATABASE_URL: \${DATABASE_URL:0:50}..." # Show first 50 chars only for security
echo "DB_HOST: \$DB_HOST"
echo "DB_PORT: \$DB_PORT"
echo "DB_NAME: \$DB_NAME"
echo "DB_USER: \$DB_USER"
# Try different ways to run Prisma migrations
# Skip npx if we know parent binary exists (npx has permission issues)
if [ -f "../node_modules/.bin/prisma" ]; then
chmod +x ../node_modules/.bin/prisma
node ../node_modules/.bin/prisma migrate deploy
elif [ -f "./node_modules/.bin/prisma" ]; then
chmod +x ./node_modules/.bin/prisma
node ./node_modules/.bin/prisma migrate deploy
elif [ -f "../node_modules/prisma/build/index.js" ]; then
node ../node_modules/prisma/build/index.js migrate deploy
elif [ -f "./node_modules/prisma/build/index.js" ]; then
node ./node_modules/prisma/build/index.js migrate deploy
elif command -v npx >/dev/null 2>&1; then
npx prisma migrate deploy
else
echo "Error: Prisma CLI not found. Trying to install..."
npm install prisma @prisma/client
npx prisma migrate deploy
fi
else
echo "Error: .env file not found"
exit 1
fi
# Stop service before rebuilding frontend
echo "Stopping service before frontend rebuild..."
systemctl stop $SERVICE_NAME 2>/dev/null || echo "Service was not running or failed to stop"
# Wait a moment for service to fully stop
echo "Waiting for service to fully stop..."
sleep 2
# Clean reinstall frontend dependencies (fixes .bin directory issues)
echo "Clean reinstalling frontend dependencies..."
cd ../frontend
# Debug: Check if we're in the right place
echo "Current directory: $(pwd)"
echo "Contents of frontend directory:"
ls -la
# Check if package.json exists
if [ ! -f "package.json" ]; then
echo "Error: package.json not found in frontend directory!"
exit 1
fi
echo "Found package.json, proceeding with clean install..."
# Fix ownership before clean install
echo "Fixing directory ownership..."
chown -R root:root $(pwd) 2>/dev/null || echo "Warning: Could not change ownership"
# More aggressive cleanup
echo "Performing aggressive cleanup..."
rm -rf node_modules package-lock.json .npm 2>/dev/null || true
npm cache clean --force 2>/dev/null || true
# Force a complete fresh install
echo "Running fresh npm install..."
if npm install --no-cache --force; then
echo "npm install completed successfully"
echo "Checking if .bin directory was created:"
if [ -d "node_modules/.bin" ]; then
echo ".bin directory created successfully:"
ls -la node_modules/.bin/ | head -5
else
echo "Warning: Still no .bin directory after npm install"
fi
else
echo "Error: npm install failed!"
exit 1
fi
# Fix ownership back to www-data after install
echo "Restoring www-data ownership..."
chown -R www-data:www-data $(pwd) 2>/dev/null || echo "Warning: Could not restore www-data ownership"
# Generate Prisma client after fresh install
echo "Generating Prisma client..."
cd ../backend
if npx prisma generate; then
echo "Prisma client generated successfully"
else
echo "Prisma generate failed, trying alternative method..."
node ../node_modules/.bin/prisma generate || echo "Error: Prisma generate failed completely"
fi
# Fix Vite and other binary permissions
echo "Fixing frontend binary permissions..."
# Get the frontend directory path
FRONTEND_DIR="$(pwd)"
echo "Frontend directory: $FRONTEND_DIR"
# Debug: Check what's in the .bin directory
echo "Contents of .bin directory:"
ls -la "$FRONTEND_DIR/node_modules/.bin/" 2>/dev/null || echo "No .bin directory found"
# Fix all binaries in .bin directory with more aggressive approach
if [ -d "$FRONTEND_DIR/node_modules/.bin" ]; then
echo "Fixing .bin directory permissions..."
chmod -R 755 "$FRONTEND_DIR/node_modules/.bin/"
chmod -R +x "$FRONTEND_DIR/node_modules/.bin/"
echo "Fixed .bin directory permissions"
fi
# Fix Vite specifically with multiple approaches
if [ -f "$FRONTEND_DIR/node_modules/.bin/vite" ]; then
echo "Fixing vite binary permissions..."
chmod 755 "$FRONTEND_DIR/node_modules/.bin/vite"
chmod +x "$FRONTEND_DIR/node_modules/.bin/vite"
echo "Vite binary permissions: $(ls -la "$FRONTEND_DIR/node_modules/.bin/vite" 2>/dev/null || echo 'not found')"
fi
# Fix all node_modules binaries
echo "Fixing all node_modules binary permissions..."
find "$FRONTEND_DIR/node_modules" -name "*.js" -path "*/bin/*" -exec chmod +x {} \; 2>/dev/null || true
find "$FRONTEND_DIR/node_modules" -name "vite*" -exec chmod +x {} \; 2>/dev/null || true
# Fix Vite in node_modules/vite
if [ -d "$FRONTEND_DIR/node_modules/vite" ]; then
echo "Fixing vite package permissions..."
chmod -R 755 "$FRONTEND_DIR/node_modules/vite/"
find "$FRONTEND_DIR/node_modules/vite" -name "vite*" -exec chmod +x {} \; 2>/dev/null || true
echo "Fixed vite directory permissions"
fi
# Also try to fix any vite binaries in the current directory
find . -name "vite*" -exec chmod +x {} \; 2>/dev/null || true
echo "Fixed any vite binaries in current directory"
# Try npm run build first, fallback to npx if it fails
if ! npm run build; then
echo "npm run build failed, trying alternative methods..."
# Try npx vite build
if command -v npx >/dev/null 2>&1; then
echo "Trying npx vite build..."
if npx vite build; then
echo "npx vite build succeeded"
else
echo "npx vite build failed, trying node wrapper..."
# Try running vite with node wrapper
if [ -f "./node_modules/.bin/vite" ]; then
echo "Trying node ./node_modules/.bin/vite build..."
node ./node_modules/.bin/vite build
elif [ -f "./node_modules/vite/bin/vite.js" ]; then
echo "Trying node ./node_modules/vite/bin/vite.js build..."
node ./node_modules/vite/bin/vite.js build
else
echo "Error: All build methods failed"
exit 1
fi
fi
else
echo "Error: npx not available and npm run build failed"
exit 1
fi
fi
systemctl restart $SERVICE_NAME
;;
"backup")
pg_dump -h localhost -U $DB_USER $DB_NAME > backup_\$(date +%Y%m%d_%H%M%S).sql
echo "Database backup created"
;;
"credentials")
echo "Credentials file: $APP_DIR/credentials.txt"
if [ -f "$APP_DIR/credentials.txt" ]; then
echo "Credentials file exists. Use 'cat $APP_DIR/credentials.txt' to view"
else
echo "Credentials file not found"
fi
;;
"reset-admin")
echo "Resetting admin password to admin123..."
cd $APP_DIR/backend
node -e "
const { PrismaClient } = require('@prisma/client');
const bcrypt = require('bcryptjs');
const prisma = new PrismaClient();
async function resetAdminPassword() {
try {
const adminUser = await prisma.user.findFirst({
where: { role: 'admin' }
});
if (adminUser) {
const hashedPassword = await bcrypt.hash('admin123', 10);
await prisma.user.update({
where: { id: adminUser.id },
data: { password: hashedPassword }
});
console.log('✅ Admin password reset to admin123');
} else {
console.log('❌ No admin user found');
}
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
await prisma.\$disconnect();
}
}
resetAdminPassword();
"
;;
*)
echo "Usage: \$0 {status|start|stop|restart|logs|update|backup|credentials|reset-admin}"
echo ""
echo "Commands:"
echo " status - Show service status"
echo " start - Start the service"
echo " stop - Stop the service"
echo " restart - Restart the service"
echo " logs - Show live logs"
echo " update - Update application code and restart"
echo " backup - Create database backup"
echo " credentials - Show credentials file location"
echo " reset-admin - Reset admin password to admin123"
;;
esac
EOF
chmod +x $APP_DIR/manage.sh
print_status "Management script created at $APP_DIR/manage.sh"
}
# Interactive timezone setup
setup_timezone_interactive() {
echo ""
print_info "🌍 Timezone Configuration"
# Show current timezone
current_tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown")
echo -e "${YELLOW}Current system timezone: $current_tz${NC}"
echo ""
# List common timezones
echo -e "${BLUE}Common timezones:${NC}"
echo "1) UTC"
echo "2) America/New_York (EST/EDT)"
echo "3) America/Chicago (CST/CDT)"
echo "4) America/Denver (MST/MDT)"
echo "5) America/Los_Angeles (PST/PDT)"
echo "6) Europe/London (GMT/BST)"
echo "7) Europe/Paris (CET/CEST)"
echo "8) Asia/Tokyo (JST)"
echo "9) Asia/Shanghai (CST)"
echo "10) Australia/Sydney (AEST/AEDT)"
echo "11) Custom timezone"
echo "12) Skip timezone setup"
echo ""
while true; do
read -p "Select timezone (1-12): " tz_choice
case $tz_choice in
1) selected_tz="UTC"; break ;;
2) selected_tz="America/New_York"; break ;;
3) selected_tz="America/Chicago"; break ;;
4) selected_tz="America/Denver"; break ;;
5) selected_tz="America/Los_Angeles"; break ;;
6) selected_tz="Europe/London"; break ;;
7) selected_tz="Europe/Paris"; break ;;
8) selected_tz="Asia/Tokyo"; break ;;
9) selected_tz="Asia/Shanghai"; break ;;
10) selected_tz="Australia/Sydney"; break ;;
11)
echo ""
read -p "Enter custom timezone (e.g., America/New_York): " selected_tz
if [ -n "$selected_tz" ]; then
# Validate timezone
if timedatectl list-timezones | grep -q "^$selected_tz$"; then
break
else
print_error "Invalid timezone. Please enter a valid timezone from the list."
echo "You can see all available timezones with: timedatectl list-timezones"
continue
fi
else
print_error "Timezone cannot be empty."
continue
fi
;;
12)
print_info "Skipping timezone setup"
return
;;
*)
print_error "Invalid choice. Please select 1-12."
;;
esac
done
# Set timezone
if [ -n "$selected_tz" ]; then
print_info "Setting timezone to $selected_tz..."
if sudo timedatectl set-timezone "$selected_tz" 2>/dev/null; then
print_status "Timezone set to $selected_tz"
else
print_error "Failed to set timezone. You may need to run this script with sudo or set timezone manually."
fi
fi
}
# Deploy new instance
deploy_instance() {
# Set default deployment branch
DEPLOYMENT_BRANCH="${DEPLOYMENT_BRANCH:-main}"
# Interactive deployment - ask for configuration
print_info "🚀 PatchMon Interactive Deployment"
echo ""
# Q1: What FQDN will you set?
while true; do
echo -e "${BLUE}Q1) What FQDN will you set?${NC}"
echo -e "${YELLOW} Enter the fully qualified domain name for this PatchMon instance:${NC}"
read -p " FQDN: " fqdn
if [ -z "$fqdn" ]; then
print_error "FQDN cannot be empty. Please enter a valid domain name."
continue
fi
# Basic FQDN validation
if [[ $fqdn =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
break
else
print_error "Invalid FQDN format. Please enter a valid domain name (e.g., patchmon.company.com)"
fi
done
# Q2: Which deployment branch do you want to use?
echo ""
echo -e "${BLUE}Q2) Which deployment branch do you want to use?${NC}"
echo -e "${YELLOW} 1 - main (stable, production-ready)${NC}"
echo -e "${YELLOW} 2 - dev (development, latest features)${NC}"
while true; do
read -p " Choose branch (1/2): " branch_choice
case $branch_choice in
1 )
DEPLOYMENT_BRANCH="main"
print_info "Selected branch: main (stable)"
break
;;
2 )
DEPLOYMENT_BRANCH="dev"
print_info "Selected branch: dev (development)"
break
;;
* )
print_error "Please choose 1 for main or 2 for dev"
;;
esac
done
# Q3: Do you wish to setup nginx?
echo ""
echo -e "${BLUE}Q3) Do you wish to setup nginx?${NC}"
echo -e "${YELLOW} This will install and configure nginx as a reverse proxy${NC}"
echo -e "${YELLOW} Choose 'n' if you're using an external proxy server${NC}"
while true; do
read -p " Setup nginx? (y/n): " setup_nginx
case $setup_nginx in
[Yy]* )
SETUP_NGINX="true"
break
;;
[Nn]* )
SETUP_NGINX="false"
break
;;
* )
print_error "Please answer yes (y) or no (n)"
;;
esac
done
# Q4: Do you wish to setup Let's Encrypt? (only if nginx is being set up)
if [ "$SETUP_NGINX" = "true" ]; then
echo ""
echo -e "${BLUE}Q4) Do you wish to setup Let's Encrypt? (only for public servers with ports 80/443 open)${NC}"
echo -e "${YELLOW} This will automatically obtain SSL certificates for your domain${NC}"
while true; do
read -p " Setup Let's Encrypt? (y/n): " setup_letsencrypt
case $setup_letsencrypt in
[Yy]* )
USE_LETSENCRYPT="true"
SERVER_PROTOCOL_SEL="https"
SERVER_PORT_SEL=443
break
;;
[Nn]* )
USE_LETSENCRYPT="false"
SERVER_PROTOCOL_SEL="http"
SERVER_PORT_SEL=80
break
;;
* )
print_error "Please answer yes (y) or no (n)"
;;
esac
done
else
# No nginx setup, but still need to ask about protocol for CORS
echo ""
echo -e "${BLUE}Q4) What protocol will you use to access the application?${NC}"
echo -e "${YELLOW} This affects CORS settings and API configuration${NC}"
while true; do
read -p " Protocol (http/https): " protocol_choice
case $protocol_choice in
[Hh][Tt][Tt][Pp]* )
SERVER_PROTOCOL_SEL="http"
break
;;
[Hh][Tt][Tt][Pp][Ss]* )
SERVER_PROTOCOL_SEL="https"
break
;;
* )
print_error "Please enter 'http' or 'https'"
;;
esac
done
USE_LETSENCRYPT="false"
SERVER_PORT_SEL=3001
FRONTEND_PORT=3000
print_info "Using $SERVER_PROTOCOL_SEL - Frontend on port $FRONTEND_PORT, Backend on port $SERVER_PORT_SEL"
print_info "External NPM should route /api/* to port $SERVER_PORT_SEL and everything else to port $FRONTEND_PORT"
fi
# Q5: Do you wish to setup time zone?
echo ""
echo -e "${BLUE}Q5) Do you wish to setup time zone?${NC}"
echo -e "${YELLOW} This will configure the system timezone for better log timestamps${NC}"
while true; do
read -p " Setup timezone? (y/n): " setup_timezone
case $setup_timezone in
[Yy]* )
setup_timezone_interactive
break
;;
[Nn]* )
print_info "Skipping timezone setup"
break
;;
* )
print_error "Please answer yes (y) or no (n)"
;;
esac
done
# Use default repository
local github_repo="$DEFAULT_GITHUB_REPO"
if [ "$PUBLIC_REPO_MODE" = "true" ]; then
# Convert SSH URL to HTTPS for public repo mode
github_repo=$(echo "$DEFAULT_GITHUB_REPO" | sed 's|git@github.com:|https://github.com/|')
print_info "Using default repository (HTTPS): $github_repo"
else
print_info "Using default repository: $github_repo"
fi
# Export configuration variables
export USE_LETSENCRYPT SERVER_PROTOCOL_SEL SERVER_PORT_SEL DEPLOYMENT_BRANCH
# Display configuration summary
echo ""
print_info "📋 Configuration Summary:"
echo -e "${YELLOW} FQDN: $fqdn${NC}"
echo -e "${YELLOW} Branch: $DEPLOYMENT_BRANCH${NC}"
echo -e "${YELLOW} Repository: $github_repo${NC}"
echo -e "${YELLOW} Nginx Setup: $([ "$SETUP_NGINX" = "true" ] && echo "Yes" || echo "No (Direct Backend Access)")${NC}"
if [ "$SETUP_NGINX" = "true" ]; then
echo -e "${YELLOW} SSL Setup: $([ "$USE_LETSENCRYPT" = "true" ] && echo "Let's Encrypt (HTTPS)" || echo "HTTP Only")${NC}"
else
echo -e "${YELLOW} Frontend Port: $FRONTEND_PORT${NC}"
echo -e "${YELLOW} Backend Port: $BACKEND_PORT${NC}"
echo -e "${YELLOW} Access URL: ${SERVER_PROTOCOL_SEL}://$FQDN (via external NPM)${NC}"
echo -e "${YELLOW} NPM Routing: /api/* → port $BACKEND_PORT, /* → port $FRONTEND_PORT${NC}"
fi
echo -e "${YELLOW} Timezone Setup: $([ "$setup_timezone" = "y" ] && echo "Yes" || echo "Skipped")${NC}"
echo ""
# Confirm deployment
while true; do
read -p "Proceed with deployment? (y/n): " confirm_deploy
case $confirm_deploy in
[Yy]* ) break ;;
[Nn]* )
print_info "Deployment cancelled by user"
exit 0
;;
* )
print_error "Please answer yes (y) or no (n)"
;;
esac
done
# Check if instance already exists
if [ -d "/opt/patchmon-$fqdn" ]; then
print_error "Instance for $fqdn already exists at /opt/patchmon-$fqdn"
print_info "Use 'update' command to update existing instance"
exit 1
fi
FQDN=$fqdn
GITHUB_REPO=$github_repo
print_info "🚀 Deploying PatchMon instance for $FQDN"
# Initialize variables
init_instance_vars
# Display generated credentials
echo -e "${BLUE}🔐 Auto-generated credentials:${NC}"
echo -e "${YELLOW}Database Name: $DB_NAME${NC}"
echo -e "${YELLOW}Database User: $DB_USER${NC}"
echo -e "${YELLOW}Database Password: $DB_PASS${NC}"
echo -e "${YELLOW}JWT Secret: $JWT_SECRET${NC}"
echo -e "${YELLOW}Backend Port: $BACKEND_PORT${NC}"
echo -e "${YELLOW}Frontend Port: $FRONTEND_PORT${NC}"
echo -e "${BLUE}📁 App directory: $APP_DIR${NC}"
echo -e "${BLUE}🗄️ Database: $DB_NAME${NC}"
echo -e "${BLUE}👤 Database user: $DB_USER${NC}"
echo ""
# SSL configuration already handled in interactive questions above
# Only configure timezone on first deployment
if ! check_system_component "postgresql"; then
configure_timezone
fi
# System setup (smart detection)
update_system
install_nodejs
install_postgresql
# Install nginx only if requested
if [ "$SETUP_NGINX" = "true" ]; then
install_nginx
install_certbot
else
print_info "Skipping nginx and certbot installation"
fi
# Instance-specific setup
setup_database
clone_application
setup_node_environment
install_dependencies
create_env_files
run_migrations
seed_default_roles
build_frontend
# Setup nginx only if requested
if [ "$SETUP_NGINX" = "true" ]; then
setup_nginx
if setup_ssl; then
print_status "SSL certificate installed successfully"
else
print_warning "SSL certificate installation failed - continuing without SSL"
print_info "You can configure SSL later through your external NPM or manually"
# Update configuration to reflect no SSL
USE_LETSENCRYPT="false"
SERVER_PROTOCOL_SEL="http"
SERVER_PORT_SEL=80
fi
else
print_info "Skipping nginx configuration - using direct backend access"
fi
setup_service
update_database_settings
create_agent_version
setup_admin_user
setup_log_rotation
save_credentials
create_management_script
fix_permissions
# Final status
echo -e "${GREEN}🎉 PatchMon deployment completed successfully!${NC}"
echo -e "${GREEN}🌐 Frontend URL: ${SERVER_PROTOCOL_SEL}://$FQDN${NC}"
echo -e "${GREEN}🔗 API URL: ${SERVER_PROTOCOL_SEL}://$FQDN/api/v1${NC}"
echo -e "${GREEN}⚡ Backend Port: $BACKEND_PORT${NC}"
echo -e "${GREEN}📁 App directory: $APP_DIR${NC}"
echo -e "${GREEN}🔧 Management: $APP_DIR/manage.sh${NC}"
echo -e "${GREEN}📊 Service: systemctl status $SERVICE_NAME${NC}"
echo -e "${YELLOW}🔐 Credentials saved to: $APP_DIR/credentials.txt${NC}"
echo -e "${YELLOW}⚠️ Please note down the credentials and delete the file for security${NC}"
echo -e "${BLUE}📋 Next steps:${NC}"
echo -e "${BLUE} 1. Visit ${SERVER_PROTOCOL_SEL}://$FQDN and login with your admin credentials${NC}"
echo -e "${BLUE} 2. Use '$APP_DIR/manage.sh' for service management${NC}"
echo -e "${BLUE} 3. Check '$APP_DIR/manage.sh credentials' for database details${NC}"
echo -e "${BLUE} 4. Install agents using: curl -s ${SERVER_PROTOCOL_SEL}://$FQDN/api/v1/hosts/agent/download | bash${NC}"
}
# Interactive instance selection for update
interactive_update() {
print_info "🔍 Scanning for PatchMon instances..."
echo ""
# Collect all instances
declare -a instances
declare -a fqdns
declare -a app_dirs
declare -a service_names
declare -a statuses
declare -a versions
local count=0
# Parse systemd services to find instances
for service in /etc/systemd/system/patchmon-*.service; do
if [ -f "$service" ]; then
local service_name=$(basename "$service" .service)
local fqdn=$(grep "Description" "$service" | sed 's/.*for //' | head -1)
local status=$(systemctl is-active "$service_name" 2>/dev/null || echo "inactive")
local app_dir="/opt/patchmon-$fqdn"
# Try to find the app directory (handle custom paths)
if [ ! -d "$app_dir" ]; then
for dir in /opt/patchmon-*; do
if [ -d "$dir" ] && [ -f "$dir/backend/.env" ]; then
if grep -q "$fqdn" "$dir/backend/.env" 2>/dev/null; then
app_dir="$dir"
break
fi
fi
done
fi
# Get version using unified function
local port=$(grep "Environment=PORT=" "/etc/systemd/system/$service_name.service" | cut -d'=' -f3 | head -1)
if [ -z "$port" ]; then
port=$(grep "PORT=" "/etc/systemd/system/$service_name.service" | cut -d'=' -f2 | head -1)
fi
local version=$(get_instance_version "$app_dir" "$fqdn" "$status" "$port")
count=$((count + 1))
instances+=("$count")
fqdns+=("$fqdn")
app_dirs+=("$app_dir")
service_names+=("$service_name")
statuses+=("$status")
versions+=("$version")
fi
done
if [ $count -eq 0 ]; then
print_error "No PatchMon instances found"
exit 1
fi
# Display instances
print_info "📋 Found $count PatchMon instance(s):"
echo ""
printf "${BLUE}%-3s %-30s %-10s %-12s %-40s${NC}\n" "ID" "FQDN" "Status" "Version" "Path"
printf "${BLUE}%-3s %-30s %-10s %-12s %-40s${NC}\n" "---" "$(printf '%*s' 30 | tr ' ' '-')" "$(printf '%*s' 10 | tr ' ' '-')" "$(printf '%*s' 12 | tr ' ' '-')" "$(printf '%*s' 40 | tr ' ' '-')"
for i in "${!instances[@]}"; do
local status_display
if [ "${statuses[$i]}" = "active" ]; then
status_display="$(printf "${GREEN}%-10s${NC}" "${statuses[$i]}")"
else
status_display="$(printf "${RED}%-10s${NC}" "${statuses[$i]}")"
fi
printf "%-3s %-30s %s %-12s %-40s\n" "${instances[$i]}" "${fqdns[$i]}" "$status_display" "${versions[$i]}" "${app_dirs[$i]}"
done
echo ""
print_info "💡 Select instances to update:"
print_info " • Enter single number (e.g., 1)"
print_info " • Enter multiple numbers separated by spaces (e.g., 1 3 5)"
print_info " • Enter range (e.g., 1-3)"
print_info " • Enter 'all' to update all instances"
print_info " • Enter 'q' to quit"
echo ""
read -p "Select instances to update: " selection
if [ "$selection" = "q" ]; then
print_info "Update cancelled"
exit 0
fi
# Parse selection
declare -a selected_indices
if [ "$selection" = "all" ]; then
for i in "${!instances[@]}"; do
selected_indices+=("$i")
done
elif [[ "$selection" =~ ^[0-9]+-[0-9]+$ ]]; then
# Handle range (e.g., 1-3)
local start=$(echo "$selection" | cut -d'-' -f1)
local end=$(echo "$selection" | cut -d'-' -f2)
for ((i=start; i<=end; i++)); do
if [ $i -le $count ]; then
selected_indices+=("$((i-1))")
fi
done
else
# Handle individual numbers or space-separated list
for num in $selection; do
if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le $count ]; then
selected_indices+=("$((num-1))")
else
print_error "Invalid selection: $num (must be between 1 and $count)"
exit 1
fi
done
fi
if [ ${#selected_indices[@]} -eq 0 ]; then
print_error "No valid instances selected"
exit 1
fi
# Confirm selection
echo ""
print_info "📝 Selected instances for update:"
for idx in "${selected_indices[@]}"; do
printf " ${GREEN}%s${NC} - %s (%s)\n" "${instances[$idx]}" "${fqdns[$idx]}" "${app_dirs[$idx]}"
done
echo ""
read -p "Proceed with update? (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
print_info "Update cancelled"
exit 0
fi
# Update selected instances
local success_count=0
local failed_instances=()
for idx in "${selected_indices[@]}"; do
local fqdn="${fqdns[$idx]}"
local app_dir="${app_dirs[$idx]}"
local service_name="${service_names[$idx]}"
echo ""
print_info "🚀 Updating instance ${instances[$idx]}: $fqdn"
print_info " Path: $app_dir"
print_info " Service: $service_name"
echo ""
if update_single_instance "$fqdn" "$app_dir" "$service_name"; then
success_count=$((success_count + 1))
print_status "✅ Instance ${instances[$idx]} ($fqdn) updated successfully"
else
failed_instances+=("${instances[$idx]} ($fqdn)")
print_error "❌ Instance ${instances[$idx]} ($fqdn) update failed"
fi
# Add separator between instances
if [ $idx != "${selected_indices[-1]}" ]; then
echo ""
echo "$(printf '%*s' 80 | tr ' ' '=')"
fi
done
# Final summary
echo ""
echo "$(printf '%*s' 80 | tr ' ' '=')"
print_info "📊 Update Summary:"
print_status "✅ Successfully updated: $success_count/${#selected_indices[@]} instances"
if [ ${#failed_instances[@]} -gt 0 ]; then
print_error "❌ Failed instances:"
for failed in "${failed_instances[@]}"; do
echo " - $failed"
done
exit 1
else
print_status "🎉 All selected instances updated successfully!"
fi
}
# Update existing instance (refactored to support both direct and interactive calls)
update_instance() {
local fqdn=$1
local custom_path=$2
# If no parameters provided, show interactive selection
if [ $# -eq 0 ]; then
interactive_update
return
fi
if [ $# -gt 2 ]; then
print_error "Usage: $0 update [fqdn] [custom-path]"
print_info "Examples:"
print_info " $0 update # Interactive mode"
print_info " $0 update pmon.manage.9.technology # Update specific instance"
print_info " $0 update pmon.manage.9.technology /opt/custom-path # Update with custom path"
exit 1
fi
local app_dir
local service_name
if [ -n "$custom_path" ]; then
# Use custom path provided by user
app_dir="$custom_path"
service_name=$(basename "$app_dir" | sed 's/^patchmon-/patchmon-/')
if [[ "$service_name" != patchmon-* ]]; then
service_name="patchmon-$(basename "$app_dir")"
fi
print_info "Using custom path: $app_dir"
print_info "Detected service name: $service_name"
else
# Use standard path based on FQDN
app_dir="/opt/patchmon-$fqdn"
service_name="patchmon-$fqdn"
# If standard path doesn't exist, try to find it automatically
if [ ! -d "$app_dir" ]; then
print_info "Standard path $app_dir not found, searching for instance..."
# Look for directories that might match this instance
local found_dirs=()
for dir in /opt/patchmon-*; do
if [ -d "$dir" ]; then
# Check if this directory contains a .env file with matching FQDN
if [ -f "$dir/backend/.env" ]; then
if grep -q "$fqdn" "$dir/backend/.env" 2>/dev/null; then
found_dirs+=("$dir")
fi
fi
# Also check if the directory name contains parts of the FQDN
if [[ "$dir" == *"$(echo "$fqdn" | cut -d'.' -f1)"* ]]; then
found_dirs+=("$dir")
fi
fi
done
# Remove duplicates
found_dirs=($(printf "%s\n" "${found_dirs[@]}" | sort -u))
if [ ${#found_dirs[@]} -eq 1 ]; then
app_dir="${found_dirs[0]}"
service_name=$(basename "$app_dir" | sed 's/^patchmon-/patchmon-/')
if [[ "$service_name" != patchmon-* ]]; then
service_name="patchmon-$(basename "$app_dir")"
fi
print_info "Found matching instance at: $app_dir"
print_info "Detected service name: $service_name"
elif [ ${#found_dirs[@]} -gt 1 ]; then
print_error "Multiple possible instances found for $fqdn:"
for dir in "${found_dirs[@]}"; do
echo " - $dir"
done
print_info "Please specify the exact path:"
print_info " $0 update $fqdn <path>"
exit 1
fi
fi
fi
if [ ! -d "$app_dir" ]; then
print_error "Instance for $fqdn not found at $app_dir"
print_info "Available instances:"
for dir in /opt/patchmon-*; do
if [ -d "$dir" ]; then
echo " - $dir"
fi
done
print_info "Use: $0 update $fqdn <custom-path>"
exit 1
fi
# Call the single instance update function
update_single_instance "$fqdn" "$app_dir" "$service_name"
}
# Update single instance (core update logic)
update_single_instance() {
local fqdn=$1
local app_dir=$2
local service_name=$3
# Reset environment for each instance to avoid cross-contamination
unset PATH_MODIFIED
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
print_info "Updating PatchMon instance for $fqdn..."
print_info "🔍 Instance details: FQDN=$fqdn, Path=$app_dir, Service=$service_name"
cd "$app_dir"
# Backup database first
print_info "Creating database backup..."
# Read database credentials from .env file
if [ -f "$app_dir/backend/.env" ]; then
local db_name=$(grep "^DATABASE_URL=" "$app_dir/backend/.env" | cut -d'=' -f2 | sed 's/.*\/\([^?]*\).*/\1/')
local db_user=$(grep "^DATABASE_URL=" "$app_dir/backend/.env" | cut -d'@' -f1 | sed 's/.*:\/\/\([^:]*\).*/\1/')
local db_pass=$(grep "^DATABASE_URL=" "$app_dir/backend/.env" | cut -d'@' -f1 | sed 's/.*:\/\/[^:]*:\([^@]*\).*/\1/')
local db_host=$(grep "^DATABASE_URL=" "$app_dir/backend/.env" | cut -d'@' -f2 | cut -d'/' -f1 | cut -d':' -f1)
local db_port=$(grep "^DATABASE_URL=" "$app_dir/backend/.env" | cut -d'@' -f2 | cut -d'/' -f1 | cut -d':' -f2)
# Set defaults if not found
db_host=${db_host:-localhost}
db_port=${db_port:-5432}
if [ -n "$db_name" ] && [ -n "$db_user" ] && [ -n "$db_pass" ]; then
PGPASSWORD="$db_pass" pg_dump -h "$db_host" -p "$db_port" -U "$db_user" "$db_name" > backup_$(date +%Y%m%d_%H%M%S).sql
print_status "Database backup created"
else
print_warning "Could not read database credentials from .env file, skipping backup"
fi
else
print_warning ".env file not found, skipping database backup"
fi
# Update code
print_info "Pulling latest code..."
# Fix git ownership issue if it exists
git config --global --add safe.directory "$app_dir" 2>/dev/null || true
# Check if we have the correct remote origin
current_origin=$(git remote get-url origin 2>/dev/null || echo "")
if [ -n "$current_origin" ]; then
print_info "Current origin: $current_origin"
# If current origin doesn't match default repo, offer to update it
local expected_repo="$DEFAULT_GITHUB_REPO"
if [ "$PUBLIC_REPO_MODE" = "true" ]; then
# Convert SSH URL to HTTPS for public repo mode
expected_repo=$(echo "$DEFAULT_GITHUB_REPO" | sed 's|git@github.com:|https://github.com/|')
fi
if [ "$current_origin" != "$expected_repo" ]; then
print_warning "Current origin differs from expected repository"
print_info "Current: $current_origin"
print_info "Expected: $expected_repo"
read -p "Update origin to expected repository? (y/N): " update_origin
if [[ "$update_origin" =~ ^[Yy]$ ]]; then
git remote set-url origin "$expected_repo"
print_status "Updated origin to: $expected_repo"
fi
fi
else
print_warning "No origin found, setting to expected repository"
local expected_repo="$DEFAULT_GITHUB_REPO"
if [ "$PUBLIC_REPO_MODE" = "true" ]; then
# Convert SSH URL to HTTPS for public repo mode
expected_repo=$(echo "$DEFAULT_GITHUB_REPO" | sed 's|git@github.com:|https://github.com/|')
fi
git remote add origin "$expected_repo" 2>/dev/null || git remote set-url origin "$expected_repo"
print_status "Set origin to: $expected_repo"
fi
# Handle potential conflicts with local changes (especially patchmon-agent.sh)
if [ "$PUBLIC_REPO_MODE" = "true" ]; then
# Public repo mode - ensure we're using HTTPS origin
current_origin=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$current_origin" == git@github.com:* ]]; then
print_info "Converting SSH origin to HTTPS for public repo mode"
HTTPS_ORIGIN=$(echo "$current_origin" | sed 's|git@github.com:|https://github.com/|')
git remote set-url origin "$HTTPS_ORIGIN"
print_status "Updated origin to HTTPS: $HTTPS_ORIGIN"
fi
fi
if ! git pull origin "$DEPLOYMENT_BRANCH"; then
print_warning "Git pull failed, likely due to local changes. Attempting to resolve..."
# Check if the conflict is with patchmon-agent.sh (common case)
if git status --porcelain | grep -q "agents/patchmon-agent.sh"; then
print_info "Detected conflict with patchmon-agent.sh (expected due to custom configuration)"
print_info "Stashing local changes and retrying pull..."
# Stash local changes
git stash push -m "Auto-stash before update $(date)"
# Try pull again
if git pull origin "$DEPLOYMENT_BRANCH"; then
print_status "Successfully pulled latest changes"
print_info "Local changes have been stashed and can be recovered if needed"
print_info "Note: patchmon-agent.sh will be regenerated with current instance configuration"
else
print_error "Git pull still failed after stashing. Manual intervention required."
print_info "You may need to resolve conflicts manually in: $app_dir"
return 1
fi
else
# Other conflicts - let user know
print_error "Git pull failed with conflicts. Showing status:"
git status
print_info "Please resolve conflicts manually in: $app_dir"
print_info "Common solutions:"
print_info " 1. git stash (to save local changes)"
print_info " 2. git reset --hard origin/main (to discard local changes)"
print_info " 3. Manually resolve conflicts and commit"
return 1
fi
else
print_status "Successfully pulled latest changes"
fi
# Note: Cleanup and installation will be handled by the 3-step process below
print_info "Proceeding to 3-step dependency management process..."
# STEP 1: CLEANUP - Remove all node_modules and lock files
print_info "🧹 STEP 1: Cleaning up all dependencies..."
# Fix ownership before cleanup
print_info "Fixing directory ownership for cleanup..."
chown -R root:root "$app_dir" 2>/dev/null || print_warning "Could not change ownership"
# Handle workspace/monorepo structure - node_modules is in parent directory
print_info "Detected workspace structure - cleaning shared dependencies"
print_info "Root directory: $app_dir"
# Clean the parent directory (where the actual node_modules is)
print_info "Removing shared node_modules from: $app_dir"
rm -rf "$app_dir/node_modules" 2>/dev/null || true
rm -rf "$app_dir/package-lock.json" 2>/dev/null || true
rm -rf "$app_dir/.npm" 2>/dev/null || true
# Clean all subdirectory lock files
rm -rf "$app_dir/frontend/package-lock.json" 2>/dev/null || true
rm -rf "$app_dir/frontend/.npm" 2>/dev/null || true
rm -rf "$app_dir/frontend/dist" 2>/dev/null || true # Remove old build
rm -rf "$app_dir/backend/package-lock.json" 2>/dev/null || true
rm -rf "$app_dir/backend/.npm" 2>/dev/null || true
# Clear npm cache
npm cache clean --force 2>/dev/null || true
# Verify cleanup worked
if [ -d "$app_dir/node_modules" ]; then
print_warning "node_modules still exists, forcing removal..."
chmod -R 777 "$app_dir/node_modules" 2>/dev/null || true
rm -rf "$app_dir/node_modules"
fi
print_info "✅ Cleanup completed - all dependencies removed"
# STEP 2: INSTALL - Fresh install of all workspace dependencies
print_info "📦 STEP 2: Installing fresh dependencies..."
# Ensure we're in the correct directory
cd "$app_dir" # Go to root directory for workspace install
print_info "Installing from workspace root: $(pwd)"
print_info "🔍 Expected directory: $app_dir"
# Verify we're in the right place
if [ "$(pwd)" != "$app_dir" ]; then
print_error "❌ Directory mismatch! Current: $(pwd), Expected: $app_dir"
exit 1
fi
# First install all dependencies (npm install should include devDependencies by default)
print_info "Running: npm install --no-cache --force"
if npm install --no-cache --force; then
print_info "✅ Initial npm install completed"
# Debug: Check what was actually installed
print_info "🔍 Checking what was installed..."
print_info "Contents of .bin directory:"
ls -la node_modules/.bin/ | head -10 || true
# Vite may be installed in the frontend workspace, not the root
if [ -f "$app_dir/frontend/node_modules/.bin/vite" ]; then
print_info "✅ Vite available in frontend workspace: $app_dir/frontend/node_modules/.bin/vite"
else
print_warning "⚠️ Vite not in root .bin (expected in frontend workspace). We'll rely on npm run build which cds into frontend."
fi
# Note: Removed automatic TailwindCSS dependency installation to avoid version conflicts
# The package.json should contain all required dependencies with correct versions
print_info "📦 Using dependencies from package.json (avoiding version conflicts)"
else
print_error "❌ Root npm install failed!"
exit 1
fi
print_info "✅ Dependencies installation completed"
# Fix ownership back to www-data after install
print_info "Restoring www-data ownership..."
chown -R www-data:www-data "$app_dir" 2>/dev/null || print_warning "Could not restore www-data ownership"
# Generate Prisma client after fresh install
print_info "Generating Prisma client..."
print_info "📂 Moving to backend directory: $app_dir/backend"
cd "$app_dir/backend"
pwd # Show current directory
# Verify we're in the backend directory
if [ ! -f "prisma/schema.prisma" ]; then
print_error "❌ Prisma schema not found! Current directory: $(pwd)"
print_error "Expected to be in: $app_dir/backend"
exit 1
fi
if npx prisma generate; then
print_info "✅ Prisma client generated successfully"
else
print_warning "⚠️ Prisma generate failed, trying alternative method..."
node ../node_modules/.bin/prisma generate || print_error "Prisma generate failed completely"
fi
# Run migrations
print_info "Running database migrations..."
cd "$app_dir/backend"
# Set environment variables for Prisma
if [ -f ".env" ]; then
export $(grep -v '^#' .env | xargs)
# Debug: Check environment variables
print_info "Environment variables loaded from .env"
print_info "DATABASE_URL: ${DATABASE_URL:0:50}..." # Show first 50 chars only for security
print_info "DB_HOST: $DB_HOST"
print_info "DB_PORT: $DB_PORT"
print_info "DB_NAME: $DB_NAME"
print_info "DB_USER: $DB_USER"
# Debug: Check what's available
print_info "Checking for Prisma CLI..."
print_info "npx available: $(command -v npx >/dev/null 2>&1 && echo 'yes' || echo 'no')"
print_info "Local prisma binary: $([ -f "./node_modules/.bin/prisma" ] && echo 'yes' || echo 'no')"
print_info "Parent prisma binary: $([ -f "../node_modules/.bin/prisma" ] && echo 'yes' || echo 'no')"
# Fix Prisma engine permissions first
print_info "Fixing Prisma engine permissions..."
find "$app_dir/node_modules/@prisma/engines" -name "schema-engine-*" -exec chmod +x {} \; 2>/dev/null || true
find "$app_dir/node_modules/@prisma/engines" -name "query-engine-*" -exec chmod +x {} \; 2>/dev/null || true
find "$app_dir/node_modules/@prisma/engines" -name "migration-engine-*" -exec chmod +x {} \; 2>/dev/null || true
find "$app_dir/node_modules/@prisma/engines" -name "introspection-engine-*" -exec chmod +x {} \; 2>/dev/null || true
find "$app_dir/node_modules/@prisma/engines" -name "prisma-fmt-*" -exec chmod +x {} \; 2>/dev/null || true
# Try different ways to run Prisma migrations
# Skip npx if we know parent binary exists (npx has permission issues)
if [ -f "$app_dir/node_modules/.bin/prisma" ]; then
print_info "Using parent prisma binary with node"
chmod +x "$app_dir/node_modules/.bin/prisma"
node "$app_dir/node_modules/.bin/prisma" migrate deploy
elif [ -f "./node_modules/.bin/prisma" ]; then
print_info "Using local prisma binary with node"
chmod +x ./node_modules/.bin/prisma
node ./node_modules/.bin/prisma migrate deploy
elif [ -f "$app_dir/node_modules/prisma/build/index.js" ]; then
print_info "Using parent prisma build index directly"
node "$app_dir/node_modules/prisma/build/index.js" migrate deploy
elif [ -f "./node_modules/prisma/build/index.js" ]; then
print_info "Using prisma build index directly"
node ./node_modules/prisma/build/index.js migrate deploy
elif command -v npx >/dev/null 2>&1; then
print_info "Using npx prisma migrate deploy (fallback)"
npx prisma migrate deploy
else
print_error "Prisma CLI not found. Trying to install..."
npm install prisma @prisma/client
npx prisma migrate deploy
fi
print_status "Database migrations completed"
else
print_error ".env file not found, cannot run migrations"
exit 1
fi
# Stop service before rebuilding frontend
print_info "Stopping service before frontend rebuild..."
systemctl stop "$service_name" 2>/dev/null || print_warning "Service was not running or failed to stop"
# Wait a moment for service to fully stop
print_info "Waiting for service to fully stop..."
sleep 2
# STEP 3: BUILD - Build the frontend from ROOT directory (PERMANENT FIX)
print_info "🏗️ STEP 3: Building frontend from ROOT directory..."
# CRITICAL: Always build from ROOT where node_modules actually is
cd "$app_dir"
print_info "Building from ROOT directory: $(pwd)"
print_info "This is where node_modules is located: $app_dir/node_modules"
# Check if root package.json exists
if [ ! -f "package.json" ]; then
print_error "❌ package.json not found in root directory!"
return 1
fi
# Check if frontend package.json exists
if [ ! -f "frontend/package.json" ]; then
print_error "❌ frontend/package.json not found!"
return 1
fi
# Verify workspace node_modules exists
if [ ! -d "node_modules" ]; then
print_error "❌ node_modules not found in root directory: $(pwd)"
print_error "This indicates the workspace install failed!"
return 1
fi
# Vite may be installed within frontend workspace; do not fail if not in root
if [ -f "node_modules/.bin/vite" ]; then
print_info "✅ Vite binary found in root: $(pwd)/node_modules/.bin/vite"
elif [ -f "frontend/node_modules/.bin/vite" ]; then
print_info "✅ Vite binary found in frontend workspace: $(pwd)/frontend/node_modules/.bin/vite"
else
print_warning "⚠️ Vite not found in root .bin; relying on npm run build which cds into frontend"
fi
# Fix permissions for workspace binaries
if [ -d "node_modules/.bin" ]; then
print_info "Fixing workspace binary permissions..."
chmod -R 755 node_modules/.bin/ 2>/dev/null || true
chmod +x node_modules/.bin/* 2>/dev/null || true
fi
# Single-method build from ROOT (no fallbacks)
print_info "🚀 Starting build process from ROOT directory..."
# Reset PATH per instance to avoid bleed-over when updating multiple instances
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH="$(pwd)/node_modules/.bin:$PATH"
print_info "Added root node_modules to PATH: $(pwd)/node_modules/.bin"
# Do not remove frontend/node_modules; workspaces expect it for local bin links
# Do not mutate package.json scripts arbitrarily
# Ensure root build script runs backend then frontend
print_info "Normalizing root build script to run backend then frontend..."
npm pkg set "scripts.build=npm run build:backend && npm run build:frontend" >/dev/null 2>&1 || true
print_info "Using original build scripts (build:backend && build:frontend)..."
print_info "🧩 Installing dependencies at root..."
# Ensure devDependencies are installed (vite et al.) regardless of environment
PREV_NPM_CONFIG_PRODUCTION="${NPM_CONFIG_PRODUCTION-}"
PREV_NODE_ENV="${NODE_ENV-}"
export NPM_CONFIG_PRODUCTION=false
unset npm_config_production
export NODE_ENV=development
npm install 2>&1 || { print_error "❌ npm install failed"; \
[ -n "$PREV_NPM_CONFIG_PRODUCTION" ] && export NPM_CONFIG_PRODUCTION="$PREV_NPM_CONFIG_PRODUCTION" || unset NPM_CONFIG_PRODUCTION; \
[ -n "$PREV_NODE_ENV" ] && export NODE_ENV="$PREV_NODE_ENV" || unset NODE_ENV; \
return 1; }
print_info "🏗️ Building project from root via npm run build..."
npm run build 2>&1 || { print_error "❌ npm run build failed"; \
[ -n "$PREV_NPM_CONFIG_PRODUCTION" ] && export NPM_CONFIG_PRODUCTION="$PREV_NPM_CONFIG_PRODUCTION" || unset NPM_CONFIG_PRODUCTION; \
[ -n "$PREV_NODE_ENV" ] && export NODE_ENV="$PREV_NODE_ENV" || unset NODE_ENV; \
return 1; }
# Restore previous env after successful build
if [ -n "$PREV_NPM_CONFIG_PRODUCTION" ]; then export NPM_CONFIG_PRODUCTION="$PREV_NPM_CONFIG_PRODUCTION"; else unset NPM_CONFIG_PRODUCTION; fi
if [ -n "$PREV_NODE_ENV" ]; then export NODE_ENV="$PREV_NODE_ENV"; else unset NODE_ENV; fi
if [ ! -d "$app_dir/frontend/dist" ]; then
print_error "Build completed but $app_dir/frontend/dist directory not found!"
return 1
fi
print_status "Frontend build completed successfully"
# Copy frontend server.js if nginx is disabled
if [ "$SETUP_NGINX" = "false" ]; then
echo -e "${BLUE}📁 Copying frontend server.js...${NC}"
cp $SCRIPT_DIR/frontend/server.js $app_dir/frontend/
print_status "Frontend server.js copied"
fi
# Prepare environment for agent version creation
export APP_DIR="$app_dir"
if [ -f "$app_dir/backend/.env" ]; then
db_url=$(grep "^DATABASE_URL=" "$app_dir/backend/.env" | cut -d'=' -f2-)
if [ -n "$db_url" ]; then
export DB_USER=$(echo "$db_url" | sed -E 's|.*://([^:]*):.*|\1|')
export DB_PASS=$(echo "$db_url" | sed -E 's|.*://[^:]*:([^@]*)@.*|\1|')
export DB_NAME=$(echo "$db_url" | sed -E 's|.*/([^?]*)\?.*|\1|')
fi
fi
# Create/update agent version for the new version
print_info "🤖 Creating/updating agent version..."
create_agent_version
# Restart services (at the very end of the update)
print_info "Restarting backend service..."
if systemctl restart "$service_name"; then
print_status "Backend service $service_name restarted successfully"
# Wait a moment and verify service is running
sleep 3
if systemctl is-active --quiet "$service_name"; then
print_status "Service $service_name is running and healthy"
else
print_warning "Service $service_name may not have started properly"
print_info "Check status with: systemctl status $service_name"
fi
else
print_error "Failed to restart backend service $service_name"
print_info "Check status with: systemctl status $service_name"
print_info "Check logs with: journalctl -u $service_name -n 20"
exit 1
fi
# Restart frontend service if nginx is disabled
if [ "$SETUP_NGINX" = "false" ]; then
local frontend_service_name="${service_name}-frontend"
print_info "Restarting frontend service..."
if systemctl restart "$frontend_service_name"; then
print_status "Frontend service $frontend_service_name restarted successfully"
# Wait a moment and verify service is running
sleep 3
if systemctl is-active --quiet "$frontend_service_name"; then
print_status "Frontend service $frontend_service_name is running and healthy"
else
print_warning "Frontend service $frontend_service_name may not have started properly"
print_info "Check status with: systemctl status $frontend_service_name"
fi
else
print_error "Failed to restart frontend service $frontend_service_name"
print_info "Check status with: systemctl status $frontend_service_name"
print_info "Check logs with: journalctl -u $frontend_service_name -n 20"
fi
fi
print_status "Instance updated successfully"
}
# Delete instance
delete_instance() {
local fqdn=$1
if [ $# -ne 1 ]; then
print_error "Usage: $0 delete <fqdn>"
exit 1
fi
local db_safe_name=$(echo $fqdn | tr '[:upper:]' '[:lower:]' | tr '.-' '__')
local db_name="patchmon_${db_safe_name}"
local db_user="patchmon_${db_safe_name}_user"
local app_dir="/opt/patchmon-$fqdn"
local service_name="patchmon-$fqdn"
if [ ! -d "$app_dir" ]; then
print_error "Instance for $fqdn not found at $app_dir"
exit 1
fi
print_warning "This will permanently delete the PatchMon instance for $fqdn"
read -p "Are you sure? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
print_info "Deletion cancelled"
exit 0
fi
print_info "Deleting PatchMon instance for $fqdn..."
# Stop and disable service
systemctl stop "$service_name" || true
systemctl disable "$service_name" || true
# Remove service file
rm -f "/etc/systemd/system/$service_name.service"
systemctl daemon-reload
# Remove application directory
rm -rf "$app_dir"
# Remove database
sudo -u postgres psql -c "DROP DATABASE IF EXISTS $db_name;" || true
sudo -u postgres psql -c "DROP USER IF EXISTS $db_user;" || true
# Remove Nginx configuration
rm -f "/etc/nginx/sites-enabled/$fqdn"
rm -f "/etc/nginx/sites-available/$fqdn"
nginx -s reload
# Remove SSL certificate
certbot delete --cert-name "$fqdn" --non-interactive || true
print_status "Instance deleted successfully"
}
# List all instances
list_instances() {
print_info "Listing all PatchMon instances..."
echo ""
# Header
printf "${BLUE}%-30s %-10s %-15s %-10s %-12s %-15s${NC}\n" "FQDN" "Status" "Backend Port" "SSL" "Version" "Service Name"
printf "${BLUE}%-30s %-10s %-15s %-10s %-12s %-15s${NC}\n" "$(printf '%*s' 30 | tr ' ' '-')" "$(printf '%*s' 10 | tr ' ' '-')" "$(printf '%*s' 15 | tr ' ' '-')" "$(printf '%*s' 10 | tr ' ' '-')" "$(printf '%*s' 12 | tr ' ' '-')" "$(printf '%*s' 15 | tr ' ' '-')"
# Collect instance data
declare -A instances
# Parse systemd services
for service in /etc/systemd/system/patchmon-*.service; do
if [ -f "$service" ]; then
service_name=$(basename "$service" .service)
fqdn=$(grep "Description" "$service" | sed 's/.*for //' | head -1)
status=$(systemctl is-active "$service_name" 2>/dev/null || echo "inactive")
port=$(grep "Environment=PORT=" "$service" | cut -d'=' -f3 | head -1)
# Check if SSL is enabled by looking for SSL certificate
ssl="HTTP"
if [ -d "/etc/letsencrypt/live/$fqdn" ] || grep -q "ssl_certificate" "/etc/nginx/sites-available/$fqdn" 2>/dev/null; then
ssl="HTTPS"
fi
# Handle missing port (try alternative method for older services)
if [ -z "$port" ]; then
port=$(grep "PORT=" "$service" | cut -d'=' -f2 | head -1)
fi
if [ -z "$port" ]; then
port="N/A"
fi
# Color code status (using printf for proper color rendering)
if [ "$status" = "active" ]; then
status_display="$(printf "${GREEN}%-10s${NC}" "$status")"
else
status_display="$(printf "${RED}%-10s${NC}" "$status")"
fi
# Color code SSL (using printf for proper color rendering)
if [ "$ssl" = "HTTPS" ]; then
ssl_display="$(printf "${GREEN}%-10s${NC}" "$ssl")"
else
ssl_display="$(printf "${YELLOW}%-10s${NC}" "$ssl")"
fi
# Get version information using unified function
app_dir="/opt/patchmon-$fqdn"
# Try to find the app directory (handle custom paths)
if [ ! -d "$app_dir" ]; then
# Look for directories that might match this instance
for dir in /opt/patchmon-*; do
if [ -d "$dir" ] && [ -f "$dir/backend/.env" ]; then
if grep -q "$fqdn" "$dir/backend/.env" 2>/dev/null; then
app_dir="$dir"
break
fi
fi
done
fi
# Use unified version detection
version=$(get_instance_version "$app_dir" "$fqdn" "$status" "$port")
printf "%-30s %s %-15s %s %-12s %-15s\n" "$fqdn" "$status_display" "$port" "$ssl_display" "$version" "$service_name"
fi
done
echo ""
print_info "Management Commands:"
echo " ./manage-patchmon.sh status <fqdn> - Show detailed status"
echo " ./manage-patchmon.sh update <fqdn> - Update instance"
echo " ./manage-patchmon.sh delete <fqdn> - Delete instance"
echo ""
print_info "Instance Management:"
echo " cd /opt/patchmon-<fqdn>/ && ./manage.sh status - Local management"
echo " systemctl status patchmon-<fqdn> - Service status"
echo " journalctl -u patchmon-<fqdn> -f - Live logs"
}
# Show instance status
show_status() {
local fqdn=$1
if [ $# -ne 1 ]; then
print_error "Usage: $0 status <fqdn>"
exit 1
fi
local app_dir="/opt/patchmon-$fqdn"
local service_name="patchmon-$fqdn"
if [ ! -d "$app_dir" ]; then
print_error "Instance for $fqdn not found"
exit 1
fi
print_info "Status for $fqdn:"
echo -e "${BLUE}Service Status:${NC}"
systemctl status "$service_name" --no-pager
echo -e "\n${BLUE}Recent Logs:${NC}"
journalctl -u "$service_name" --no-pager -n 20
echo -e "\n${BLUE}Disk Usage:${NC}"
du -sh "$app_dir"
echo -e "\n${BLUE}Database Size:${NC}"
local db_safe_name=$(echo $fqdn | tr '[:upper:]' '[:lower:]' | tr '.-' '__')
local db_name="patchmon_${db_safe_name}"
sudo -u postgres psql -c "SELECT pg_size_pretty(pg_database_size('$db_name'));"
}
# Show help
show_help() {
echo -e "${BLUE}PatchMon Unified Management System${NC}"
echo ""
echo "Usage: $0 <command> [options]"
echo ""
echo "Commands:"
echo " deploy [public-repo]"
echo " Deploy a new PatchMon instance with interactive configuration"
echo " - Interactive FQDN setup with validation"
echo " - Branch selection (main/dev)"
echo " - Optional Let's Encrypt SSL certificate setup"
echo " - Optional timezone configuration"
echo " - Uses default repository: $DEFAULT_GITHUB_REPO"
echo " - Auto-generates all credentials"
echo " - Detects existing components (PostgreSQL, Nginx, Node.js)"
echo " - Assigns unique ports automatically"
echo " - Creates isolated Python virtual environments"
echo " - public-repo: Use HTTPS clone instead of SSH (simplified for public repos)"
echo ""
echo " update [public-repo] [fqdn] [custom-path]"
echo " Update existing instance(s) with latest code"
echo " - Interactive mode: Shows numbered list of instances to select from"
echo " - Direct mode: Update specific instance by FQDN"
echo " - Supports multiple instance selection (1 3 5, 1-3, all)"
echo " - Automatically detects instance location if standard path not found"
echo " - Optionally specify custom instance path for direct mode"
echo " - public-repo: Use HTTPS instead of SSH for git operations"
echo ""
echo " delete <fqdn>"
echo " Delete an instance completely"
echo ""
echo " list"
echo " List all instances with ports and status"
echo ""
echo " status <fqdn>"
echo " Show detailed status of a specific instance"
echo ""
echo "Examples:"
echo " $0 deploy # Interactive deployment"
echo " $0 deploy public-repo # Public repo deployment (HTTPS)"
echo " $0 update # Interactive mode"
echo " $0 update public-repo # Interactive mode with HTTPS"
echo " $0 update customer1.patchmon.com # Update specific instance"
echo " $0 update public-repo customer1.patchmon.com # Update with HTTPS"
echo " $0 update pmon.manage.9.technology /opt/patchmon-pmon_patchmon_db # Custom path"
echo " $0 list"
echo " $0 status customer1.patchmon.com"
echo ""
echo "Features:"
echo " ✅ Interactive deployment configuration"
echo " ✅ FQDN validation and setup"
echo " ✅ Optional nginx reverse proxy setup"
echo " ✅ Optional Let's Encrypt SSL setup (when nginx enabled)"
echo " ✅ Optional timezone configuration"
echo " ✅ Smart component detection (skips already installed)"
echo " ✅ Automatic port allocation (prevents conflicts)"
echo " ✅ Isolated Node.js environments per instance"
echo " ✅ FQDN-based database and folder naming"
echo " ✅ Complete instance isolation"
echo " ✅ Automatic credential generation"
}
# Main execution
case $1 in
"deploy")
print_banner
# Check for public-repo option
if [ "$2" = "public-repo" ]; then
PUBLIC_REPO_MODE="true"
print_info "🌐 Public repository mode enabled - using HTTPS clone"
fi
deploy_instance
;;
"update")
print_banner
shift
# Check for public-repo option
if [ "$1" = "public-repo" ]; then
PUBLIC_REPO_MODE="true"
print_info "🌐 Public repository mode enabled for update - using HTTPS"
shift
fi
update_instance "$@"
;;
"delete")
print_banner
delete_instance "$2"
;;
"list")
print_banner
list_instances
;;
"status")
print_banner
show_status "$2"
;;
"help"|"-h"|"--help")
print_banner
show_help
;;
*)
print_banner
print_error "Unknown command: $1"
show_help
exit 1
;;
esac