diff --git a/ct/calibre-web.sh b/ct/calibre-web.sh index 278dd9ff8..00d96e3df 100644 --- a/ct/calibre-web.sh +++ b/ct/calibre-web.sh @@ -35,20 +35,24 @@ function update_script() { msg_ok "Stopped Service" msg_info "Backing up Data" - cp -r /opt/calibre-web/app.db /opt/calibre-web/app.db_backup 2>/dev/null + cp -r /opt/calibre-web/app.db /opt/app.db_backup + cp -r /opt/calibre-web/data /opt/data_backup msg_ok "Backed up Data" - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "calibre-web" "janeczku/calibre-web" "tarball" "latest" "/opt/calibre-web" + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "calibre-web" "janeczku/calibre-web" "prebuild" "latest" "/opt/calibre-web" "calibre-web*.tar.gz" setup_uv msg_info "Installing Dependencies" cd /opt/calibre-web - $STD uv sync --no-dev + $STD uv venv + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir --upgrade pip setuptools wheel + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r requirements.txt msg_ok "Installed Dependencies" msg_info "Restoring Data" - cp /opt/calibre-web/app.db_backup /opt/calibre-web/app.db 2>/dev/null - rm -f /opt/calibre-web/app.db_backup + cp /opt/app.db_backup /opt/calibre-web/app.db 2>/dev/null + cp -r /opt/data_backup /opt/calibre-web/data 2>/dev/null + rm -rf /opt/app.db_backup /opt/data_backup msg_ok "Restored Data" msg_info "Starting Service" diff --git a/ct/cronmaster.sh b/ct/cronmaster.sh deleted file mode 100644 index 7a2e0ac50..000000000 --- a/ct/cronmaster.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: - -APP="CRONMASTER" -var_tags="${var_tags:-}" -var_cpu="${var_cpu:-2}" -var_ram="${var_ram:-4096}" -var_disk="${var_disk:-8}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" -#var_fuse="${var_fuse:-no}" -#var_tun="${var_tun:-no}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - if [[ ! -d /opt/cronmaster ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - msg_info "Updating Debian LXC" - $STD apt update - $STD apt upgrade -y - msg_ok "Updated Debian LXC" - cleanup_lxc - exit -} - -start -build_container -description - -msg_ok "Completed successfully!" -msg_custom "🚀" "${GN}" "${APP} setup has been successfully initialized!" diff --git a/ct/databasus.sh b/ct/databasus.sh deleted file mode 100644 index c0eab526b..000000000 --- a/ct/databasus.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/databasus/databasus - -APP="Databasus" -var_tags="${var_tags:-backup;postgresql;database}" -var_cpu="${var_cpu:-2}" -var_ram="${var_ram:-2048}" -var_disk="${var_disk:-8}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - - if [[ ! -f /opt/databasus/databasus ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - if check_for_gh_release "databasus" "databasus/databasus"; then - msg_info "Stopping Databasus" - $STD systemctl stop databasus - msg_ok "Stopped Databasus" - - msg_info "Backing up Configuration" - cp /opt/databasus/.env /tmp/databasus.env.bak - msg_ok "Backed up Configuration" - - msg_info "Updating Databasus" - fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus" - - cd /opt/databasus/frontend - $STD npm ci - $STD npm run build - - cd /opt/databasus/backend - $STD go mod download - $STD /root/go/bin/swag init -g cmd/main.go -o swagger - $STD env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o databasus ./cmd/main.go - mv /opt/databasus/backend/databasus /opt/databasus/databasus - - cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/ - cp -r /opt/databasus/backend/migrations /opt/databasus/ - chown -R postgres:postgres /opt/databasus - msg_ok "Updated Databasus" - - msg_info "Restoring Configuration" - cp /tmp/databasus.env.bak /opt/databasus/.env - rm -f /tmp/databasus.env.bak - chown postgres:postgres /opt/databasus/.env - msg_ok "Restored Configuration" - - msg_info "Starting Databasus" - $STD systemctl start databasus - msg_ok "Started Databasus" - msg_ok "Updated successfully!" - fi - exit -} - -start -build_container -description - -msg_ok "Completed successfully!\n" -echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}" diff --git a/ct/discourse.sh b/ct/discourse.sh index 044ef5f48..f17086fb3 100644 --- a/ct/discourse.sh +++ b/ct/discourse.sh @@ -43,10 +43,12 @@ function update_script() { msg_ok "Backed up Data" msg_info "Updating Discourse" + PG_VERSION="16" PG_MODULES="pgvector" setup_postgresql cd /opt/discourse git pull origin main $STD bundle install --deployment --without test development $STD yarn install + $STD runuser -u postgres -- psql -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;" $STD bundle exec rails assets:precompile $STD bundle exec rails db:migrate msg_ok "Updated Discourse" @@ -70,6 +72,5 @@ msg_ok "Completed Successfully!\n" echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" echo -e "${INFO}${YW} Access it using the following URL:${CL}" echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}" -echo -e "${INFO}${YW} Default Credentials:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}Username: admin${CL}" -echo -e "${TAB}${GATEWAY}${BGN}Password: Check /opt/discourse/.env${CL}" +echo -e "${INFO}${YW} Admin Setup:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}Create the first account in the web UI (use admin@local to match developer emails)${CL}" diff --git a/ct/gramps-web.sh b/ct/gramps-web.sh index fe3d29ece..a90a80b82 100644 --- a/ct/gramps-web.sh +++ b/ct/gramps-web.sh @@ -93,7 +93,9 @@ function update_script() { msg_ok "Updated Gramps Web Frontend" msg_info "Applying Database Migration" + cd /opt/gramps-web-api GRAMPS_API_CONFIG=/opt/gramps-web/config/config.cfg \ + ALEMBIC_CONFIG=/opt/gramps-web-api/alembic.ini \ GRAMPSHOME=/opt/gramps-web/data/gramps \ GRAMPS_DATABASE_PATH=/opt/gramps-web/data/gramps/grampsdb \ $STD /opt/gramps-web/venv/bin/python3 -m gramps_webapi user migrate diff --git a/frontend/public/json/cronmaster.json b/frontend/public/json/cronmaster.json new file mode 100644 index 000000000..690b7227f --- /dev/null +++ b/frontend/public/json/cronmaster.json @@ -0,0 +1,35 @@ +{ + "name": "CronMaster", + "slug": "cronmaster", + "categories": [ + 1 + ], + "date_created": "2026-02-17", + "type": "pve", + "updateable": true, + "privileged": false, + "interface_port": 3000, + "documentation": "https://github.com/fccview/cronmaster", + "website": "https://github.com/fccview/cronmaster", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/cr-nmaster.webp", + "config_path": "/opt/cronmaster/.env", + "description": "Self-hosted cron job scheduler with web UI, live logs, auth and prebuilt binaries provided upstream.", + "install_methods": [ + { + "type": "default", + "script": "tools/addon/cronmaster.sh", + "resources": { + "cpu": null, + "ram": null, + "hdd": null, + "os": null, + "version": null + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [] +} \ No newline at end of file diff --git a/frontend/public/json/databasus.json b/frontend/public/json/databasus.json deleted file mode 100644 index 766d88925..000000000 --- a/frontend/public/json/databasus.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "Databasus", - "slug": "databasus", - "categories": [ - 7 - ], - "date_created": "2025-01-14", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 80, - "documentation": "https://github.com/databasus/databasus", - "website": "https://github.com/databasus/databasus", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/databasus.webp", - "config_path": "/opt/databasus/.env", - "description": "Free, open source and self-hosted solution for automated PostgreSQL backups. With multiple storage options, notifications, scheduling, and a beautiful web interface for managing database backups across multiple PostgreSQL instances.", - "install_methods": [ - { - "type": "default", - "script": "ct/databasus.sh", - "resources": { - "cpu": 2, - "ram": 2048, - "hdd": 8, - "os": "Debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": "admin@localhost", - "password": "See /root/databasus.creds" - }, - "notes": [ - { - "text": "Supports PostgreSQL versions 12-18 with cloud and self-hosted instances", - "type": "info" - }, - { - "text": "Features: Scheduled backups, multiple storage providers, notifications, encryption", - "type": "info" - } - ] -} diff --git a/frontend/public/json/dependency-check.json b/frontend/public/json/dependency-check.json index 5f9307da1..22f08ae79 100644 --- a/frontend/public/json/dependency-check.json +++ b/frontend/public/json/dependency-check.json @@ -23,7 +23,7 @@ "ram": null, "hdd": null, "os": null, - "version": null + "version": "PVE 8.x / 9.x" } } ], @@ -36,6 +36,10 @@ "text": "Execute within the Proxmox shell", "type": "info" }, + { + "text": "The script supports --install (default), --status and --uninstall for clean lifecycle management.", + "type": "info" + }, { "text": "To wait until a certain host is available, tag the VM or container with `dep_ping_` where `` is the name or IP of the host to ping. The script will wait until the host is reachable before proceeding with the startup.", "type": "info" @@ -45,4 +49,4 @@ "type": "info" } ] -} +} \ No newline at end of file diff --git a/install/calibre-web-install.sh b/install/calibre-web-install.sh index dc201423c..786a2b77c 100644 --- a/install/calibre-web-install.sh +++ b/install/calibre-web-install.sh @@ -15,7 +15,12 @@ update_os msg_info "Installing Dependencies" $STD apt install -y \ + build-essential \ python3 \ + python3-dev \ + libldap2-dev \ + libsasl2-dev \ + libssl-dev \ imagemagick \ libpango-1.0-0 \ libharfbuzz0b \ @@ -27,12 +32,14 @@ msg_info "Installing Calibre (for eBook conversion)" $STD apt install -y calibre msg_ok "Installed Calibre" -fetch_and_deploy_gh_release "calibre-web" "janeczku/calibre-web" "tarball" "latest" "/opt/calibre-web" +fetch_and_deploy_gh_release "calibre-web" "janeczku/calibre-web" "prebuild" "latest" "/opt/calibre-web" "calibre-web*.tar.gz" setup_uv msg_info "Installing Python Dependencies" cd /opt/calibre-web -$STD uv sync --no-dev +$STD uv venv +$STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir --upgrade pip setuptools wheel +$STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r requirements.txt msg_ok "Installed Python Dependencies" msg_info "Creating Service" diff --git a/install/cronmaster-install.sh b/install/cronmaster-install.sh deleted file mode 100644 index 16b10816a..000000000 --- a/install/cronmaster-install.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: Slaviša Arežina (tremor021) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/fccview/cronmaster - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os -setup_hwaccel - -msg_info "Installing dependencies" -$STD apt install -y pciutils -msg_ok "Installed dependencies" - -NODE_VERSION="24" NODE_MODULE="yarn" setup_nodejs - -setup_deb822_repo \ - "docker" \ - "https://download.docker.com/linux/debian/gpg" \ - "https://download.docker.com/linux/debian" \ - "trixie" \ - "stable" -$STD apt install -y docker-ce-cli -fetch_and_deploy_gh_release "cronmaster" "fccview/cronmaster" "tarball" - -msg_info "Setting up CronMaster" -AUTH_PASS="$(openssl rand -base64 18 | cut -c1-13)" -cd /opt/cronmaster -$STD yarn --frozen-lockfile -export NEXT_TELEMETRY_DISABLED=1 -$STD yarn build -cat </opt/cronmaster/.env -NODE_ENV=production -APP_URL= -LOCALE= -HOME= -AUTH_PASSWORD=${AUTH_PASS} -PORT=3000 -HOSTNAME="0.0.0.0" -NEXT_TELEMETRY_DISABLED=1 -EOF -{ - echo "CronMaster Credentials:" - echo "" - echo "Password: $AUTH_PASS" -}>>~/cronmaster.creds -msg_ok "Setup CronMaster" - -msg_info "Creating Service" -cat </etc/systemd/system/cronmaster.service -[Unit] -Description=CronMaster Service -After=network.target - -[Service] -EnvironmentFile=/opt/cronmaster/.env -WorkingDirectory=/opt/cronmaster -ExecStart=/usr/bin/yarn start -Restart=always - -[Install] -WantedBy=multi-user.target -EOF -systemctl start --now -q cronmaster -msg_info "Created Service" - -motd_ssh -customize -cleanup_lxc diff --git a/install/databasus-install.sh b/install/databasus-install.sh deleted file mode 100644 index b6001ab2a..000000000 --- a/install/databasus-install.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/databasus/databasus - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -msg_info "Installing Dependencies" -$STD apt install -y \ - nginx \ - valkey -msg_ok "Installed Dependencies" - -PG_VERSION="17" setup_postgresql -setup_go -NODE_VERSION="24" setup_nodejs - -fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus" - -msg_info "Building Databasus (Patience)" -cd /opt/databasus/frontend -$STD npm ci -$STD npm run build -cd /opt/databasus/backend -$STD go mod tidy -$STD go mod download -$STD go install github.com/swaggo/swag/cmd/swag@latest -$STD /root/go/bin/swag init -g cmd/main.go -o swagger -$STD env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o databasus ./cmd/main.go -mv /opt/databasus/backend/databasus /opt/databasus/databasus -mkdir -p /databasus-data/{pgdata,temp,backups,data,logs} -mkdir -p /opt/databasus/ui/build -mkdir -p /opt/databasus/migrations -cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/ -cp -r /opt/databasus/backend/migrations/* /opt/databasus/migrations/ -chown -R postgres:postgres /databasus-data -msg_ok "Built Databasus" - -msg_info "Configuring Databasus" -JWT_SECRET=$(openssl rand -hex 32) -ENCRYPTION_KEY=$(openssl rand -hex 32) -# Create PostgreSQL version symlinks for compatibility -for v in 12 13 14 15 16 18; do - ln -sf /usr/lib/postgresql/17 /usr/lib/postgresql/$v -done -# Install goose for migrations -$STD go install github.com/pressly/goose/v3/cmd/goose@latest -ln -sf /root/go/bin/goose /usr/local/bin/goose -cat </opt/databasus/.env -# Environment -ENV_MODE=production - -# Server -SERVER_PORT=4005 -SERVER_HOST=0.0.0.0 - -# Database -DATABASE_DSN=host=localhost user=postgres password=postgres dbname=databasus port=5432 sslmode=disable -DATABASE_URL=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable - -# Migrations -GOOSE_DRIVER=postgres -GOOSE_DBSTRING=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable -GOOSE_MIGRATION_DIR=/opt/databasus/migrations - -# Valkey (Redis-compatible cache) -VALKEY_HOST=localhost -VALKEY_PORT=6379 - -# Security -JWT_SECRET=${JWT_SECRET} -ENCRYPTION_KEY=${ENCRYPTION_KEY} - -# Paths -DATA_DIR=/databasus-data/data -BACKUP_DIR=/databasus-data/backups -LOG_DIR=/databasus-data/logs -EOF -chown postgres:postgres /opt/databasus/.env -chmod 600 /opt/databasus/.env -msg_ok "Configured Databasus" - -msg_info "Configuring Valkey" -cat >/etc/valkey/valkey.conf </dev/null || true -$STD sudo -u postgres psql -c "ALTER USER postgres WITH SUPERUSER CREATEROLE CREATEDB;" 2>/dev/null || true -msg_ok "Created Database" - -msg_info "Creating Databasus Service" -cat </etc/systemd/system/databasus.service -[Unit] -Description=Databasus - Database Backup Management -After=network.target postgresql.service valkey.service -Requires=postgresql.service valkey.service - -[Service] -Type=simple -WorkingDirectory=/opt/databasus -EnvironmentFile=/opt/databasus/.env -ExecStart=/opt/databasus/databasus -Restart=always -RestartSec=5 -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target -EOF -$STD systemctl daemon-reload -$STD systemctl enable -q --now databasus -msg_ok "Created Databasus Service" - -msg_info "Configuring Nginx" -cat </etc/nginx/sites-available/databasus -server { - listen 80; - server_name _; - - location / { - proxy_pass http://127.0.0.1:4005; - 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_buffering off; - proxy_read_timeout 86400s; - proxy_send_timeout 86400s; - } -} -EOF -ln -sf /etc/nginx/sites-available/databasus /etc/nginx/sites-enabled/databasus -rm -f /etc/nginx/sites-enabled/default -$STD nginx -t -$STD systemctl enable -q --now nginx -$STD systemctl reload nginx -msg_ok "Configured Nginx" - -motd_ssh -customize -cleanup_lxc diff --git a/install/discourse-install.sh b/install/discourse-install.sh index aa3e575d6..47a5cb0a4 100644 --- a/install/discourse-install.sh +++ b/install/discourse-install.sh @@ -24,30 +24,27 @@ $STD apt install -y \ git \ imagemagick \ gsfonts \ + brotli \ nginx \ redis-server msg_ok "Installed Dependencies" -PG_VERSION="16" setup_postgresql +PG_VERSION="16" PG_MODULES="pgvector" setup_postgresql NODE_VERSION="22" setup_nodejs RUBY_VERSION="3.4.4" setup_ruby msg_info "Configuring PostgreSQL for Discourse" DISCOURSE_DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13) -# Configure pg_hba.conf for md5 authentication PG_HBA="/etc/postgresql/16/main/pg_hba.conf" sed -i 's/^local\s\+all\s\+all\s\+peer$/local all all md5/' "$PG_HBA" $STD systemctl restart postgresql -# Create user + database explicitly for reliable bootstrap PG_DB_NAME="discourse" PG_DB_USER="discourse" PG_DB_PASS="$DISCOURSE_DB_PASS" setup_postgresql_db msg_ok "Configured PostgreSQL for Discourse" msg_info "Configuring Discourse" DISCOURSE_SECRET_KEY=$(openssl rand -hex 64) - -git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse +$STD git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse cd /opt/discourse - cat </opt/discourse/.env RAILS_ENV=production RAILS_LOG_TO_STDOUT=true @@ -93,6 +90,7 @@ export RAILS_ENV=production set -a source /opt/discourse/.env set +a +$STD runuser -u postgres -- psql -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;" $STD bundle exec rails db:migrate msg_ok "Set Up Database" @@ -107,16 +105,8 @@ set +a $STD bundle exec rails assets:precompile msg_ok "Built Discourse Assets" -msg_info "Creating Discourse Admin User" -cd /opt/discourse -export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH" -eval "$(rbenv init - bash)" 2>/dev/null || true -export RAILS_ENV=production -set -a -source /opt/discourse/.env -set +a -$STD bundle exec rails runner "User.create!(email: 'admin@local', username: 'admin', password: '${DISCOURSE_DB_PASS}', admin: true)" || true -msg_ok "Created Discourse Admin User" +msg_info "Preparing Admin Onboarding" +msg_ok "Automatic admin bootstrap skipped (use first signup in UI with admin@local)" msg_info "Creating Service" cat </etc/systemd/system/discourse.service diff --git a/install/gramps-web-install.sh b/install/gramps-web-install.sh index dde886341..5a64cf5ac 100644 --- a/install/gramps-web-install.sh +++ b/install/gramps-web-install.sh @@ -94,7 +94,9 @@ corepack enable $STD npm install $STD npm run build +cd /opt/gramps-web-api GRAMPS_API_CONFIG=/opt/gramps-web/config/config.cfg \ + ALEMBIC_CONFIG=/opt/gramps-web-api/alembic.ini \ GRAMPSHOME=/opt/gramps-web/data/gramps \ GRAMPS_DATABASE_PATH=/opt/gramps-web/data/gramps/grampsdb \ $STD /opt/gramps-web/venv/bin/python3 -m gramps_webapi user migrate diff --git a/misc/api.func b/misc/api.func index 4a554ecfc..dad7a2ebe 100644 --- a/misc/api.func +++ b/misc/api.func @@ -91,7 +91,7 @@ detect_repo_source() { community-scripts/ProxmoxVED) REPO_SOURCE="ProxmoxVED" ;; "") # No URL detected — use hardcoded fallback - # CI sed transforms this on promotion: ProxmoxVED → ProxmoxVE + # This value must match the repo: ProxmoxVE for production, ProxmoxVED for dev REPO_SOURCE="ProxmoxVED" ;; *) @@ -135,24 +135,50 @@ explain_exit_code() { # --- Generic / Shell --- 1) echo "General error / Operation not permitted" ;; 2) echo "Misuse of shell builtins (e.g. syntax error)" ;; + 10) echo "Docker / privileged mode required (unsupported environment)" ;; # --- curl / wget errors (commonly seen in downloads) --- + 4) echo "curl: Feature not supported or protocol error" ;; + 5) echo "curl: Could not resolve proxy" ;; 6) echo "curl: DNS resolution failed (could not resolve host)" ;; 7) echo "curl: Failed to connect (network unreachable / host down)" ;; + 8) echo "curl: FTP server reply error" ;; 22) echo "curl: HTTP error returned (404, 429, 500+)" ;; + 23) echo "curl: Write error (disk full or permissions)" ;; + 25) echo "curl: Upload failed" ;; 28) echo "curl: Operation timeout (network slow or server not responding)" ;; + 30) echo "curl: FTP port command failed" ;; 35) echo "curl: SSL/TLS handshake failed (certificate error)" ;; + 56) echo "curl: Receive error (connection reset by peer)" ;; + 75) echo "Temporary failure (retry later)" ;; + 78) echo "curl: Remote file not found (404 on FTP/file)" ;; # --- Package manager / APT / DPKG --- 100) echo "APT: Package manager error (broken packages / dependency problems)" ;; 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;; 102) echo "APT: Lock held by another process (dpkg/apt still running)" ;; + # --- BSD sysexits.h (64-78) --- + 64) echo "Usage error (wrong arguments)" ;; + 65) echo "Data format error (bad input data)" ;; + 66) echo "Input file not found (cannot open input)" ;; + 67) echo "User not found (addressee unknown)" ;; + 68) echo "Host not found (hostname unknown)" ;; + 69) echo "Service unavailable" ;; + 70) echo "Internal software error" ;; + 71) echo "System error (OS-level failure)" ;; + 72) echo "Critical OS file missing" ;; + 73) echo "Cannot create output file" ;; + 74) echo "I/O error" ;; + 76) echo "Remote protocol error" ;; + 77) echo "Permission denied" ;; + # --- Common shell/system errors --- 124) echo "Command timed out (timeout command)" ;; 126) echo "Command invoked cannot execute (permission problem?)" ;; 127) echo "Command not found" ;; 128) echo "Invalid argument to exit" ;; + 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; 130) echo "Aborted by user (SIGINT)" ;; 134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;; 137) echo "Killed (SIGKILL / Out of memory?)" ;; @@ -237,16 +263,21 @@ explain_exit_code() { # json_escape() # # - Escapes a string for safe JSON embedding +# - Strips ANSI escape sequences and non-printable control characters # - Handles backslashes, quotes, newlines, tabs, and carriage returns # ------------------------------------------------------------------------------ json_escape() { local s="$1" + # Strip ANSI escape sequences (color codes etc.) + s=$(printf '%s' "$s" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g') s=${s//\\/\\\\} s=${s//"/\\"/} s=${s//$'\n'/\\n} s=${s//$'\r'/} s=${s//$'\t'/\\t} - echo "$s" + # Remove any remaining control characters (0x00-0x1F except those already handled) + s=$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037') + printf '%s' "$s" } # ------------------------------------------------------------------------------ @@ -283,7 +314,33 @@ get_error_text() { fi if [[ -n "$logfile" && -s "$logfile" ]]; then - tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' + tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' + fi +} + +# ------------------------------------------------------------------------------ +# build_error_string() +# +# - Builds a structured error string for telemetry reporting +# - Format: "exit_code= | \n---\n" +# - If no log lines available, returns just the explanation +# - Arguments: +# * $1: exit_code (numeric) +# * $2: log_text (optional, output from get_error_text) +# - Returns structured error string via stdout +# ------------------------------------------------------------------------------ +build_error_string() { + local exit_code="${1:-1}" + local log_text="${2:-}" + local explanation + explanation=$(explain_exit_code "$exit_code") + + if [[ -n "$log_text" ]]; then + # Structured format: header + separator + log lines + printf 'exit_code=%s | %s\n---\n%s' "$exit_code" "$explanation" "$log_text" + else + # No log available - just the explanation with exit code + printf 'exit_code=%s | %s' "$exit_code" "$explanation" fi } @@ -369,18 +426,19 @@ detect_cpu() { # - Detects RAM speed using dmidecode # - Sets RAM_SPEED global (e.g., "4800" for DDR5-4800) # - Requires root access for dmidecode -# - Returns empty if not available +# - Returns empty if not available or if speed is "Unknown" (nested VMs) # ------------------------------------------------------------------------------ detect_ram() { RAM_SPEED="" if command -v dmidecode &>/dev/null; then # Get configured memory speed (actual running speed) - RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) + # Use || true to handle "Unknown" values in nested VMs (no numeric match) + RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) || true # Fallback to Speed: if Configured not available if [[ -z "$RAM_SPEED" ]]; then - RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1) + RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1) || true fi fi @@ -592,6 +650,8 @@ EOF curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ -H "Content-Type: application/json" \ -d "$JSON_PAYLOAD" &>/dev/null || true + + POST_TO_API_DONE=true } # ------------------------------------------------------------------------------ @@ -665,13 +725,12 @@ post_update_to_api() { else exit_code=1 fi + # Get log lines and build structured error string local error_text="" error_text=$(get_error_text) - if [[ -n "$error_text" ]]; then - error=$(json_escape "$error_text") - else - error=$(json_escape "$(explain_exit_code "$exit_code")") - fi + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") short_error=$(json_escape "$(explain_exit_code "$exit_code")") error_category=$(categorize_error "$exit_code") [[ -z "$error" ]] && error="Unknown error" @@ -814,31 +873,52 @@ EOF categorize_error() { local code="$1" case "$code" in - # Network errors - 6 | 7 | 22 | 28 | 35) echo "network" ;; + # Network errors (curl/wget) + 6 | 7 | 22 | 35) echo "network" ;; - # Storage errors - 214 | 217 | 219) echo "storage" ;; + # Timeout errors + 28 | 124 | 211) echo "timeout" ;; - # Dependency/Package errors - 100 | 101 | 102 | 127 | 160 | 161 | 162) echo "dependency" ;; + # Storage errors (Proxmox storage) + 214 | 217 | 219 | 224) echo "storage" ;; + + # Dependency/Package errors (APT, DPKG, pip, commands) + 100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;; # Permission errors 126 | 152) echo "permission" ;; - # Timeout errors - 124 | 28 | 211) echo "timeout" ;; + # Configuration errors (Proxmox config, invalid args) + 128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; - # Configuration errors - 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; + # Proxmox container/template errors + 200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 231) echo "proxmox" ;; + + # Service/Systemd errors + 150 | 151 | 153 | 154) echo "service" ;; + + # Database errors (PostgreSQL, MySQL, MongoDB) + 170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;; + + # Node.js / JavaScript runtime errors + 243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;; + + # Python environment errors + # (already covered: 160-162 under dependency) # Aborted by user 130) echo "aborted" ;; - # Resource errors (OOM, etc) - 137 | 134) echo "resource" ;; + # Resource errors (OOM, SIGKILL, SIGABRT) + 134 | 137) echo "resource" ;; - # Default + # Signal/Process errors (SIGTERM, SIGPIPE, SIGSEGV) + 129 | 139 | 141 | 143) echo "signal" ;; + + # Shell errors (general error, syntax error) + 1 | 2) echo "shell" ;; + + # Default - truly unknown *) echo "unknown" ;; esac } @@ -901,11 +981,9 @@ post_tool_to_api() { [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 local error_text="" error_text=$(get_error_text) - if [[ -n "$error_text" ]]; then - error=$(json_escape "$error_text") - else - error=$(json_escape "$(explain_exit_code "$exit_code")") - fi + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") error_category=$(categorize_error "$exit_code") fi @@ -968,11 +1046,9 @@ post_addon_to_api() { [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 local error_text="" error_text=$(get_error_text) - if [[ -n "$error_text" ]]; then - error=$(json_escape "$error_text") - else - error=$(json_escape "$(explain_exit_code "$exit_code")") - fi + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") error_category=$(categorize_error "$exit_code") fi @@ -1067,11 +1143,9 @@ post_update_to_api_extended() { fi local error_text="" error_text=$(get_error_text) - if [[ -n "$error_text" ]]; then - error=$(json_escape "$error_text") - else - error=$(json_escape "$(explain_exit_code "$exit_code")") - fi + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") error_category=$(categorize_error "$exit_code") [[ -z "$error" ]] && error="Unknown error" fi diff --git a/misc/core.func b/misc/core.func index e4c7efcc6..74b98ac4f 100644 --- a/misc/core.func +++ b/misc/core.func @@ -1641,4 +1641,17 @@ function get_lxc_ip() { # SIGNAL TRAPS # ============================================================================== +# ------------------------------------------------------------------------------ +# on_hup_keepalive() +# +# - SIGHUP (terminal hangup) trap handler +# - Keeps long-running scripts alive if terminal/SSH session disconnects +# - Stops spinner safely and writes warning to active log +# ------------------------------------------------------------------------------ +on_hup_keepalive() { + stop_spinner + log_msg "[WARN] Received SIGHUP (terminal hangup). Continuing execution in background." +} + +trap 'on_hup_keepalive' HUP trap 'stop_spinner' EXIT INT TERM diff --git a/misc/error_handler.func b/misc/error_handler.func index 87c2b4883..bd6d98929 100644 --- a/misc/error_handler.func +++ b/misc/error_handler.func @@ -49,6 +49,7 @@ if ! declare -f explain_exit_code &>/dev/null; then 126) echo "Command invoked cannot execute (permission problem?)" ;; 127) echo "Command not found" ;; 128) echo "Invalid argument to exit" ;; + 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; 130) echo "Terminated by Ctrl+C (SIGINT)" ;; 134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;; 137) echo "Killed (SIGKILL / Out of memory?)" ;; diff --git a/tools/addon/cronmaster.sh b/tools/addon/cronmaster.sh new file mode 100644 index 000000000..faf2780be --- /dev/null +++ b/tools/addon/cronmaster.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/fccview/cronmaster + +if ! command -v curl &>/dev/null; then + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + apt-get update >/dev/null 2>&1 + apt-get install -y curl >/dev/null 2>&1 +fi +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/tools.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/error_handler.func) + +# Enable error handling +set -Eeuo pipefail +trap 'error_handler' ERR + +# ============================================================================== +# CONFIGURATION +# ============================================================================== +APP="CronMaster" +APP_TYPE="addon" +INSTALL_PATH="/opt/cronmaster" +CONFIG_PATH="/opt/cronmaster/.env" +DEFAULT_PORT=3000 + +# Initialize all core functions (colors, formatting, icons, STD mode) +load_functions + +# ============================================================================== +# HEADER +# ============================================================================== +function header_info { + clear + cat <<"EOF" + ______ __ ___ __ + / ____/________ ____ / |/ /___ ______/ /____ _____ + / / / ___/ __ \/ __ \/ /|_/ / __ `/ ___/ __/ _ \/ ___/ +/ /___/ / / /_/ / / / / / / / /_/ (__ ) /_/ __/ / +\____/_/ \____/_/ /_/_/ /_/\__,_/____/\__/\___/_/ + +EOF +} + +# ============================================================================== +# OS DETECTION +# ============================================================================== +if [[ -f "/etc/alpine-release" ]]; then + msg_error "Alpine is not supported for ${APP}. Use Debian/Ubuntu." + exit 1 +elif [[ -f "/etc/debian_version" ]]; then + OS="Debian" + SERVICE_PATH="/etc/systemd/system/cronmaster.service" +else + echo -e "${CROSS} Unsupported OS detected. Exiting." + exit 1 +fi + +# ============================================================================== +# UNINSTALL +# ============================================================================== +function uninstall() { + msg_info "Uninstalling ${APP}" + systemctl disable --now cronmaster.service &>/dev/null || true + rm -f "$SERVICE_PATH" + rm -rf "$INSTALL_PATH" + rm -f "/usr/local/bin/update_cronmaster" + rm -f "$HOME/.cronmaster" + msg_ok "${APP} has been uninstalled" +} + +# ============================================================================== +# UPDATE +# ============================================================================== +function update() { + if check_for_gh_release "cronmaster" "fccview/cronmaster"; then + msg_info "Stopping service" + systemctl stop cronmaster.service &>/dev/null || true + msg_ok "Stopped service" + + msg_info "Backing up configuration" + cp "$CONFIG_PATH" /tmp/cronmaster.env.bak 2>/dev/null || true + msg_ok "Backed up configuration" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cronmaster" "fccview/cronmaster" "prebuild" "latest" "$INSTALL_PATH" "cronmaster_*_prebuild.tar.gz" + + msg_info "Restoring configuration" + cp /tmp/cronmaster.env.bak "$CONFIG_PATH" 2>/dev/null || true + rm -f /tmp/cronmaster.env.bak + msg_ok "Restored configuration" + + msg_info "Starting service" + systemctl start cronmaster + msg_ok "Started service" + msg_ok "Updated successfully" + exit + fi +} + +# ============================================================================== +# INSTALL +# ============================================================================== +function install() { + # Setup Node.js (only installs if not present or different version) + if command -v node &>/dev/null; then + msg_ok "Node.js already installed ($(node -v))" + else + NODE_VERSION="22" setup_nodejs + fi + + # Force fresh download by removing version cache + rm -f "$HOME/.cronmaster" + fetch_and_deploy_gh_release "cronmaster" "fccview/cronmaster" "prebuild" "latest" "$INSTALL_PATH" "cronmaster_*_prebuild.tar.gz" + + local AUTH_PASS + AUTH_PASS="$(openssl rand -base64 18 | cut -c1-13)" + + msg_info "Creating configuration" + cat <"$CONFIG_PATH" +NODE_ENV=production +AUTH_PASSWORD=${AUTH_PASS} +PORT=${DEFAULT_PORT} +HOSTNAME=0.0.0.0 +NEXT_TELEMETRY_DISABLED=1 +EOF + chmod 600 "$CONFIG_PATH" + msg_ok "Created configuration" + + msg_info "Creating service" + cat <"$SERVICE_PATH" +[Unit] +Description=CronMaster Service +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=${INSTALL_PATH} +EnvironmentFile=${CONFIG_PATH} +ExecStart=/usr/bin/node ${INSTALL_PATH}/server.js +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-target.target +EOF + systemctl enable --now cronmaster &>/dev/null + msg_ok "Created and started service" + + # Create update script + msg_info "Creating update script" + cat <<'UPDATEEOF' >/usr/local/bin/update_cronmaster +#!/usr/bin/env bash +# CronMaster Update Script +type=update bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/tools/addon/cronmaster.sh)" +UPDATEEOF + chmod +x /usr/local/bin/update_cronmaster + msg_ok "Created update script (/usr/local/bin/update_cronmaster)" + + echo "" + msg_ok "${APP} is reachable at: ${BL}http://${LOCAL_IP}:${DEFAULT_PORT}${CL}" + msg_ok "Password: ${BL}${AUTH_PASS}${CL}" + echo "" +} + +# ============================================================================== +# MAIN +# ============================================================================== + +# Handle type=update (called from update script) +if [[ "${type:-}" == "update" ]]; then + header_info + if [[ -d "$INSTALL_PATH" ]]; then + update + else + msg_error "${APP} is not installed. Nothing to update." + exit 1 + fi + exit 0 +fi + +header_info +get_lxc_ip + +# Check if already installed +if [[ -d "$INSTALL_PATH" && -n "$(ls -A "$INSTALL_PATH" 2>/dev/null)" ]]; then + msg_warn "${APP} is already installed." + echo "" + + echo -n "${TAB}Uninstall ${APP}? (y/N): " + read -r uninstall_prompt + if [[ "${uninstall_prompt,,}" =~ ^(y|yes)$ ]]; then + uninstall + exit 0 + fi + + echo -n "${TAB}Update ${APP}? (y/N): " + read -r update_prompt + if [[ "${update_prompt,,}" =~ ^(y|yes)$ ]]; then + update + exit 0 + fi + + msg_warn "No action selected. Exiting." + exit 0 +fi + +# Fresh installation +msg_warn "${APP} is not installed." +echo "" +echo -e "${TAB}${INFO} This will install:" +echo -e "${TAB} - Node.js 22" +echo -e "${TAB} - CronMaster (prebuild)" +echo "" + +echo -n "${TAB}Install ${APP}? (y/N): " +read -r install_prompt +if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then + install +else + msg_warn "Installation cancelled. Exiting." + exit 0 +fi diff --git a/tools/addon/jellystat.sh b/tools/addon/jellystat.sh new file mode 100644 index 000000000..2afd4fa81 --- /dev/null +++ b/tools/addon/jellystat.sh @@ -0,0 +1,374 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://github.com/CyferShepard/Jellystat + +if ! command -v curl &>/dev/null; then + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + apt-get update >/dev/null 2>&1 + apt-get install -y curl >/dev/null 2>&1 +fi +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) + +# Enable error handling +set -Eeuo pipefail +trap 'error_handler' ERR + +# ============================================================================== +# CONFIGURATION +# ============================================================================== +APP="Jellystat" +APP_TYPE="addon" +INSTALL_PATH="/opt/jellystat" +CONFIG_PATH="/opt/jellystat/.env" +DEFAULT_PORT=3000 + +# Initialize all core functions (colors, formatting, icons, STD mode) +load_functions + +# ============================================================================== +# HEADER +# ============================================================================== +function header_info { + clear + cat <<"EOF" + __ ____ __ __ + / /__ / / /_ _______/ /_____ _/ /_ + __ / / _ \/ / / / / / ___/ __/ __ `/ __/ +/ /_/ / __/ / / /_/ (__ ) /_/ /_/ / /_ +\____/\___/_/_/\__, /____/\__/\__,_/\__/ + /____/ +EOF +} + +# ============================================================================== +# OS DETECTION +# ============================================================================== +if [[ -f "/etc/alpine-release" ]]; then + msg_error "Alpine is not supported for ${APP}. Use Debian/Ubuntu." + exit 1 +elif [[ -f "/etc/debian_version" ]]; then + OS="Debian" + SERVICE_PATH="/etc/systemd/system/jellystat.service" +else + echo -e "${CROSS} Unsupported OS detected. Exiting." + exit 1 +fi + +# ============================================================================== +# UNINSTALL +# ============================================================================== +function uninstall() { + msg_info "Uninstalling ${APP}" + systemctl disable --now jellystat.service &>/dev/null || true + rm -f "$SERVICE_PATH" + rm -rf "$INSTALL_PATH" + rm -f "/usr/local/bin/update_jellystat" + rm -f "$HOME/.jellystat" + msg_ok "${APP} has been uninstalled" + + # Ask about PostgreSQL database removal + echo "" + echo -n "${TAB}Also remove PostgreSQL database 'jellystat'? (y/N): " + read -r db_prompt + if [[ "${db_prompt,,}" =~ ^(y|yes)$ ]]; then + if command -v psql &>/dev/null; then + msg_info "Removing PostgreSQL database and user" + $STD sudo -u postgres psql -c "DROP DATABASE IF EXISTS jellystat;" &>/dev/null || true + $STD sudo -u postgres psql -c "DROP USER IF EXISTS jellystat;" &>/dev/null || true + msg_ok "Removed PostgreSQL database 'jellystat' and user 'jellystat'" + else + msg_warn "PostgreSQL not found - database may have been removed already" + fi + else + msg_warn "PostgreSQL database was NOT removed. Remove manually if needed:" + echo -e "${TAB} sudo -u postgres psql -c \"DROP DATABASE jellystat;\"" + echo -e "${TAB} sudo -u postgres psql -c \"DROP USER jellystat;\"" + fi +} + +# ============================================================================== +# UPDATE +# ============================================================================== +function update() { + if check_for_gh_release "jellystat" "CyferShepard/Jellystat"; then + msg_info "Stopping service" + systemctl stop jellystat.service &>/dev/null || true + msg_ok "Stopped service" + + msg_info "Backing up configuration" + cp "$CONFIG_PATH" /tmp/jellystat.env.bak 2>/dev/null || true + msg_ok "Backed up configuration" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "jellystat" "CyferShepard/Jellystat" "tarball" "latest" "$INSTALL_PATH" + + msg_info "Restoring configuration" + cp /tmp/jellystat.env.bak "$CONFIG_PATH" 2>/dev/null || true + rm -f /tmp/jellystat.env.bak + msg_ok "Restored configuration" + + msg_info "Installing dependencies" + cd "$INSTALL_PATH" + $STD npm install + msg_ok "Installed dependencies" + + msg_info "Building ${APP}" + $STD npm run build + msg_ok "Built ${APP}" + + msg_info "Starting service" + systemctl start jellystat + msg_ok "Started service" + msg_ok "Updated successfully" + exit + fi +} + +# ============================================================================== +# INSTALL +# ============================================================================== +function install() { + # Setup Node.js (only installs if not present or different version) + if command -v node &>/dev/null; then + msg_ok "Node.js already installed ($(node -v))" + else + NODE_VERSION="22" setup_nodejs + fi + + # Setup PostgreSQL (only installs if not present) + if command -v psql &>/dev/null; then + msg_ok "PostgreSQL already installed" + else + PG_VERSION="17" setup_postgresql + fi + + # Create database and user (skip if already exists) + local DB_NAME="jellystat" + local DB_USER="jellystat" + local DB_PASS + + msg_info "Setting up PostgreSQL database" + + # Check if database already exists + if sudo -u postgres psql -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw "$DB_NAME"; then + msg_warn "Database '${DB_NAME}' already exists - skipping creation" + echo -n "${TAB}Enter existing database password for '${DB_USER}': " + read -rs DB_PASS + echo "" + else + # Generate new password + DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16) + + # Check if user exists, create if not + if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" 2>/dev/null | grep -q 1; then + msg_info "User '${DB_USER}' exists, updating password" + $STD sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" || { + msg_error "Failed to update PostgreSQL user" + return 1 + } + else + $STD sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" || { + msg_error "Failed to create PostgreSQL user" + return 1 + } + fi + + # Create database (use template0 for UTF8 encoding compatibility) + $STD sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} WITH OWNER ${DB_USER} ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE template0;" || { + msg_error "Failed to create PostgreSQL database" + return 1 + } + $STD sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" || { + msg_error "Failed to grant privileges" + return 1 + } + + # Grant schema permissions (required for PostgreSQL 15+) + $STD sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO ${DB_USER};" || true + + # Configure pg_hba.conf for password authentication on localhost + local PG_HBA + PG_HBA=$(sudo -u postgres psql -tAc "SHOW hba_file;" 2>/dev/null | tr -d ' ') + if [[ -n "$PG_HBA" && -f "$PG_HBA" ]]; then + # Check if md5/scram-sha-256 auth is already configured for local connections + if ! grep -qE "^host\s+${DB_NAME}\s+${DB_USER}\s+127.0.0.1" "$PG_HBA"; then + msg_info "Configuring PostgreSQL authentication" + # Add password auth for jellystat user on localhost (before the default rules) + sed -i "/^# IPv4 local connections:/a host ${DB_NAME} ${DB_USER} 127.0.0.1/32 scram-sha-256" "$PG_HBA" + sed -i "/^# IPv4 local connections:/a host ${DB_NAME} ${DB_USER} ::1/128 scram-sha-256" "$PG_HBA" + # Reload PostgreSQL to apply changes + systemctl reload postgresql + msg_ok "Configured PostgreSQL authentication" + fi + fi + + msg_ok "Created PostgreSQL database '${DB_NAME}'" + fi + + # Generate JWT Secret + local JWT_SECRET + JWT_SECRET=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c32) + + # Force fresh download by removing version cache + rm -f "$HOME/.jellystat" + fetch_and_deploy_gh_release "jellystat" "CyferShepard/Jellystat" "tarball" "latest" "$INSTALL_PATH" + + msg_info "Installing dependencies" + cd "$INSTALL_PATH" + $STD npm install + msg_ok "Installed dependencies" + + msg_info "Building ${APP}" + $STD npm run build + msg_ok "Built ${APP}" + + msg_info "Creating configuration" + cat <"$CONFIG_PATH" +# Jellystat Configuration +# Database +POSTGRES_USER=${DB_USER} +POSTGRES_PASSWORD=${DB_PASS} +POSTGRES_IP=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=${DB_NAME} + +# Security +JWT_SECRET=${JWT_SECRET} + +# Server +JS_LISTEN_IP=0.0.0.0 +JS_BASE_URL=/ +TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") + +# Optional: GeoLite for IP Geolocation +# JS_GEOLITE_ACCOUNT_ID= +# JS_GEOLITE_LICENSE_KEY= + +# Optional: Master Override (if you forget your password) +# JS_USER=admin +# JS_PASSWORD=admin + +# Optional: Minimum playback duration to record (seconds) +# MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK=1 + +# Optional: Self-signed certificates +REJECT_SELF_SIGNED_CERTIFICATES=true +EOF + chmod 600 "$CONFIG_PATH" + msg_ok "Created configuration" + + msg_info "Creating service" + cat <"$SERVICE_PATH" +[Unit] +Description=Jellystat - Statistics for Jellyfin +After=network.target postgresql.service + +[Service] +Type=simple +User=root +WorkingDirectory=${INSTALL_PATH}/backend +EnvironmentFile=${CONFIG_PATH} +ExecStart=/usr/bin/node ${INSTALL_PATH}/backend/server.js +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + systemctl enable --now jellystat &>/dev/null + msg_ok "Created and started service" + + # Create update script (simple wrapper that calls this addon with type=update) + msg_info "Creating update script" + cat <<'UPDATEEOF' >/usr/local/bin/update_jellystat +#!/usr/bin/env bash +# Jellystat Update Script +type=update bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/jellystat.sh)" +UPDATEEOF + chmod +x /usr/local/bin/update_jellystat + msg_ok "Created update script (/usr/local/bin/update_jellystat)" + + # Save credentials + local CREDS_FILE="/root/jellystat.creds" + cat <"$CREDS_FILE" +Jellystat Credentials +===================== +Database User: ${DB_USER} +Database Password: ${DB_PASS} +Database Name: ${DB_NAME} +JWT Secret: ${JWT_SECRET} + +Web UI: http://${LOCAL_IP}:${DEFAULT_PORT} +EOF + chmod 600 "$CREDS_FILE" + + echo "" + msg_ok "${APP} is reachable at: ${BL}http://${LOCAL_IP}:${DEFAULT_PORT}${CL}" + msg_ok "Credentials saved to: ${BL}${CREDS_FILE}${CL}" + echo "" + msg_warn "On first access, you'll need to configure your Jellyfin server connection." +} + +# ============================================================================== +# MAIN +# ============================================================================== + +# Handle type=update (called from update script) +if [[ "${type:-}" == "update" ]]; then + header_info + if [[ -d "$INSTALL_PATH" && -f "$INSTALL_PATH/package.json" ]]; then + update + else + msg_error "${APP} is not installed. Nothing to update." + exit 1 + fi + exit 0 +fi + +header_info +get_lxc_ip + +# Check if already installed +if [[ -d "$INSTALL_PATH" && -f "$INSTALL_PATH/package.json" ]]; then + msg_warn "${APP} is already installed." + echo "" + + echo -n "${TAB}Uninstall ${APP}? (y/N): " + read -r uninstall_prompt + if [[ "${uninstall_prompt,,}" =~ ^(y|yes)$ ]]; then + uninstall + exit 0 + fi + + echo -n "${TAB}Update ${APP}? (y/N): " + read -r update_prompt + if [[ "${update_prompt,,}" =~ ^(y|yes)$ ]]; then + update + exit 0 + fi + + msg_warn "No action selected. Exiting." + exit 0 +fi + +# Fresh installation +msg_warn "${APP} is not installed." +echo "" +echo -e "${TAB}${INFO} This will install:" +echo -e "${TAB} - Node.js 22" +echo -e "${TAB} - PostgreSQL 17" +echo -e "${TAB} - Jellystat" +echo "" + +echo -n "${TAB}Install ${APP}? (y/N): " +read -r install_prompt +if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then + install +else + msg_warn "Installation cancelled. Exiting." + exit 0 +fi diff --git a/tools/pve/dependency-check copy.sh b/tools/pve/dependency-check copy.sh new file mode 100644 index 000000000..d0a328912 --- /dev/null +++ b/tools/pve/dependency-check copy.sh @@ -0,0 +1,361 @@ +#!/usr/bin/env bash + +# Copyright (c) 2023 community-scripts ORG +# This script is designed to install the Proxmox Dependency Check Hookscript. +# It sets up a dependency-checking hookscript and automates its +# application to all new and existing guests using a systemd watcher. +# License: MIT + +function header_info { + clear + cat <<"EOF" + ____ _ ____ _ _ + | _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ / ___| |__ ___ ___| | __ + | | | |/ _ \ '_ \ / _ \ '_ \ / _` |/ _ \ '_ \ / __| | | | | | '_ \ / _ \/ __| |/ / + | |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | |___| | | | __/ (__| < + |____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, |\____|_| |_|\___|\___|_|\_\ + |_| |___/ +EOF +} + +# Color variables +YW=$(echo "\033[33m") +GN=$(echo "\033[1;92m") +RD=$(echo "\033[01;31m") +CL=$(echo "\033[m") +BFR="\\r\\033[K" +HOLD=" " +CM="${GN}✓${CL}" +CROSS="${RD}✗${CL}" + +# Spinner for progress indication (simplified) +spinner() { + local pid=$! + local delay=0.1 + local spinstr='|/-\' + while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do + local temp=${spinstr#?} + printf " [%c] " "$spinstr" + local spinstr=$temp${spinstr%"$temp"} + sleep $delay + printf "\b\b\b\b\b\b" + done + printf " \b\b\b\b" +} + +# Message functions +msg_info() { + echo -ne " ${YW}›${CL} $1..." +} + +msg_ok() { + echo -e "${BFR} ${CM} $1${CL}" +} + +msg_error() { + echo -e "${BFR} ${CROSS} $1${CL}" +} +# --- End of base script functions --- + +# --- Installation Functions --- + +# Function to create the actual hookscript that runs before guest startup +create_dependency_hookscript() { + msg_info "Creating dependency-check hookscript" + mkdir -p /var/lib/vz/snippets + cat <<'EOF' >/var/lib/vz/snippets/dependency-check.sh +#!/bin/bash +# Proxmox Hookscript for Pre-Start Dependency Checking +# Works for both QEMU VMs and LXC Containers + +# --- Configuration --- +POLL_INTERVAL=5 # Seconds to wait between checks +MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes) +# --- End Configuration --- + +VMID=$1 +PHASE=$2 + +# Function for logging to syslog with a consistent format +log() { + echo "[hookscript-dep-check] VMID $VMID: $1" +} + +# This script only runs in the 'pre-start' phase +if [ "$PHASE" != "pre-start" ]; then + exit 0 +fi + +log "--- Starting Pre-Start Dependency Check ---" + +# --- Determine Guest Type (QEMU or LXC) --- +GUEST_TYPE="" +CONFIG_CMD="" +if qm config "$VMID" >/dev/null 2>&1; then + GUEST_TYPE="qemu" + CONFIG_CMD="qm config" + log "Guest type is QEMU (VM)." +elif pct config "$VMID" >/dev/null 2>&1; then + GUEST_TYPE="lxc" + CONFIG_CMD="pct config" + log "Guest type is LXC (Container)." +else + log "ERROR: Could not determine guest type for $VMID. Aborting." + exit 1 +fi + +GUEST_CONFIG=$($CONFIG_CMD "$VMID") + +# --- 1. Storage Availability Check --- +log "Checking storage availability..." +# Grep for all disk definitions (scsi, sata, virtio, ide, rootfs, mp) +# and extract the storage identifier (the field between the colons). +# Sort -u gets the unique list of storage pools. +STORAGE_IDS=$(echo "$GUEST_CONFIG" | grep -E '^(scsi|sata|virtio|ide|rootfs|mp)[0-9]*:' | awk -F'[:]' '{print $2}' | awk '{print$1}' | sort -u) + +if [ -z "$STORAGE_IDS" ]; then + log "No storage dependencies found to check." +else + for STORAGE_ID in $STORAGE_IDS; do + log "Checking status of storage: '$STORAGE_ID'" + ATTEMPTS=0 + while true; do + # Grep for the storage ID line in pvesm status and check the 'Active' column (3rd column) + STATUS=$(pvesm status | grep "^\s*$STORAGE_ID\s" | awk '{print $3}') + if [ "$STATUS" == "active" ]; then + log "Storage '$STORAGE_ID' is active." + break + fi + + ATTEMPTS=$((ATTEMPTS + 1)) + if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then + log "ERROR: Timeout waiting for storage '$STORAGE_ID' to become active. Aborting start." + exit 1 + fi + + log "Storage '$STORAGE_ID' is not active (current status: '${STATUS:-inactive/unknown}'). Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" + sleep $POLL_INTERVAL + done + done +fi +log "All storage dependencies are met." + + +# --- 2. Custom Tag-Based Dependency Check --- +log "Checking for custom tag-based dependencies..." +TAGS=$(echo "$GUEST_CONFIG" | grep '^tags:' | awk '{print $2}') + +if [ -z "$TAGS" ]; then + log "No tags found. Skipping custom dependency check." +else + # Replace colons with spaces to loop through tags + for TAG in ${TAGS//;/ }; do + # Check if the tag matches our dependency format 'dep_*' + if [[ $TAG == dep_* ]]; then + log "Found dependency tag: '$TAG'" + + # Split tag into parts using underscore as delimiter + IFS='_' read -ra PARTS <<< "$TAG" + DEP_TYPE="${PARTS[1]}" + + ATTEMPTS=0 + while true; do + CHECK_PASSED=false + case "$DEP_TYPE" in + "tcp") + HOST="${PARTS[2]}" + PORT="${PARTS[3]}" + if [ -z "$HOST" ] || [ -z "$PORT" ]; then + log "ERROR: Malformed TCP dependency tag '$TAG'. Skipping." + CHECK_PASSED=true # Skip to avoid infinite loop + # nc -z is great for this. -w sets a timeout. + elif nc -z -w 2 "$HOST" "$PORT"; then + log "TCP dependency met: Host $HOST port $PORT is open." + CHECK_PASSED=true + fi + ;; + + "ping") + HOST="${PARTS[2]}" + if [ -z "$HOST" ]; then + log "ERROR: Malformed PING dependency tag '$TAG'. Skipping." + CHECK_PASSED=true # Skip to avoid infinite loop + # ping -c 1 (one packet) -W 2 (2-second timeout) + elif ping -c 1 -W 2 "$HOST" >/dev/null 2>&1; then + log "Ping dependency met: Host $HOST is reachable." + CHECK_PASSED=true + fi + ;; + + *) + log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring." + CHECK_PASSED=true # Mark as passed to avoid getting stuck + ;; + esac + + if $CHECK_PASSED; then + break + fi + + ATTEMPTS=$((ATTEMPTS + 1)) + if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then + log "ERROR: Timeout waiting for dependency '$TAG'. Aborting start." + exit 1 + fi + + log "Dependency '$TAG' not met. Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" + sleep $POLL_INTERVAL + done + fi + done +fi + +log "All custom dependencies are met." +log "--- Dependency Check Complete. Proceeding with start. ---" +exit 0 +EOF + chmod +x /var/lib/vz/snippets/dependency-check.sh + msg_ok "Created dependency-check hookscript" +} + +# Function to create the config file for exclusions +create_exclusion_config() { + msg_info "Creating exclusion configuration file" + if [ -f /etc/default/pve-auto-hook ]; then + msg_ok "Exclusion file already exists, skipping." + else + cat <<'EOF' >/etc/default/pve-auto-hook +# +# Configuration for the Proxmox Automatic Hookscript Applicator +# +# Add VM or LXC IDs here to prevent the hookscript from being added. +# Separate IDs with spaces. +# +# Example: +# IGNORE_IDS="9000 9001 105" +# + +IGNORE_IDS="" +EOF + msg_ok "Created exclusion configuration file" + fi +} + +# Function to create the script that applies the hook +create_applicator_script() { + msg_info "Creating the hookscript applicator script" + cat <<'EOF' >/usr/local/bin/pve-apply-hookscript.sh +#!/bin/bash +HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh" +CONFIG_FILE="/etc/default/pve-auto-hook" +LOG_TAG="pve-auto-hook-list" + +log() { + systemd-cat -t "$LOG_TAG" <<< "$1" +} + +if [ -f "$CONFIG_FILE" ]; then + source "$CONFIG_FILE" +fi + +# Process QEMU VMs +qm list | awk 'NR>1 {print $1}' | while read -r VMID; do + is_ignored=false + for id_to_ignore in $IGNORE_IDS; do + if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi + done + if $is_ignored; then continue; fi + if qm config "$VMID" | grep -q '^hookscript:'; then continue; fi + log "Hookscript not found for VM $VMID. Applying..." + qm set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" +done + +# Process LXC Containers +pct list | awk 'NR>1 {print $1}' | while read -r VMID; do + is_ignored=false + for id_to_ignore in $IGNORE_IDS; do + if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi + done + if $is_ignored; then continue; fi + if pct config "$VMID" | grep -q '^hookscript:'; then continue; fi + log "Hookscript not found for LXC $VMID. Applying..." + pct set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" +done +EOF + chmod +x /usr/local/bin/pve-apply-hookscript.sh + msg_ok "Created applicator script" +} + +# Function to set up the systemd watcher and service +create_systemd_units() { + msg_info "Creating systemd watcher and service units" + cat <<'EOF' >/etc/systemd/system/pve-auto-hook.path +[Unit] +Description=Watch for new Proxmox guest configs to apply hookscript + +[Path] +PathModified=/etc/pve/qemu-server/ +PathModified=/etc/pve/lxc/ + +[Install] +WantedBy=multi-user.target +EOF + + cat <<'EOF' >/etc/systemd/system/pve-auto-hook.service +[Unit] +Description=Automatically add hookscript to new Proxmox guests + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/pve-apply-hookscript.sh +EOF + msg_ok "Created systemd units" +} + +# --- Main Execution --- +header_info + +if ! command -v pveversion >/dev/null 2>&1; then + msg_error "This script must be run on a Proxmox VE host." + exit 1 +fi + +echo -e "\nThis script will install a service to automatically apply a" +echo -e "dependency-checking hookscript to all new and existing Proxmox guests." +echo -e "${YW}This includes creating files in:${CL}" +echo -e " - /var/lib/vz/snippets/" +echo -e " - /usr/local/bin/" +echo -e " - /etc/default/" +echo -e " - /etc/systemd/system/\n" + +read -p "Do you want to proceed with the installation? (y/n): " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + msg_error "Installation cancelled." + exit 1 +fi + +echo -e "\n" +create_dependency_hookscript +create_exclusion_config +create_applicator_script +create_systemd_units + +msg_info "Reloading systemd and enabling the watcher" +(systemctl daemon-reload && systemctl enable --now pve-auto-hook.path) >/dev/null 2>&1 & +spinner +msg_ok "Systemd watcher enabled and running" + +msg_info "Performing initial run to update existing guests" +/usr/local/bin/pve-apply-hookscript.sh >/dev/null 2>&1 & +spinner +msg_ok "Initial run complete" + +echo -e "\n\n${GN}Installation successful!${CL}" +echo -e "The service is now active and will monitor for new guests." +echo -e "To ${YW}exclude${CL} a VM or LXC, add its ID to the ${YW}IGNORE_IDS${CL} variable in:" +echo -e " ${YW}/etc/default/pve-auto-hook${CL}" +echo -e "\nYou can monitor the service's activity with:" +echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n" + +exit 0 diff --git a/tools/pve/dependency-check.sh b/tools/pve/dependency-check.sh index b7798d95a..36418dbc1 100644 --- a/tools/pve/dependency-check.sh +++ b/tools/pve/dependency-check.sh @@ -1,10 +1,9 @@ #!/usr/bin/env bash -# Copyright (c) 2023 community-scripts ORG -# This script is designed to install the Proxmox Dependency Check Hookscript. -# It sets up a dependency-checking hookscript and automates its -# application to all new and existing guests using a systemd watcher. -# License: MIT +# Copyright (c) 2023-2026 community-scripts ORG +# Author: MickLesk | Maintainer: community-scripts +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://www.proxmox.com/ function header_info { clear @@ -18,195 +17,220 @@ function header_info { EOF } -# Color variables YW=$(echo "\033[33m") GN=$(echo "\033[1;92m") RD=$(echo "\033[01;31m") +BL=$(echo "\033[36m") CL=$(echo "\033[m") BFR="\\r\\033[K" -HOLD=" " -CM="${GN}✓${CL}" -CROSS="${RD}✗${CL}" +CM="${GN}✔️${CL}" +CROSS="${RD}✖️${CL}" +INFO="${BL}ℹ️${CL}" -# Spinner for progress indication (simplified) -spinner() { - local pid=$! - local delay=0.1 - local spinstr='|/-\' - while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do - local temp=${spinstr#?} - printf " [%c] " "$spinstr" - local spinstr=$temp${spinstr%"$temp"} - sleep $delay - printf "\b\b\b\b\b\b" - done - printf " \b\b\b\b" -} - -# Message functions msg_info() { - echo -ne " ${YW}›${CL} $1..." + local msg="$1" + echo -e "${INFO} ${YW}${msg}...${CL}" } msg_ok() { - echo -e "${BFR} ${CM} $1${CL}" + local msg="$1" + echo -e "${BFR} ${CM} ${GN}${msg}${CL}" } msg_error() { - echo -e "${BFR} ${CROSS} $1${CL}" + local msg="$1" + echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}" } -# --- End of base script functions --- +SCRIPT_NAME="$(basename "$0")" +HOOKSCRIPT_FILE="/var/lib/vz/snippets/dependency-check.sh" +HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh" +CONFIG_FILE="/etc/default/pve-auto-hook" +APPLICATOR_FILE="/usr/local/bin/pve-apply-hookscript.sh" +PATH_UNIT_FILE="/etc/systemd/system/pve-auto-hook.path" +SERVICE_UNIT_FILE="/etc/systemd/system/pve-auto-hook.service" -# --- Installation Functions --- +function print_usage { + cat </dev/null 2>&1; then + msg_error "This script must be run on a Proxmox VE host" + exit 1 + fi + + local pve_version major + pve_version=$(pveversion | grep -oE 'pve-manager/[0-9.]+' | cut -d'/' -f2) + major=$(echo "$pve_version" | cut -d'.' -f1) + + if [[ -z "$major" ]] || ! [[ "$major" =~ ^[0-9]+$ ]]; then + msg_error "Unable to detect a supported Proxmox version" + exit 1 + fi + + if [[ "$major" -lt 8 ]] || [[ "$major" -gt 9 ]]; then + msg_error "Supported on Proxmox VE 8.x and 9.x (detected: $pve_version)" + exit 1 + fi + + msg_ok "Proxmox VE $pve_version detected" +} + +function confirm_action { + local prompt="$1" + read -r -p "$prompt (y/n): " -n 1 REPLY + echo + [[ "$REPLY" =~ ^[Yy]$ ]] +} -# Function to create the actual hookscript that runs before guest startup create_dependency_hookscript() { - msg_info "Creating dependency-check hookscript" - mkdir -p /var/lib/vz/snippets - cat <<'EOF' > /var/lib/vz/snippets/dependency-check.sh + msg_info "Creating dependency-check hookscript" + mkdir -p /var/lib/vz/snippets + cat <<'EOF' >/var/lib/vz/snippets/dependency-check.sh #!/bin/bash # Proxmox Hookscript for Pre-Start Dependency Checking # Works for both QEMU VMs and LXC Containers -# --- Configuration --- POLL_INTERVAL=5 # Seconds to wait between checks MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes) -# --- End Configuration --- VMID=$1 PHASE=$2 -# Function for logging to syslog with a consistent format log() { - echo "[hookscript-dep-check] VMID $VMID: $1" + logger -t hookscript-dep-check "VMID $VMID: $1" +} + +has_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +check_tcp() { + local host="$1" + local port="$2" + + if has_cmd nc; then + nc -z -w 2 "$host" "$port" >/dev/null 2>&1 + return $? + fi + + timeout 2 bash -c "/dev/null 2>&1 +} + +wait_until() { + local description="$1" + local check_cmd="$2" + + local attempts=0 + while true; do + if eval "$check_cmd"; then + log "$description" + return 0 + fi + + attempts=$((attempts + 1)) + if [ "$attempts" -ge "$MAX_ATTEMPTS" ]; then + log "ERROR: Timeout waiting for condition: $description" + return 1 + fi + + log "Waiting ${POLL_INTERVAL}s for condition: $description (Attempt ${attempts}/${MAX_ATTEMPTS})" + sleep "$POLL_INTERVAL" + done } -# This script only runs in the 'pre-start' phase if [ "$PHASE" != "pre-start" ]; then exit 0 fi log "--- Starting Pre-Start Dependency Check ---" -# --- Determine Guest Type (QEMU or LXC) --- -GUEST_TYPE="" -CONFIG_CMD="" if qm config "$VMID" >/dev/null 2>&1; then - GUEST_TYPE="qemu" - CONFIG_CMD="qm config" + CONFIG_CMD=(qm config "$VMID") log "Guest type is QEMU (VM)." elif pct config "$VMID" >/dev/null 2>&1; then - GUEST_TYPE="lxc" - CONFIG_CMD="pct config" + CONFIG_CMD=(pct config "$VMID") log "Guest type is LXC (Container)." else log "ERROR: Could not determine guest type for $VMID. Aborting." exit 1 fi -GUEST_CONFIG=$($CONFIG_CMD "$VMID") +GUEST_CONFIG=$("${CONFIG_CMD[@]}") -# --- 1. Storage Availability Check --- log "Checking storage availability..." -# Grep for all disk definitions (scsi, sata, virtio, ide, rootfs, mp) -# and extract the storage identifier (the field between the colons). -# Sort -u gets the unique list of storage pools. -STORAGE_IDS=$(echo "$GUEST_CONFIG" | grep -E '^(scsi|sata|virtio|ide|rootfs|mp)[0-9]*:' | awk -F'[:]' '{print $2}' | awk '{print$1}' | sort -u) +STORAGE_IDS=$(echo "$GUEST_CONFIG" | awk -F':' ' + /^(scsi|sata|virtio|ide|efidisk|tpmstate|unused|rootfs|mp)[0-9]*:/ { + val=$2 + gsub(/^[[:space:]]+/, "", val) + split(val, parts, ",") + storage=parts[1] + + # Skip bind-mount style paths and empty values + if (storage == "" || storage ~ /^\//) next + + print storage + } +' | sort -u) if [ -z "$STORAGE_IDS" ]; then log "No storage dependencies found to check." else for STORAGE_ID in $STORAGE_IDS; do - log "Checking status of storage: '$STORAGE_ID'" - ATTEMPTS=0 - while true; do - # Grep for the storage ID line in pvesm status and check the 'Active' column (3rd column) - STATUS=$(pvesm status | grep "^\s*$STORAGE_ID\s" | awk '{print $3}') - if [ "$STATUS" == "active" ]; then - log "Storage '$STORAGE_ID' is active." - break - fi + STATUS=$(pvesm status 2>/dev/null | awk -v id="$STORAGE_ID" '$1 == id { print $3; exit }') - ATTEMPTS=$((ATTEMPTS + 1)) - if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then - log "ERROR: Timeout waiting for storage '$STORAGE_ID' to become active. Aborting start." - exit 1 - fi + if [ -z "$STATUS" ]; then + log "WARNING: Storage '$STORAGE_ID' not found in 'pvesm status'. Skipping this dependency." + continue + fi - log "Storage '$STORAGE_ID' is not active (current status: '${STATUS:-inactive/unknown}'). Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" - sleep $POLL_INTERVAL - done + wait_until "Storage '$STORAGE_ID' is active." "[ \"\$(pvesm status 2>/dev/null | awk -v id=\"$STORAGE_ID\" '\$1 == id { print \$3; exit }')\" = \"active\" ]" || exit 1 done fi log "All storage dependencies are met." - -# --- 2. Custom Tag-Based Dependency Check --- log "Checking for custom tag-based dependencies..." -TAGS=$(echo "$GUEST_CONFIG" | grep '^tags:' | awk '{print $2}') +TAGS=$(echo "$GUEST_CONFIG" | awk -F': ' '/^tags:/ {print $2}') if [ -z "$TAGS" ]; then log "No tags found. Skipping custom dependency check." else - # Replace colons with spaces to loop through tags for TAG in ${TAGS//;/ }; do - # Check if the tag matches our dependency format 'dep_*' if [[ $TAG == dep_* ]]; then log "Found dependency tag: '$TAG'" - # Split tag into parts using underscore as delimiter - IFS='_' read -ra PARTS <<< "$TAG" - DEP_TYPE="${PARTS[1]}" + IFS='_' read -r _ DEP_TYPE HOST PORT EXTRA <<< "$TAG" - ATTEMPTS=0 - while true; do - CHECK_PASSED=false - case "$DEP_TYPE" in - "tcp") - HOST="${PARTS[2]}" - PORT="${PARTS[3]}" - if [ -z "$HOST" ] || [ -z "$PORT" ]; then - log "ERROR: Malformed TCP dependency tag '$TAG'. Skipping." - CHECK_PASSED=true # Skip to avoid infinite loop - # nc -z is great for this. -w sets a timeout. - elif nc -z -w 2 "$HOST" "$PORT"; then - log "TCP dependency met: Host $HOST port $PORT is open." - CHECK_PASSED=true - fi - ;; - - "ping") - HOST="${PARTS[2]}" - if [ -z "$HOST" ]; then - log "ERROR: Malformed PING dependency tag '$TAG'. Skipping." - CHECK_PASSED=true # Skip to avoid infinite loop - # ping -c 1 (one packet) -W 2 (2-second timeout) - elif ping -c 1 -W 2 "$HOST" >/dev/null 2>&1; then - log "Ping dependency met: Host $HOST is reachable." - CHECK_PASSED=true - fi - ;; - - *) - log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring." - CHECK_PASSED=true # Mark as passed to avoid getting stuck - ;; - esac - - if $CHECK_PASSED; then - break - fi - - ATTEMPTS=$((ATTEMPTS + 1)) - if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then - log "ERROR: Timeout waiting for dependency '$TAG'. Aborting start." - exit 1 - fi - - log "Dependency '$TAG' not met. Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" - sleep $POLL_INTERVAL - done + case "$DEP_TYPE" in + ping) + if [ -z "$HOST" ]; then + log "WARNING: Malformed ping dependency tag '$TAG'. Ignoring." + continue + fi + wait_until "Ping dependency met: Host $HOST is reachable." "ping -c 1 -W 2 \"$HOST\" >/dev/null 2>&1" || exit 1 + ;; + tcp) + if [ -z "$HOST" ] || [ -z "$PORT" ] || ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then + log "WARNING: Malformed TCP dependency tag '$TAG'. Expected dep_tcp__. Ignoring." + continue + fi + wait_until "TCP dependency met: Host $HOST port $PORT is open." "check_tcp \"$HOST\" \"$PORT\"" || exit 1 + ;; + *) + log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring." + ;; + esac fi done fi @@ -215,17 +239,16 @@ log "All custom dependencies are met." log "--- Dependency Check Complete. Proceeding with start. ---" exit 0 EOF - chmod +x /var/lib/vz/snippets/dependency-check.sh - msg_ok "Created dependency-check hookscript" + chmod +x "$HOOKSCRIPT_FILE" + msg_ok "Created dependency-check hookscript" } -# Function to create the config file for exclusions create_exclusion_config() { - msg_info "Creating exclusion configuration file" - if [ -f /etc/default/pve-auto-hook ]; then - msg_ok "Exclusion file already exists, skipping." - else - cat <<'EOF' > /etc/default/pve-auto-hook + msg_info "Creating exclusion configuration file" + if [ -f "$CONFIG_FILE" ]; then + msg_ok "Exclusion file already exists, skipping." + else + cat <<'EOF' >/etc/default/pve-auto-hook # # Configuration for the Proxmox Automatic Hookscript Applicator # @@ -238,71 +261,99 @@ create_exclusion_config() { IGNORE_IDS="" EOF - msg_ok "Created exclusion configuration file" - fi + chmod 0644 "$CONFIG_FILE" + msg_ok "Created exclusion configuration file" + fi } -# Function to create the script that applies the hook create_applicator_script() { - msg_info "Creating the hookscript applicator script" - cat <<'EOF' > /usr/local/bin/pve-apply-hookscript.sh + msg_info "Creating the hookscript applicator script" + cat <<'EOF' >/usr/local/bin/pve-apply-hookscript.sh #!/bin/bash HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh" CONFIG_FILE="/etc/default/pve-auto-hook" -LOG_TAG="pve-auto-hook-list" +LOG_TAG="pve-auto-hook" +IGNORE_IDS="" log() { systemd-cat -t "$LOG_TAG" <<< "$1" } if [ -f "$CONFIG_FILE" ]; then - source "$CONFIG_FILE" + IGNORE_IDS=$(grep -E '^IGNORE_IDS=' "$CONFIG_FILE" | head -n1 | cut -d'=' -f2- | tr -d '"') fi -# Process QEMU VMs -qm list | awk 'NR>1 {print $1}' | while read -r VMID; do - is_ignored=false +is_ignored() { + local vmid="$1" for id_to_ignore in $IGNORE_IDS; do - if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi + if [ "$id_to_ignore" = "$vmid" ]; then + return 0 + fi done - if $is_ignored; then continue; fi - if qm config "$VMID" | grep -q '^hookscript:'; then continue; fi - log "Hookscript not found for VM $VMID. Applying..." - qm set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" -done - -# Process LXC Containers -pct list | awk 'NR>1 {print $1}' | while read -r VMID; do - is_ignored=false - for id_to_ignore in $IGNORE_IDS; do - if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi - done - if $is_ignored; then continue; fi - if pct config "$VMID" | grep -q '^hookscript:'; then continue; fi - log "Hookscript not found for LXC $VMID. Applying..." - pct set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" -done -EOF - chmod +x /usr/local/bin/pve-apply-hookscript.sh - msg_ok "Created applicator script" + return 1 +} + +ensure_hookscript() { + local guest_type="$1" + local vmid="$2" + local current_hook="" + + if [ "$guest_type" = "qemu" ]; then + current_hook=$(qm config "$vmid" | awk '/^hookscript:/ {print $2}') + else + current_hook=$(pct config "$vmid" | awk '/^hookscript:/ {print $2}') + fi + + if [ -n "$current_hook" ]; then + if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then + return 0 + fi + log "Guest $guest_type/$vmid already has another hookscript ($current_hook). Leaving unchanged." + return 0 + fi + + log "Applying hookscript to $guest_type/$vmid" + if [ "$guest_type" = "qemu" ]; then + qm set "$vmid" --hookscript "$HOOKSCRIPT_VOLUME_ID" >/dev/null 2>&1 + else + pct set "$vmid" --hookscript "$HOOKSCRIPT_VOLUME_ID" >/dev/null 2>&1 + fi +} + +qm list | awk 'NR>1 {print $1}' | while read -r VMID; do + if is_ignored "$VMID"; then + continue + fi + ensure_hookscript "qemu" "$VMID" +done + +pct list | awk 'NR>1 {print $1}' | while read -r VMID; do + if is_ignored "$VMID"; then + continue + fi + ensure_hookscript "lxc" "$VMID" +done +EOF + chmod +x "$APPLICATOR_FILE" + msg_ok "Created applicator script" } -# Function to set up the systemd watcher and service create_systemd_units() { - msg_info "Creating systemd watcher and service units" - cat <<'EOF' > /etc/systemd/system/pve-auto-hook.path + msg_info "Creating systemd watcher and service units" + cat <<'EOF' >/etc/systemd/system/pve-auto-hook.path [Unit] Description=Watch for new Proxmox guest configs to apply hookscript [Path] -PathModified=/etc/pve/qemu-server/ -PathModified=/etc/pve/lxc/ +PathExistsGlob=/etc/pve/qemu-server/*.conf +PathExistsGlob=/etc/pve/lxc/*.conf +Unit=pve-auto-hook.service [Install] WantedBy=multi-user.target EOF - cat <<'EOF' > /etc/systemd/system/pve-auto-hook.service + cat <<'EOF' >/etc/systemd/system/pve-auto-hook.service [Unit] Description=Automatically add hookscript to new Proxmox guests @@ -310,54 +361,149 @@ Description=Automatically add hookscript to new Proxmox guests Type=oneshot ExecStart=/usr/local/bin/pve-apply-hookscript.sh EOF - msg_ok "Created systemd units" + chmod 0644 "$PATH_UNIT_FILE" "$SERVICE_UNIT_FILE" + msg_ok "Created systemd units" } +remove_hookscript_assignments() { + msg_info "Removing hookscript assignment from guests using dependency-check" + + qm list | awk 'NR>1 {print $1}' | while read -r vmid; do + current_hook=$(qm config "$vmid" | awk '/^hookscript:/ {print $2}') + if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then + qm set "$vmid" --delete hookscript >/dev/null 2>&1 && msg_ok "Removed hookscript from VM $vmid" + fi + done + + pct list | awk 'NR>1 {print $1}' | while read -r vmid; do + current_hook=$(pct config "$vmid" | awk '/^hookscript:/ {print $2}') + if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then + pct set "$vmid" --delete hookscript >/dev/null 2>&1 && msg_ok "Removed hookscript from LXC $vmid" + fi + done +} + +install_stack() { + create_dependency_hookscript + create_exclusion_config + create_applicator_script + create_systemd_units + + msg_info "Reloading systemd and enabling watcher" + if systemctl daemon-reload && systemctl enable --now pve-auto-hook.path >/dev/null 2>&1; then + msg_ok "Systemd watcher enabled and running" + else + msg_error "Could not enable pve-auto-hook.path" + exit 1 + fi + + msg_info "Performing initial run to update existing guests" + if "$APPLICATOR_FILE" >/dev/null 2>&1; then + msg_ok "Initial run complete" + else + msg_error "Initial run failed" + exit 1 + fi +} + +uninstall_stack() { + remove_hookscript_assignments + + msg_info "Stopping and disabling systemd units" + systemctl disable --now pve-auto-hook.path >/dev/null 2>&1 || true + systemctl disable --now pve-auto-hook.service >/dev/null 2>&1 || true + + msg_info "Removing installed files" + rm -f "$HOOKSCRIPT_FILE" "$APPLICATOR_FILE" "$PATH_UNIT_FILE" "$SERVICE_UNIT_FILE" "$CONFIG_FILE" + + if systemctl daemon-reload >/dev/null 2>&1; then + msg_ok "systemd daemon reloaded" + else + msg_error "Failed to reload systemd daemon" + exit 1 + fi + + msg_ok "Dependency-check stack successfully removed" +} + +show_status() { + echo -e "\n${BL}Dependency-check status${CL}" + echo -e "--------------------------------" + [ -f "$HOOKSCRIPT_FILE" ] && echo -e "Hookscript file: ${GN}present${CL}" || echo -e "Hookscript file: ${RD}missing${CL}" + [ -f "$APPLICATOR_FILE" ] && echo -e "Applicator script: ${GN}present${CL}" || echo -e "Applicator script: ${RD}missing${CL}" + [ -f "$CONFIG_FILE" ] && echo -e "Config file: ${GN}present${CL}" || echo -e "Config file: ${RD}missing${CL}" + [ -f "$PATH_UNIT_FILE" ] && echo -e "Path unit: ${GN}present${CL}" || echo -e "Path unit: ${RD}missing${CL}" + [ -f "$SERVICE_UNIT_FILE" ] && echo -e "Service unit: ${GN}present${CL}" || echo -e "Service unit: ${RD}missing${CL}" + + if systemctl is-enabled pve-auto-hook.path >/dev/null 2>&1; then + echo -e "Watcher enabled: ${GN}yes${CL}" + else + echo -e "Watcher enabled: ${YW}no${CL}" + fi + + if systemctl is-active pve-auto-hook.path >/dev/null 2>&1; then + echo -e "Watcher active: ${GN}yes${CL}" + else + echo -e "Watcher active: ${YW}no${CL}" + fi +} -# --- Main Execution --- header_info +ensure_supported_pve -if ! command -v pveversion >/dev/null 2>&1; then - msg_error "This script must be run on a Proxmox VE host." +case "${1:---install}" in +--help | -h) + print_usage + exit 0 + ;; +--status) + show_status + exit 0 + ;; +--install) + echo -e "\nThis script will install a service to automatically apply a" + echo -e "dependency-checking hookscript to all new and existing Proxmox guests." + echo -e "${YW}This includes creating files in:${CL}" + echo -e " - /var/lib/vz/snippets/" + echo -e " - /usr/local/bin/" + echo -e " - /etc/default/" + echo -e " - /etc/systemd/system/\n" + + if ! confirm_action "Do you want to proceed with the installation?"; then + msg_error "Installation cancelled" exit 1 -fi + fi -echo -e "\nThis script will install a service to automatically apply a" -echo -e "dependency-checking hookscript to all new and existing Proxmox guests." -echo -e "${YW}This includes creating files in:${CL}" -echo -e " - /var/lib/vz/snippets/" -echo -e " - /usr/local/bin/" -echo -e " - /etc/default/" -echo -e " - /etc/systemd/system/\n" + echo "" + install_stack -read -p "Do you want to proceed with the installation? (y/n): " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - msg_error "Installation cancelled." + echo -e "\n${GN}Installation successful!${CL}" + echo -e "The service is now active and will monitor for new guests." + echo -e "To ${YW}exclude${CL} a VM or LXC, add its ID to ${YW}IGNORE_IDS${CL} in:" + echo -e " ${YW}${CONFIG_FILE}${CL}" + echo -e "\nMonitor activity with:" + echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n" + ;; +--uninstall) + echo -e "\nThis will completely remove the dependency-check stack:" + echo -e " - hookscript and applicator" + echo -e " - systemd path/service units" + echo -e " - exclusion config" + echo -e " - hookscript assignment from guests using ${HOOKSCRIPT_VOLUME_ID}\n" + + if ! confirm_action "Do you want to proceed with uninstall?"; then + msg_error "Uninstall cancelled" exit 1 -fi + fi -echo -e "\n" -create_dependency_hookscript -create_exclusion_config -create_applicator_script -create_systemd_units - -msg_info "Reloading systemd and enabling the watcher" -(systemctl daemon-reload && systemctl enable --now pve-auto-hook.path) >/dev/null 2>&1 & -spinner -msg_ok "Systemd watcher enabled and running" - -msg_info "Performing initial run to update existing guests" -/usr/local/bin/pve-apply-hookscript.sh >/dev/null 2>&1 & -spinner -msg_ok "Initial run complete" - -echo -e "\n\n${GN}Installation successful!${CL}" -echo -e "The service is now active and will monitor for new guests." -echo -e "To ${YW}exclude${CL} a VM or LXC, add its ID to the ${YW}IGNORE_IDS${CL} variable in:" -echo -e " ${YW}/etc/default/pve-auto-hook${CL}" -echo -e "\nYou can monitor the service's activity with:" -echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n" + echo "" + uninstall_stack + ;; +*) + msg_error "Unknown option: $1" + print_usage + exit 1 + ;; +esac exit 0