Merge branch 'community-scripts:main' into truenas-vm-whiptail-box-size-fix

This commit is contained in:
juronja 2026-02-17 18:46:26 +01:00 committed by GitHub
commit edb291bb29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1516 additions and 691 deletions

View File

@ -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"

View File

@ -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!"

View File

@ -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}"

View File

@ -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}"

View File

@ -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

View File

@ -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": []
}

View File

@ -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"
}
]
}

View File

@ -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_<hostname>` where `<hostname>` 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"

View File

@ -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"

View File

@ -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 <<EOF >/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 <<EOF >/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

View File

@ -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 <<EOF >/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 <<EOF
port 6379
bind 127.0.0.1
protected-mode yes
save ""
maxmemory 256mb
maxmemory-policy allkeys-lru
EOF
systemctl enable -q --now valkey-server
systemctl restart valkey-server
msg_ok "Configured Valkey"
msg_info "Creating Database"
# Configure PostgreSQL to allow local password auth for databasus
PG_HBA="/etc/postgresql/17/main/pg_hba.conf"
if ! grep -q "databasus" "$PG_HBA"; then
sed -i '/^local\s*all\s*all/i local databasus postgres trust' "$PG_HBA"
sed -i '/^host\s*all\s*all\s*127/i host databasus postgres 127.0.0.1/32 trust' "$PG_HBA"
systemctl reload postgresql
fi
$STD sudo -u postgres psql -c "CREATE DATABASE databasus;" 2>/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 <<EOF >/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 <<EOF >/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

View File

@ -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 <<EOF >/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 <<EOF >/etc/systemd/system/discourse.service

View File

@ -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

View File

@ -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> | <explanation>\n---\n<last 20 log lines>"
# - 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

View File

@ -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

View File

@ -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?)" ;;

226
tools/addon/cronmaster.sh Normal file
View File

@ -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 <<EOF >"$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 <<EOF >"$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

374
tools/addon/jellystat.sh Normal file
View File

@ -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 <<EOF >"$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 <<EOF >"$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 <<EOF >"$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

View File

@ -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

View File

@ -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 <<EOF
Usage: ${SCRIPT_NAME} [OPTIONS]
Install or remove the Proxmox startup dependency-check hook system.
Options:
--install Install/update hookscript automation (default)
--uninstall Remove automation and cleanup hookscript assignments
--status Show current installation state
--help, -h Show this help message
EOF
}
function ensure_supported_pve {
if ! command -v pveversion >/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/tcp/${host}/${port}" >/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_<host>_<port>. 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