Compare commits

...

39 Commits

Author SHA1 Message Date
CanbiZ (MickLesk)
1aa26290ae replace msg_info with msg_warn 2026-03-26 16:23:28 +01:00
CanbiZ (MickLesk)
ef425844ae Add CDN mirror fallback for package updates
Improve robustness of package updates and installs by adding CDN/mirror fallback logic for Alpine and Debian/Ubuntu workflows.

- misc/alpine-install.func: update_os() now attempts apk -U upgrade and, on failure, iterates a shuffled list of alternate Alpine mirrors, writes /etc/apk/repositories, tests the mirror and falls back with clear warnings/errors if none succeed.

- misc/build.func: when apk add inside an Alpine container fails, run an in-container mirror scan that rewrites /etc/apk/repositories and retries apk update/add against multiple mirrors. Added a check to inject public DNS (8.8.8.8/1.1.1.1) into containers when APT repo DNS resolution fails.

- misc/build.func (Debian/Ubuntu path): expanded apt-get failure handling to detect the failing mirror, then run a multi-phase mirror scanner (global → primary → regional), detect hash/SSL errors, and prompt the user for a custom mirror if automatic attempts fail.

- misc/install.func: added apt_update_safe() implementing the apt-get update fallback logic (mirror lists, regional selection by timezone, reachable scanning, hash/SSL detection, and user prompt), returning failure if all mirrors fail.

These changes aim to mitigate CDN outages, broken CDNs/mirrors, or DNS issues during container builds and OS updates, improving reliability and providing clear messages and fallbacks when automatic fixes are exhausted.
2026-03-26 16:20:42 +01:00
community-scripts-pr-app[bot]
d12a0f1701 Update CHANGELOG.md (#13315)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-26 15:08:09 +00:00
CanbiZ (MickLesk)
fbe5b57c76 core/tools: replace generic return 1 exit_codes with more specific exit_codes (#13311) 2026-03-26 16:07:38 +01:00
community-scripts-pr-app[bot]
b14dfccc99 Update CHANGELOG.md (#13314)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-26 15:07:06 +00:00
CanbiZ (MickLesk)
38a283e549 update(frigate): bump to v0.17.1 and fix OpenVino model build (#13304) 2026-03-26 16:06:36 +01:00
community-scripts-pr-app[bot]
d06a70819d Update CHANGELOG.md (#13307)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-26 11:35:20 +00:00
community-scripts-pr-app[bot]
53e73e2f1a Update CHANGELOG.md (#13306)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-26 11:34:51 +00:00
Tom Frenzel
ee66508879 SparkyFitness: add garmin microservice (#12642)
* feat(sparkyfitness): add garmin microservice

* chore(sparkyfitness): add note to json

* chore(sparkyfitness): set garmin url

* feat(sparkyfitness): add garmin install option to update menu

* fix(sparkyfitness): typo

* chore(sparkyfitness): streamline naming and refactor menu

* feat: add sparlkyfitness garmin addon

* fix: remove EOF

* fix: update uninstall

* chore: update telemetry

* fix: app name

* chore: update env vars

* fix: typo

* chore: update default port

* fix: message format

* fix: update uninstall

* fix: rename functions

* fix: remove custom header_info

* fix: add header file

* chore: revert function naming
2026-03-26 12:34:28 +01:00
community-scripts-pr-app[bot]
83cfa0b5b4 Update CHANGELOG.md (#13305)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-26 11:24:51 +00:00
CanbiZ (MickLesk)
0a458c0a11 Immich: Bump to 2.6.2 | use start.sh in service, ensure DB_HOSTNAME in .env | Fix Rights Issue with ZFS Shares (#13199)
* fix(immich): use start.sh in service, ensure DB_HOSTNAME in .env

* Bump Immich to v2.6.2 and adjust chown handling

Update Immich release references from v2.6.1 to v2.6.2 in ct/immich.sh and install/immich-install.sh. Replace broad recursive chown -R on the install dir with a safer approach that avoids recursing into the upload directory (which may be a mounted volume with restricted permissions): set ownership on the install dir itself, chown each top-level entry except 'upload', and attempt to chown the upload path while ignoring errors. Also adjust ordering for /var/log/immich chown to avoid permission issues when enabling services.
2026-03-26 12:24:26 +01:00
community-scripts-pr-app[bot]
6703fca0e4 Update CHANGELOG.md (#13301)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-26 09:12:14 +00:00
CanbiZ (MickLesk)
42fbf1afc5 core: use /usr/bin/install to prevent function shadowing (#13299) 2026-03-26 10:11:47 +01:00
community-scripts-pr-app[bot]
d1c5b03fa7 Update CHANGELOG.md (#13298)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-26 09:04:24 +00:00
CanbiZ (MickLesk)
b9a39db667 fix(tools.func): pin npm to 11.11.0 to work around Node.js 22.22.2 regression (#13296)
Node.js 22.22.2 ships with a broken npm self-upgrade path where 'npm install -g npm@latest' fails with MODULE_NOT_FOUND for promise-retry. Pin to npm@11.11.0 as a known-good version until the upstream issue is resolved. Ref: nodejs/node#62425, npm/cli#9151
2026-03-26 10:04:00 +01:00
community-scripts-pr-app[bot]
0872f086ef Update CHANGELOG.md (#13297)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-26 08:54:51 +00:00
CanbiZ (MickLesk)
4eecca8aea fix(tools.func): use absolute path for install in setup_uv
Using bare 'install' command gets shadowed when scripts define their own install() function, causing setup_uv to hang. Use /usr/bin/install instead.
2026-03-26 09:54:17 +01:00
CanbiZ (MickLesk)
d915dee103 Update website link from .com to .org 2026-03-25 17:52:46 +01:00
CanbiZ (MickLesk)
97bf744e96 fix typo (org instead of com) 2026-03-25 17:48:03 +01:00
community-scripts-pr-app[bot]
7425f5d8fe Update CHANGELOG.md (#13284)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-25 12:06:51 +00:00
CanbiZ (MickLesk)
53bc492fdb Make shell command substitutions safe with || true (#13279)
Add defensive fallbacks (|| true) to multiple command substitutions to prevent non-zero exits when commands produce no output or are unavailable. Changes touch misc/api.func, misc/build.func and misc/tools.func and cover places like lspci, /proc/cpuinfo parsing, /etc/os-release reads, hostname -I usage, grep reads from vars files and maps, pct config parsing, storage/template lookups, tool version detection, NVIDIA driver version extraction, and MeiliSearch config parsing. These edits do not change functional behavior aside from ensuring the scripts continue running (variables will be empty) instead of failing in stricter shells or when commands return non-zero status.
2026-03-25 13:06:21 +01:00
CanbiZ (MickLesk)
de356fa8b6 set gawk 2026-03-25 08:51:06 +01:00
community-scripts-pr-app[bot]
00c538dc3b Update CHANGELOG.md (#13278)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-25 07:00:00 +00:00
CanbiZ (MickLesk)
7c4882384f komodo: migrate env vars to v2 and update source (#13262)
Update Komodo addon script: switch source GitHub URL to moghtech, create a timestamped backup of the compose env before updating, and add migrations for Komodo v2. Migrate image tag from 'latest' to ':2', rename DB credential variables (KOMODO_DB_* -> KOMODO_DATABASE_*), remove the deprecated KOMODO_PASSKEY, and ensure COMPOSE_KOMODO_BACKUPS_PATH is set. Adjust install routine to stop generating/setting PASSKEY and to use the new DATABASE variable names.
2026-03-25 07:59:27 +01:00
community-scripts-pr-app[bot]
8e4d174a65 Update .app files (#13271)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-24 20:51:26 +01:00
community-scripts-pr-app[bot]
d7112450c7 Update CHANGELOG.md (#13272)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-24 19:51:05 +00:00
CanbiZ (MickLesk)
caf03fe274 chore: replace helper-scripts.com with community-scripts.com (#13244) 2026-03-24 20:50:40 +01:00
community-scripts-pr-app[bot]
e731ddf61d Update CHANGELOG.md (#13270)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-24 19:50:04 +00:00
CanbiZ (MickLesk)
86f5c48fc2 Remove: Booklore (#13265) 2026-03-24 20:49:40 +01:00
community-scripts-pr-app[bot]
d1d786cbc7 Update CHANGELOG.md (#13259)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-24 14:54:30 +00:00
CanbiZ (MickLesk)
1c3c223e51 Turnkey: modernize turnkey.sh with shared libraries (#13242)
* refactor(turnkey): modernize turnkey.sh with shared libraries and telemetry

- Source core.func, error_handler.func, api.func instead of custom error/msg functions
- Replace custom error_exit/warn/info/msg with msg_info/msg_ok/msg_error/msg_warn
- Upgrade validate_container_id to cluster-aware (pvesh + all-node config check)
- Add diagnostics_check() and telemetry (post_to_api / post_update_to_api)
- Add pve_check, shell_check, root_check for environment validation
- Use proper EXIT trap for cleanup (destroy container on error, restart monitor)
- Improve quoting throughout (PCT_OPTIONS as array, quoted variables)
- Secure credentials file with chmod 600
- Use exit_script for user cancellations (consistent with other scripts)

* fix(turnkey): replace diagnostics_check with inline config read

diagnostics_check() is defined in build.func which is not sourced.
Read the diagnostics config file directly instead — respects existing
user preference without prompting (turnkey has no settings menu).

* bump hardcoded names to dynamic list

* Preserve telemetry type and report failures

Respect a pre-set TELEMETRY_TYPE in misc/api.func and use it in the API payload instead of the hardcoded "lxc". In turnkey/turnkey.sh, set TELEMETRY_TYPE="turnkey" for turnkey installs and enhance turnkey_cleanup() to report failed installs to telemetry (calls post_update_to_api "failed" with the exit code when POST_TO_API_DONE is true and POST_UPDATE_DONE is not), then destroy the failed container. These changes ensure correct telemetry type propagation and that failed turnkey deployments are reported.

---------

Co-authored-by: Slaviša Arežina <58952836+tremor021@users.noreply.github.com>
2026-03-24 15:54:01 +01:00
CanbiZ (MickLesk)
0980a85021 qf typo 2026-03-24 15:10:33 +01:00
community-scripts-pr-app[bot]
81547bb7a1 Update CHANGELOG.md (#13255)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-24 10:51:59 +00:00
push-app-to-main[bot]
c62e1ba882 Homebrew (Addon) (#13249)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-03-24 11:51:35 +01:00
community-scripts-pr-app[bot]
201a26a19e Update .app files (#13254)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-24 11:51:32 +01:00
community-scripts-pr-app[bot]
1dda554e40 Update CHANGELOG.md (#13253)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-24 10:49:32 +00:00
push-app-to-main[bot]
6b1b255ff6 NextExplorer (#13252)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-03-24 11:49:03 +01:00
community-scripts-pr-app[bot]
5d2fea107d Update CHANGELOG.md (#13250)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-24 08:58:07 +00:00
CanbiZ (MickLesk)
86c658909a Classify exit-1 errors & guard telemetry
Analyze logs for generic exit code 1 and export an ERROR_CATEGORY_OVERRIDE so telemetry receives a more accurate error category (apt, oom, network, storage, dependency). Preserve any existing TELEMETRY_TYPE when posting updates. Add defense-in-depth by disabling strict error traps before running grep/sed log analysis to avoid spurious error_handler invocations. Mark successful installs with INSTALL_COMPLETE and update the error handler to only report a successful "done" telemetry state when INSTALL_COMPLETE is explicitly set, preventing false-positive success reports from early zero-exit exits.
2026-03-24 09:57:43 +01:00
64 changed files with 1949 additions and 974 deletions

View File

@@ -21,7 +21,7 @@ jobs:
const message = `Hello, it looks like you are referencing the **old tteck repo**.
This repository is no longer used for active scripts.
**Please update your bookmarks** and use: [https://helper-scripts.com](https://helper-scripts.com)
**Please update your bookmarks** and use: [https://community-scripts.com](https://community-scripts.com)
Also make sure your Bash command starts with:
\`\`\`bash

View File

@@ -426,6 +426,68 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-03-26
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Immich: Bump to 2.6.2 | use start.sh in service, ensure DB_HOSTNAME in .env | Fix Rights Issue with ZFS Shares [@MickLesk](https://github.com/MickLesk) ([#13199](https://github.com/community-scripts/ProxmoxVE/pull/13199))
- #### ✨ New Features
- Frigate: bump to v0.17.1 & change build order [@MickLesk](https://github.com/MickLesk) ([#13304](https://github.com/community-scripts/ProxmoxVE/pull/13304))
- SparkyFitness: add garmin microservice as addon [@tomfrenzel](https://github.com/tomfrenzel) ([#12642](https://github.com/community-scripts/ProxmoxVE/pull/12642))
### 💾 Core
- #### 🐞 Bug Fixes
- tools.func: pin npm to 11.11.0 to work around Node.js 22.22.2 regression [@MickLesk](https://github.com/MickLesk) ([#13296](https://github.com/community-scripts/ProxmoxVE/pull/13296))
- #### ✨ New Features
- core/tools: replace generic return 1 exit_codes with more specific exit_codes [@MickLesk](https://github.com/MickLesk) ([#13311](https://github.com/community-scripts/ProxmoxVE/pull/13311))
- #### 🔧 Refactor
- core: use /usr/bin/install to prevent function shadowing [@MickLesk](https://github.com/MickLesk) ([#13299](https://github.com/community-scripts/ProxmoxVE/pull/13299))
## 2026-03-25
### 🚀 Updated Scripts
- #### ✨ New Features
- Komodo v2: migrate env vars to v2 and update source [@MickLesk](https://github.com/MickLesk) ([#13262](https://github.com/community-scripts/ProxmoxVE/pull/13262))
### 💾 Core
- #### 🔧 Refactor
- core: make shell command substitutions safe with || true [@MickLesk](https://github.com/MickLesk) ([#13279](https://github.com/community-scripts/ProxmoxVE/pull/13279))
## 2026-03-24
### 🆕 New Scripts
- Homebrew (Addon) ([#13249](https://github.com/community-scripts/ProxmoxVE/pull/13249))
- NextExplorer ([#13252](https://github.com/community-scripts/ProxmoxVE/pull/13252))
### 🚀 Updated Scripts
- #### ✨ New Features
- Turnkey: modernize turnkey.sh with shared libraries [@MickLesk](https://github.com/MickLesk) ([#13242](https://github.com/community-scripts/ProxmoxVE/pull/13242))
- #### 🔧 Refactor
- chore: replace helper-scripts.com with community-scripts.com [@MickLesk](https://github.com/MickLesk) ([#13244](https://github.com/community-scripts/ProxmoxVE/pull/13244))
### 🗑️ Deleted Scripts
- Remove: Booklore [@MickLesk](https://github.com/MickLesk) ([#13265](https://github.com/community-scripts/ProxmoxVE/pull/13265))
## 2026-03-23
### 🚀 Updated Scripts

View File

@@ -5,7 +5,7 @@
<p><em>A Community Legacy in Memory of @tteck</em></p>
<p>
<a href="https://helper-scripts.com">
<a href="https://community-scripts.org">
<img src="https://img.shields.io/badge/🌐_Website-Visit-4c9b3f?style=for-the-badge&labelColor=2d3748" alt="Website" />
</a>
<a href="https://discord.gg/3AnUqsXnmK">

View File

@@ -35,6 +35,8 @@ function update_script() {
read -r -p "${TAB}Migrate update function now? [y/N]: " CONFIRM
if [[ ! "${CONFIRM,,}" =~ ^(y|yes)$ ]]; then
msg_warn "Migration skipped. The old update will continue to work for now."
msg_warn "⚠️ Komodo v2 uses :2 image tags. The :latest tag is deprecated and will not receive v2 updates."
msg_warn "Please migrate to the addon script to receive Komodo v2."
msg_info "Updating ${APP} (legacy)"
COMPOSE_FILE=$(find /opt/komodo -maxdepth 1 -type f -name '*.compose.yaml' ! -name 'compose.env' | head -n1)
if [[ -z "$COMPOSE_FILE" ]]; then

View File

@@ -1,113 +0,0 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# 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/booklore-app/BookLore
APP="BookLore"
var_tags="${var_tags:-books;library}"
var_cpu="${var_cpu:-3}"
var_ram="${var_ram:-3072}"
var_disk="${var_disk:-7}"
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 [[ ! -d /opt/booklore ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "booklore" "booklore-app/BookLore"; then
JAVA_VERSION="25" setup_java
NODE_VERSION="22" setup_nodejs
setup_mariadb
setup_yq
ensure_dependencies ffmpeg
msg_info "Stopping Service"
systemctl stop booklore
msg_ok "Stopped Service"
if grep -qE "^BOOKLORE_(DATA_PATH|BOOKDROP_PATH|BOOKS_PATH|PORT)=" /opt/booklore_storage/.env 2>/dev/null; then
msg_info "Migrating old environment variables"
sed -i 's/^BOOKLORE_DATA_PATH=/APP_PATH_CONFIG=/g' /opt/booklore_storage/.env
sed -i 's/^BOOKLORE_BOOKDROP_PATH=/APP_BOOKDROP_FOLDER=/g' /opt/booklore_storage/.env
sed -i '/^BOOKLORE_BOOKS_PATH=/d' /opt/booklore_storage/.env
sed -i '/^BOOKLORE_PORT=/d' /opt/booklore_storage/.env
msg_ok "Migrated old environment variables"
fi
msg_info "Backing up old installation"
mv /opt/booklore /opt/booklore_bak
msg_ok "Backed up old installation"
fetch_and_deploy_gh_release "booklore" "booklore-app/BookLore" "tarball"
msg_info "Building Frontend"
cd /opt/booklore/booklore-ui
$STD npm install --force
$STD npm run build --configuration=production
msg_ok "Built Frontend"
msg_info "Embedding Frontend into Backend"
mkdir -p /opt/booklore/booklore-api/src/main/resources/static
cp -r /opt/booklore/booklore-ui/dist/booklore/browser/* /opt/booklore/booklore-api/src/main/resources/static/
msg_ok "Embedded Frontend into Backend"
msg_info "Building Backend"
cd /opt/booklore/booklore-api
APP_VERSION=$(get_latest_github_release "booklore-app/BookLore")
yq eval ".app.version = \"${APP_VERSION}\"" -i src/main/resources/application.yaml
$STD ./gradlew clean build -x test --no-daemon
mkdir -p /opt/booklore/dist
JAR_PATH=$(find /opt/booklore/booklore-api/build/libs -maxdepth 1 -type f -name "booklore-api-*.jar" ! -name "*plain*" | head -n1)
if [[ -z "$JAR_PATH" ]]; then
msg_error "Backend JAR not found"
exit
fi
cp "$JAR_PATH" /opt/booklore/dist/app.jar
msg_ok "Built Backend"
if systemctl is-active --quiet nginx 2>/dev/null; then
msg_info "Removing Nginx (no longer needed)"
systemctl disable --now nginx
$STD apt-get purge -y nginx nginx-common
msg_ok "Removed Nginx"
fi
if ! grep -q "^SERVER_PORT=" /opt/booklore_storage/.env 2>/dev/null; then
echo "SERVER_PORT=6060" >>/opt/booklore_storage/.env
fi
sed -i 's|ExecStart=.*|ExecStart=/usr/bin/java -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError -jar /opt/booklore/dist/app.jar|' /etc/systemd/system/booklore.service
systemctl daemon-reload
msg_info "Starting Service"
systemctl start booklore
rm -rf /opt/booklore_bak
msg_ok "Started Service"
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}:6060${CL}"

View File

@@ -1,6 +0,0 @@
____ __ __
/ __ )____ ____ / /__/ / ____ ________
/ __ / __ \/ __ \/ //_/ / / __ \/ ___/ _ \
/ /_/ / /_/ / /_/ / ,< / /___/ /_/ / / / __/
/_____/\____/\____/_/|_/_____/\____/_/ \___/

6
ct/headers/nextexplorer Normal file
View File

@@ -0,0 +1,6 @@
__ ______ __
____ ___ _ __/ /_/ ____/ ______ / /___ ________ _____
/ __ \/ _ \| |/_/ __/ __/ | |/_/ __ \/ / __ \/ ___/ _ \/ ___/
/ / / / __/> </ /_/ /____> </ /_/ / / /_/ / / / __/ /
/_/ /_/\___/_/|_|\__/_____/_/|_/ .___/_/\____/_/ \___/_/
/_/

View File

@@ -73,7 +73,7 @@ function update_script() {
$STD curl -fsSL https://github.com/filebrowser/filebrowser/releases/download/v2.23.0/linux-amd64-filebrowser.tar.gz | tar -xzv -C /usr/local/bin
$STD filebrowser config init -a '0.0.0.0'
$STD filebrowser config set -a '0.0.0.0'
$STD filebrowser users add admin helper-scripts.com --perm.admin
$STD filebrowser users add admin community-scripts.org --perm.admin
msg_ok "Installed FileBrowser"
msg_info "Creating Service"
@@ -93,7 +93,7 @@ WantedBy=default.target" >$service_path
msg_ok "Completed successfully!\n"
echo -e "FileBrowser should be reachable by going to the following URL.
${BL}http://$LOCAL_IP:8080${CL} admin|helper-scripts.com\n"
${BL}http://$LOCAL_IP:8080${CL} admin|community-scripts.org\n"
exit
fi
}

View File

@@ -109,7 +109,7 @@ EOF
msg_ok "Image-processing libraries up to date"
fi
RELEASE="v2.6.1"
RELEASE="v2.6.2"
if check_for_gh_release "Immich" "immich-app/immich" "${RELEASE}" "each release is tested individually before the version is updated. Please do not open issues for this"; then
if [[ $(cat ~/.immich) > "2.5.1" ]]; then
msg_info "Enabling Maintenance Mode"
@@ -214,7 +214,10 @@ EOF
cd "$SRC_DIR"/machine-learning
mkdir -p "$ML_DIR"
chown -R immich:immich "$INSTALL_DIR"
# chown excluding upload dir contents (may be a mount with restricted permissions)
chown immich:immich "$INSTALL_DIR"
find "$INSTALL_DIR" -maxdepth 1 -mindepth 1 ! -name upload -exec chown -R immich:immich {} +
chown immich:immich "${UPLOAD_DIR:-$INSTALL_DIR/upload}" 2>/dev/null || true
chown immich:immich ./uv.lock
export VIRTUAL_ENV="${ML_DIR}"/ml-venv
export UV_HTTP_TIMEOUT=300
@@ -263,7 +266,20 @@ EOF
[[ ! -f /usr/bin/immich ]] && ln -sf "$APP_DIR"/cli/bin/immich /usr/bin/immich
[[ ! -f /usr/bin/immich-admin ]] && ln -sf "$APP_DIR"/bin/immich-admin /usr/bin/immich-admin
chown -R immich:immich "$INSTALL_DIR"
if ! grep -q '^DB_HOSTNAME=' "$INSTALL_DIR"/.env; then
sed -i '/^DB_DATABASE_NAME/a DB_HOSTNAME=127.0.0.1' "$INSTALL_DIR"/.env
fi
if grep -q 'ExecStart=/usr/bin/node' /etc/systemd/system/immich-web.service; then
sed -i '/^EnvironmentFile=/d' /etc/systemd/system/immich-web.service
sed -i "s|^ExecStart=.*|ExecStart=${APP_DIR}/bin/start.sh|" /etc/systemd/system/immich-web.service
systemctl daemon-reload
fi
# chown excluding upload dir contents (may be a mount with restricted permissions)
chown immich:immich "$INSTALL_DIR"
find "$INSTALL_DIR" -maxdepth 1 -mindepth 1 ! -name upload -exec chown -R immich:immich {} +
chown immich:immich "${UPLOAD_DIR:-$INSTALL_DIR/upload}" 2>/dev/null || true
if [[ "${MAINT_MODE:-0}" == 1 ]]; then
msg_info "Disabling Maintenance Mode"
cd /opt/immich/app/bin

View File

@@ -39,6 +39,8 @@ function update_script() {
read -r -p "${TAB}Migrate update function now? [y/N]: " CONFIRM
if [[ ! "${CONFIRM,,}" =~ ^(y|yes)$ ]]; then
msg_warn "Migration skipped. The old update will continue to work for now."
msg_warn "⚠️ Komodo v2 uses :2 image tags. The :latest tag is deprecated and will not receive v2 updates."
msg_warn "Please migrate to the addon script to receive Komodo v2."
msg_info "Updating ${APP} (legacy)"
COMPOSE_FILE=$(find /opt/komodo -maxdepth 1 -type f -name '*.compose.yaml' ! -name 'compose.env' | head -n1)
if [[ -z "$COMPOSE_FILE" ]]; then

76
ct/nextexplorer.sh Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: vhsdream
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/nxzai/nextExplorer
APP="nextExplorer"
var_tags="${var_tags:-files;documents}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-3072}"
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 [[ ! -d /opt/nextExplorer ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
NODE_VERSION="24" setup_nodejs
if check_for_gh_release "nextExplorer" "nxzai/nextExplorer"; then
msg_info "Stopping nextExplorer"
$STD systemctl stop nextexplorer
msg_ok "Stopped nextExplorer"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "nextExplorer" "nxzai/nextExplorer" "tarball" "latest" "/opt/nextExplorer"
msg_info "Updating nextExplorer"
APP_DIR="/opt/nextExplorer/app"
mkdir -p "$APP_DIR"
cd /opt/nextExplorer
export NODE_ENV=production
$STD npm ci --omit=dev --workspace backend
mv node_modules "$APP_DIR"
mv backend/{src,package.json} "$APP_DIR"
unset NODE_ENV
export NODE_ENV=development
$STD npm ci --workspace frontend
$STD npm run -w frontend build -- --sourcemap false
unset NODE_ENV
mv frontend/dist/ "$APP_DIR"/src/public
chown -R explorer:explorer "$APP_DIR" /etc/nextExplorer
sed -i "\|version|s|$(jq -cr '.version' ${APP_DIR}/package.json)|$(cat ~/.nextexplorer)|" "$APP_DIR"/package.json
sed -i 's/app.js/server.js/' /etc/systemd/system/nextexplorer.service && systemctl daemon-reload
msg_ok "Updated nextExplorer"
msg_info "Starting nextExplorer"
$STD systemctl start nextexplorer
msg_ok "Started nextExplorer"
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}:3000${CL}"

View File

@@ -68,7 +68,7 @@ function update_script() {
$STD curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
$STD filebrowser config init -a '0.0.0.0'
$STD filebrowser config set -a '0.0.0.0'
$STD filebrowser users add admin helper-scripts.com --perm.admin
$STD filebrowser users add admin community-scripts.org --perm.admin
msg_ok "Installed FileBrowser"
msg_info "Creating Service"
@@ -90,7 +90,7 @@ EOF
msg_ok "Completed successfully!\n"
echo -e "FileBrowser should be reachable by going to the following URL.
${BL}http://$LOCAL_IP:8080${CL} admin|helper-scripts.com\n"
${BL}http://$LOCAL_IP:8080${CL} admin|community-scripts.org\n"
exit
fi
if [ "$UPD" == "4" ]; then

View File

@@ -50,7 +50,7 @@ function update_script() {
/opt/semaphore/config.json
SEM_PW=$(cat ~/semaphore.creds)
systemctl start semaphore
$STD semaphore user add --admin --login admin --email admin@helper-scripts.com --name Administrator --password "${SEM_PW}" --config /opt/semaphore/config.json
$STD semaphore user add --admin --login admin --email admin@community-scripts.org --name Administrator --password "${SEM_PW}" --config /opt/semaphore/config.json
msg_ok "Moved from BoltDB to SQLite"
fi

View File

@@ -62,10 +62,10 @@ expect "Email address"
send "\r"
expect "Password"
send "helper-scripts.com\r"
send "community-scripts.org\r"
expect "Password (again)"
send "helper-scripts.com\r"
send "community-scripts.org\r"
expect eof
EOF

View File

@@ -58,7 +58,7 @@ service:
use_prerelease: false
dashboard:
icon: https://raw.githubusercontent.com/community-scripts/ProxmoxVE/refs/heads/main/misc/images/logo.png
icon_link_to: https://helper-scripts.com/
icon_link_to: https://community-scripts.org/
web_url: https://github.com/community-scripts/ProxmoxVE/releases
EOF
msg_ok "Setup Config"

View File

@@ -1,92 +0,0 @@
#!/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/booklore-app/BookLore
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 ffmpeg
msg_ok "Installed Dependencies"
JAVA_VERSION="25" setup_java
NODE_VERSION="22" setup_nodejs
setup_mariadb
setup_yq
MARIADB_DB_NAME="booklore_db" MARIADB_DB_USER="booklore_user" MARIADB_DB_EXTRA_GRANTS="GRANT SELECT ON \`mysql\`.\`time_zone_name\`" setup_mariadb_db
fetch_and_deploy_gh_release "booklore" "booklore-app/BookLore" "tarball"
msg_info "Building Frontend"
cd /opt/booklore/booklore-ui
$STD npm install --force
$STD npm run build --configuration=production
msg_ok "Built Frontend"
msg_info "Embedding Frontend into Backend"
mkdir -p /opt/booklore/booklore-api/src/main/resources/static
cp -r /opt/booklore/booklore-ui/dist/booklore/browser/* /opt/booklore/booklore-api/src/main/resources/static/
msg_ok "Embedded Frontend into Backend"
msg_info "Creating Environment"
mkdir -p /opt/booklore_storage/{data,books,bookdrop}
cat <<EOF >/opt/booklore_storage/.env
# Database Configuration
DATABASE_URL=jdbc:mariadb://localhost:3306/${MARIADB_DB_NAME}
DATABASE_USERNAME=${MARIADB_DB_USER}
DATABASE_PASSWORD=${MARIADB_DB_PASS}
# App Configuration (Spring Boot mapping from app.* properties)
APP_PATH_CONFIG=/opt/booklore_storage/data
APP_BOOKDROP_FOLDER=/opt/booklore_storage/bookdrop
SERVER_PORT=6060
EOF
msg_ok "Created Environment"
msg_info "Building Backend"
cd /opt/booklore/booklore-api
APP_VERSION=$(get_latest_github_release "booklore-app/BookLore")
yq eval ".app.version = \"${APP_VERSION}\"" -i src/main/resources/application.yaml
$STD ./gradlew clean build -x test --no-daemon
mkdir -p /opt/booklore/dist
JAR_PATH=$(find /opt/booklore/booklore-api/build/libs -maxdepth 1 -type f -name "booklore-api-*.jar" ! -name "*plain*" | head -n1)
if [[ -z "$JAR_PATH" ]]; then
msg_error "Backend JAR not found"
exit 153
fi
cp "$JAR_PATH" /opt/booklore/dist/app.jar
msg_ok "Built Backend"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/booklore.service
[Unit]
Description=BookLore Java Service
After=network.target mariadb.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/booklore/dist
ExecStart=/usr/bin/java -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError -jar /opt/booklore/dist/app.jar
EnvironmentFile=/opt/booklore_storage/.env
SuccessExitStatus=143
TimeoutStopSec=10
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now booklore
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc

View File

@@ -110,7 +110,7 @@ export AUTOGRAPH_VERBOSITY=0
export GLOG_minloglevel=3
export GLOG_logtostderr=0
fetch_and_deploy_gh_release "frigate" "blakeblackshear/frigate" "tarball" "v0.17.0" "/opt/frigate"
fetch_and_deploy_gh_release "frigate" "blakeblackshear/frigate" "tarball" "v0.17.1" "/opt/frigate"
msg_info "Building Nginx"
$STD bash /opt/frigate/docker/main/build_nginx.sh
@@ -182,23 +182,6 @@ cp /opt/frigate/audio-labelmap.txt /audio-labelmap.txt
rm -f /tmp/yamnet.tar.gz
msg_ok "Downloaded Audio Model"
msg_info "Installing HailoRT Runtime"
$STD bash /opt/frigate/docker/main/install_hailort.sh
cp -a /opt/frigate/docker/main/rootfs/. /
sed -i '/^.*unset DEBIAN_FRONTEND.*$/d' /opt/frigate/docker/main/install_deps.sh
echo "libedgetpu1-max libedgetpu/accepted-eula boolean true" | debconf-set-selections
echo "libedgetpu1-max libedgetpu/install-confirm-max boolean true" | debconf-set-selections
echo 'force-overwrite' >/etc/dpkg/dpkg.cfg.d/force-overwrite
$STD bash /opt/frigate/docker/main/install_deps.sh
rm -f /etc/dpkg/dpkg.cfg.d/force-overwrite
$STD pip3 install -U /wheels/*.whl
ldconfig
msg_ok "Installed HailoRT Runtime"
msg_info "Installing MemryX Runtime"
$STD bash /opt/frigate/docker/main/install_memryx.sh
msg_ok "Installed MemryX Runtime"
msg_info "Installing OpenVino"
$STD pip3 install -r /opt/frigate/docker/main/requirements-ov.txt
msg_ok "Installed OpenVino"
@@ -228,6 +211,23 @@ else
msg_warn "OpenVino build failed (CPU may not support required instructions). Frigate will use CPU model."
fi
msg_info "Installing HailoRT Runtime"
$STD bash /opt/frigate/docker/main/install_hailort.sh
cp -a /opt/frigate/docker/main/rootfs/. /
sed -i '/^.*unset DEBIAN_FRONTEND.*$/d' /opt/frigate/docker/main/install_deps.sh
echo "libedgetpu1-max libedgetpu/accepted-eula boolean true" | debconf-set-selections
echo "libedgetpu1-max libedgetpu/install-confirm-max boolean true" | debconf-set-selections
echo 'force-overwrite' >/etc/dpkg/dpkg.cfg.d/force-overwrite
$STD bash /opt/frigate/docker/main/install_deps.sh
rm -f /etc/dpkg/dpkg.cfg.d/force-overwrite
$STD pip3 install -U /wheels/*.whl
ldconfig
msg_ok "Installed HailoRT Runtime"
msg_info "Installing MemryX Runtime"
$STD bash /opt/frigate/docker/main/install_memryx.sh
msg_ok "Installed MemryX Runtime"
msg_info "Building Frigate Application (Patience)"
cd /opt/frigate
$STD pip3 install -r /opt/frigate/docker/main/requirements-dev.txt

View File

@@ -35,7 +35,7 @@ PG_DB_NAME="healthchecks_db" PG_DB_USER="hc_user" PG_DB_PASS=$(openssl rand -bas
msg_info "Setup Keys (Admin / Secret)"
SECRET_KEY="$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | cut -c1-32)"
ADMIN_EMAIL="admin@helper-scripts.local"
ADMIN_EMAIL="admin@community-scripts.org"
ADMIN_PASSWORD="$PG_DB_PASS"
{
echo "healthchecks Admin Email: $ADMIN_EMAIL"

View File

@@ -295,7 +295,7 @@ ML_DIR="${APP_DIR}/machine-learning"
GEO_DIR="${INSTALL_DIR}/geodata"
mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "v2.6.1" "$SRC_DIR"
fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "v2.6.2" "$SRC_DIR"
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
NODE_VERSION="24" NODE_MODULE="pnpm@${PNPM_VERSION}" setup_nodejs
@@ -344,7 +344,11 @@ msg_ok "Installed Immich Server, Web and Plugin Components"
cd "$SRC_DIR"/machine-learning
$STD useradd -U -s /usr/sbin/nologin -r -M -d "$INSTALL_DIR" immich
mkdir -p "$ML_DIR" && chown -R immich:immich "$INSTALL_DIR"
mkdir -p "$ML_DIR"
# chown excluding upload dir contents (may be a mount with restricted permissions)
chown immich:immich "$INSTALL_DIR"
find "$INSTALL_DIR" -maxdepth 1 -mindepth 1 ! -name upload -exec chown -R immich:immich {} +
chown immich:immich "$UPLOAD_DIR" 2>/dev/null || true
export VIRTUAL_ENV="${ML_DIR}/ml-venv"
export UV_HTTP_TIMEOUT=300
if [[ -f ~/.openvino ]]; then
@@ -469,8 +473,7 @@ User=immich
Group=immich
UMask=0077
WorkingDirectory=${APP_DIR}
EnvironmentFile=${INSTALL_DIR}/.env
ExecStart=/usr/bin/node ${APP_DIR}/dist/main
ExecStart=${APP_DIR}/bin/start.sh
Restart=on-failure
SyslogIdentifier=immich-web
StandardOutput=append:/var/log/immich/web.log
@@ -500,7 +503,11 @@ StandardError=append:/var/log/immich/ml.log
[Install]
WantedBy=multi-user.target
EOF
chown -R immich:immich "$INSTALL_DIR" /var/log/immich
chown -R immich:immich /var/log/immich
# chown excluding upload dir contents (may be a mount with restricted permissions)
chown immich:immich "$INSTALL_DIR"
find "$INSTALL_DIR" -maxdepth 1 -mindepth 1 ! -name upload -exec chown -R immich:immich {} +
chown immich:immich "$UPLOAD_DIR" 2>/dev/null || true
systemctl enable -q --now immich-ml.service immich-web.service
msg_ok "Modified user, created env file, scripts and services"

View File

@@ -17,7 +17,7 @@ fetch_and_deploy_gh_release "inspircd" "inspircd/inspircd" "binary" "latest" "/o
msg_info "Configuring InspIRCd"
cat <<EOF >/etc/inspircd/inspircd.conf
<define name="networkDomain" value="helper-scripts.com">
<define name="networkDomain" value="community-scripts.org">
<define name="networkName" value="Proxmox VE Helper-Scripts">
<server

View File

@@ -55,10 +55,10 @@ $STD expect <<EOF
set timeout -1
log_user 0
spawn bin/console kimai:user:create admin admin@helper-scripts.com ROLE_SUPER_ADMIN
spawn bin/console kimai:user:create admin admin@community-scripts.org ROLE_SUPER_ADMIN
expect "Please enter the password:"
send "helper-scripts.com\r"
send "community-scripts.org\r"
expect eof
EOF

View File

@@ -35,7 +35,7 @@ fetch_and_deploy_gh_release "kometa-quickstart" "Kometa-Team/Quickstart" "tarbal
msg_info "Installing Kometa Quickstart"
cd /opt/kometa-quickstart
$STD uv venv /opt/kometa-quickstart/.venv
$STD /opt/kometa-quickstart/.venv/bin/python -m pip install -r requirements.txt
$STD uv pip install -r requirements.txt -p /opt/kometa-quickstart/.venv/bin/python
msg_ok "Installed Kometa Quickstart"
msg_info "Creating Service"

View File

@@ -33,7 +33,7 @@ $STD yarn config set ignore-engines true
$STD yarn install
$STD yarn run production
$STD php artisan key:generate
$STD php artisan setup:production --email=admin@helper-scripts.com --password=helper-scripts.com --force
$STD php artisan setup:production --email=admin@community-scripts.org --password=community-scripts.org --force
chown -R www-data:www-data /opt/monica
chmod -R 775 /opt/monica/storage
echo "* * * * * root php /opt/monica/artisan schedule:run >> /dev/null 2>&1" >>/etc/crontab

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: vhsdream
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/nxzai/nextExplorer
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 \
ripgrep \
imagemagick \
ffmpeg \
libva-drm2 \
libva2 \
mesa-va-drivers \
vainfo
msg_ok "Installed Dependencies"
NODE_VERSION="24" setup_nodejs
fetch_and_deploy_gh_release "nextExplorer" "nxzai/nextExplorer" "tarball" "latest" "/opt/nextExplorer"
msg_info "Building nextExplorer"
APP_DIR="/opt/nextExplorer/app"
LOCAL_IP="$(hostname -I | awk '{print $1}')"
mkdir -p "$APP_DIR"
mkdir -p /etc/nextExplorer
cd /opt/nextExplorer
export NODE_ENV=production
$STD npm ci --omit=dev --workspace backend
mv node_modules "$APP_DIR"
mv backend/{src,package.json} "$APP_DIR"
unset NODE_ENV
export NODE_ENV=development
export NODE_OPTIONS="--max-old-space-size=2048"
$STD npm ci --workspace frontend
$STD npm run -w frontend build -- --sourcemap false
unset NODE_ENV
mv frontend/dist/ "$APP_DIR"/src/public
msg_ok "Built nextExplorer"
msg_info "Configuring nextExplorer"
SECRET=$(openssl rand -hex 32)
cat <<EOF >/etc/nextExplorer/.env
NODE_ENV=production
PORT=3000
VOLUME_ROOT=/mnt
CONFIG_DIR=/etc/nextExplorer
CACHE_DIR=/etc/nextExplorer/cache
# USER_ROOT=
PUBLIC_URL=${LOCAL_IP}:3000
# TRUST_PROXY=
# CORS_ORIGINS=
TERMINAL_ENABLED=false
LOG_LEVEL=info
DEBUG=false
ENABLE_HTTP_LOGGING=false
AUTH_ENABLED=true
AUTH_MODE=both
SESSION_SECRET="${SECRET}"
# AUTH_MAX_FAILED=
# AUTH_LOCK_MINUTES=
# AUTH_USER_EMAIL=
# AUTH_USER_PASSWORD=
# OIDC_ENABLED=
# OIDC_ISSUER=
# OIDC_AUTHORIZATION_URL=
# OIDC_TOKEN_URL=
# OIDC_USERINFO_URL=
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
# OIDC_CALLBACK_URL=
# OIDC_LOGOUT_URL=
# OIDC_SCOPES=
# OIDC_AUTO_CREATE_USERS=true
# SEARCH_DEEP=
# SEARCH_RIPGREP=
# SEARCH_MAX_FILESIZE=
# ONLYOFFICE_URL=
# ONLYOFFICE_SECRET=
# ONLYOFFICE_LANG=
# ONLYOFFICE_FORCE_SAVE=
# ONLYOFFICE_FILE_EXTENSIONS=
# COLLABORA_URL=
# COLLABORA_DISCOVERY_URL=
# COLLABORA_SECRET=
# COLLABORA_LANG=
# COLLABORA_FILE_EXTENSIONS=
SHOW_VOLUME_USAGE=true
# USER_DIR_ENABLED=
# SKIP_HOME=
# EDITOR_EXTENSIONS=
# FFMPEG_PATH=
# FFPROBE_PATH=
## Hardware acceleration
# FFMPEG_HWACCEL=vaapi
# FFMPEG_HWACCEL_DEVICE=/dev/dri/renderD128
# FFMPEG_HWACCEL_OUTPUT_FORMAT=nv12
FAVORITES_DEFAULT_ICON=outline.StarIcon
SHARES_ENABLED=true
# SHARES_TOKEN_LENGTH=10
# SHARES_MAX_PER_USER=100
# SHARES_DEFAULT_EXPIRY_DAYS=30
# SHARES_GUEST_SESSION_HOURS=24
# SHARES_ALLOW_PASSWORD=true
# SHARES_ALLOW_ANONYMOUS=true
EOF
chmod 600 /etc/nextExplorer/.env
$STD useradd -U -s /usr/sbin/nologin -m -d /home/explorer explorer
chown -R explorer:explorer "$APP_DIR" /etc/nextExplorer
sed -i "\|version|s|$(jq -cr '.version' ${APP_DIR}/package.json)|$(cat ~/.nextexplorer)|" "$APP_DIR"/package.json
msg_ok "Configured nextExplorer"
msg_info "Creating nextExplorer Service"
cat <<EOF >/etc/systemd/system/nextexplorer.service
[Unit]
Description=nextExplorer Service
After=network.target
[Service]
Type=simple
User=explorer
Group=explorer
WorkingDirectory=/opt/nextExplorer/app
EnvironmentFile=/etc/nextExplorer/.env
ExecStart=/usr/bin/node ./src/server.js
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
$STD systemctl enable -q --now nextexplorer
msg_ok "Created nextExplorer Service"
motd_ssh
customize
cleanup_lxc

View File

@@ -91,16 +91,16 @@ expect "Format: mongodb://*" {
send "$MONGO_CONNECTION_STRING\r"
}
expect "Administrator username" {
send "helper-scripts\r"
send "community-scripts\r"
}
expect "Administrator email address" {
send "helper-scripts@local.com\r"
send "admin@community-scripts.org\r"
}
expect "Password" {
send "helper-scripts\r"
send "community-scripts\r"
}
expect "Confirm Password" {
send "helper-scripts\r"
send "community-scripts\r"
}
expect eof
EOF

View File

@@ -60,7 +60,7 @@ read -r -p "${TAB3}Enter your ACME Email: " ACME_EMAIL_INPUT
yq -i "
.services.npmplus.environment |=
(map(select(. != \"TZ=*\" and . != \"ACME_EMAIL=*\" and . != \"INITIAL_ADMIN_EMAIL=*\" and . != \"INITIAL_ADMIN_PASSWORD=*\")) +
[\"TZ=$TZ_INPUT\", \"ACME_EMAIL=$ACME_EMAIL_INPUT\", \"INITIAL_ADMIN_EMAIL=admin@local.com\", \"INITIAL_ADMIN_PASSWORD=helper-scripts.com\"])
[\"TZ=$TZ_INPUT\", \"ACME_EMAIL=$ACME_EMAIL_INPUT\", \"INITIAL_ADMIN_EMAIL=admin@local.com\", \"INITIAL_ADMIN_PASSWORD=community-scripts.org\"])
" /opt/compose.yaml
msg_info "Building and Starting NPMplus (Patience)"

View File

@@ -99,7 +99,7 @@ PHOTOPRISM_DEBUG='false'
PHOTOPRISM_LOG_LEVEL='info'
# Site Info
PHOTOPRISM_SITE_CAPTION='https://Helper-Scripts.com'
PHOTOPRISM_SITE_CAPTION='https://community-scripts.org'
PHOTOPRISM_SITE_DESCRIPTION=''
PHOTOPRISM_SITE_AUTHOR=''
EOF

View File

@@ -40,7 +40,7 @@ cat <<EOF >/opt/semaphore/config.json
"access_key_encryption": "${SEM_KEY}"
}
EOF
$STD semaphore user add --admin --login admin --email admin@helper-scripts.com --name Administrator --password "${SEM_PW}" --config /opt/semaphore/config.json
$STD semaphore user add --admin --login admin --email admin@community-scripts.org --name Administrator --password "${SEM_PW}" --config /opt/semaphore/config.json
echo "${SEM_PW}" >~/semaphore.creds
msg_ok "Setup Semaphore"

View File

@@ -40,6 +40,7 @@ sed \
-e "s|^SPARKY_FITNESS_SERVER_HOST=.*|SPARKY_FITNESS_SERVER_HOST=localhost|" \
-e "s|^SPARKY_FITNESS_SERVER_PORT=.*|SPARKY_FITNESS_SERVER_PORT=3010|" \
-e "s|^SPARKY_FITNESS_FRONTEND_URL=.*|SPARKY_FITNESS_FRONTEND_URL=http://${LOCAL_IP}:80|" \
-e "s|^GARMIN_MICROSERVICE_URL=.*|GARMIN_MICROSERVICE_URL=http://${LOCAL_IP}:8000|" \
-e "s|^SPARKY_FITNESS_API_ENCRYPTION_KEY=.*|SPARKY_FITNESS_API_ENCRYPTION_KEY=$(openssl rand -hex 32)|" \
-e "s|^BETTER_AUTH_SECRET=.*|BETTER_AUTH_SECRET=$(openssl rand -hex 32)|" \
"/etc/sparkyfitness/.env"

View File

@@ -135,10 +135,34 @@ network_check() {
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
}
# This function updates the Container OS by running apt-get update and upgrade
# This function updates the Container OS by running apk upgrade with mirror fallback
update_os() {
msg_info "Updating Container OS"
$STD apk -U upgrade
if ! $STD apk -U upgrade; then
msg_warn "apk update failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
local alpine_mirrors="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
local apk_ok=false
for m in $(printf '%s\n' $alpine_mirrors | shuf); do
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${m}"
cat <<EOF >/etc/apk/repositories
http://$m/alpine/latest-stable/main
http://$m/alpine/latest-stable/community
EOF
if $STD apk -U upgrade; then
msg_ok "CDN set to ${m}: tests passed"
apk_ok=true
break
else
msg_warn "Mirror ${m} failed"
fi
fi
done
if [[ "$apk_ok" != true ]]; then
msg_error "All Alpine mirrors failed. Check network or try again later."
exit 1
fi
fi
local tools_content
tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) || {
msg_error "Failed to download tools.func"

View File

@@ -20,7 +20,7 @@ need_tool() {
msg_info "Installing tools: $*"
apk add --no-cache "$@" >/dev/null 2>&1 || {
msg_error "apk add failed for: $*"
return 1
return 100
}
msg_ok "Tools ready: $*"
fi
@@ -52,17 +52,17 @@ ensure_usr_local_bin_persist() {
download_with_progress() {
# $1 url, $2 dest
local url="$1" out="$2" cl
need_tool curl pv || return 1
need_tool curl pv || return 127
cl=$(curl -fsSLI "$url" 2>/dev/null | awk 'tolower($0) ~ /^content-length:/ {print $2}' | tr -d '\r')
if [ -n "$cl" ]; then
curl -fsSL "$url" | pv -s "$cl" >"$out" || {
msg_error "Download failed: $url"
return 1
return 250
}
else
curl -fL# -o "$out" "$url" || {
msg_error "Download failed: $url"
return 1
return 250
}
fi
}
@@ -82,14 +82,14 @@ check_for_gh_release() {
net_resolves api.github.com || {
msg_error "DNS/network error: api.github.com"
return 1
return 6
}
need_tool curl jq || return 1
need_tool curl jq || return 127
tag=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | jq -r '.tag_name // empty')
[ -z "$tag" ] && {
msg_error "Unable to fetch latest tag for $app"
return 1
return 22
}
release="${tag#v}"
@@ -133,12 +133,12 @@ fetch_and_deploy_gh() {
net_resolves api.github.com || {
msg_error "DNS/network error"
return 1
return 6
}
need_tool curl jq tar || return 1
need_tool curl jq tar || return 127
[ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true
tmpd="$(mktemp -d)" || return 1
tmpd="$(mktemp -d)" || return 252
mkdir -p "$target"
# Release JSON
@@ -146,13 +146,13 @@ fetch_and_deploy_gh() {
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")" || {
msg_error "GitHub API failed"
rm -rf "$tmpd"
return 1
return 22
}
else
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || {
msg_error "GitHub API failed"
rm -rf "$tmpd"
return 1
return 22
}
fi
@@ -163,7 +163,7 @@ fetch_and_deploy_gh() {
[ -z "$version" ] && {
msg_error "No tag in release json"
rm -rf "$tmpd"
return 1
return 65
}
case "$mode" in
@@ -173,26 +173,26 @@ fetch_and_deploy_gh() {
filename="${app_lc}-${version}.tar.gz"
download_with_progress "$url" "$tmpd/$filename" || {
rm -rf "$tmpd"
return 1
return 250
}
tar -xzf "$tmpd/$filename" -C "$tmpd" || {
msg_error "tar extract failed"
rm -rf "$tmpd"
return 1
return 251
}
unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)"
# copy content of unpack to target
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
msg_error "copy failed"
rm -rf "$tmpd"
return 1
return 252
}
;;
prebuild)
[ -n "$pattern" ] || {
msg_error "prebuild requires asset pattern"
rm -rf "$tmpd"
return 1
return 65
}
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
BEGIN{IGNORECASE=1}
@@ -201,19 +201,19 @@ fetch_and_deploy_gh() {
[ -z "$url" ] && {
msg_error "asset not found for pattern: $pattern"
rm -rf "$tmpd"
return 1
return 250
}
filename="${url##*/}"
download_with_progress "$url" "$tmpd/$filename" || {
rm -rf "$tmpd"
return 1
return 250
}
# unpack archive (Zip or tarball)
case "$filename" in
*.zip)
need_tool unzip || {
rm -rf "$tmpd"
return 1
return 127
}
mkdir -p "$tmpd/unp"
unzip -q "$tmpd/$filename" -d "$tmpd/unp"
@@ -225,7 +225,7 @@ fetch_and_deploy_gh() {
*)
msg_error "unsupported archive: $filename"
rm -rf "$tmpd"
return 1
return 251
;;
esac
# top-level folder strippen
@@ -234,13 +234,13 @@ fetch_and_deploy_gh() {
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
msg_error "copy failed"
rm -rf "$tmpd"
return 1
return 252
}
else
(cd "$tmpd/unp" && tar -cf - .) | (cd "$target" && tar -xf -) || {
msg_error "copy failed"
rm -rf "$tmpd"
return 1
return 252
}
fi
;;
@@ -248,7 +248,7 @@ fetch_and_deploy_gh() {
[ -n "$pattern" ] || {
msg_error "singlefile requires asset pattern"
rm -rf "$tmpd"
return 1
return 65
}
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
BEGIN{IGNORECASE=1}
@@ -257,19 +257,19 @@ fetch_and_deploy_gh() {
[ -z "$url" ] && {
msg_error "asset not found for pattern: $pattern"
rm -rf "$tmpd"
return 1
return 250
}
filename="${url##*/}"
download_with_progress "$url" "$target/$app" || {
rm -rf "$tmpd"
return 1
return 250
}
chmod +x "$target/$app"
;;
*)
msg_error "Unknown mode: $mode"
rm -rf "$tmpd"
return 1
return 65
;;
esac
@@ -291,20 +291,20 @@ setup_yq() {
return 0
fi
need_tool curl || return 1
need_tool curl || return 127
local arch bin url tmp
case "$(uname -m)" in
x86_64) arch="amd64" ;;
aarch64) arch="arm64" ;;
*)
msg_error "Unsupported arch for yq: $(uname -m)"
return 1
return 238
;;
esac
url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}"
tmp="$(mktemp)"
download_with_progress "$url" "$tmp" || return 1
install -m 0755 "$tmp" /usr/local/bin/yq
download_with_progress "$url" "$tmp" || return 250
/usr/bin/install -m 0755 "$tmp" /usr/local/bin/yq
rm -f "$tmp"
msg_ok "Setup yq ($(yq --version 2>/dev/null))"
}
@@ -313,13 +313,13 @@ setup_yq() {
# Adminer Alpine
# ------------------------------
setup_adminer() {
need_tool curl || return 1
need_tool curl || return 127
msg_info "Setup Adminer (Alpine)"
mkdir -p /var/www/localhost/htdocs/adminer
curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \
-o /var/www/localhost/htdocs/adminer/index.php || {
msg_error "Adminer download failed"
return 1
return 250
}
msg_ok "Adminer at /adminer (served by your webserver)"
}
@@ -329,7 +329,7 @@ setup_adminer() {
# optional: PYTHON_VERSION="3.12"
# ------------------------------
setup_uv() {
need_tool curl tar || return 1
need_tool curl tar || return 127
local UV_BIN="/usr/local/bin/uv"
local arch tarball url tmpd ver installed
@@ -338,7 +338,7 @@ setup_uv() {
aarch64) arch="aarch64-unknown-linux-musl" ;;
*)
msg_error "Unsupported arch for uv: $(uname -m)"
return 1
return 238
;;
esac
@@ -346,7 +346,7 @@ setup_uv() {
ver="${ver#v}"
[ -z "$ver" ] && {
msg_error "uv: cannot determine latest version"
return 1
return 250
}
if has "$UV_BIN"; then
@@ -360,29 +360,29 @@ setup_uv() {
msg_info "Setup uv $ver"
fi
tmpd="$(mktemp -d)" || return 1
tmpd="$(mktemp -d)" || return 252
tarball="uv-${arch}.tar.gz"
url="https://github.com/astral-sh/uv/releases/download/v${ver}/${tarball}"
download_with_progress "$url" "$tmpd/uv.tar.gz" || {
rm -rf "$tmpd"
return 1
return 250
}
tar -xzf "$tmpd/uv.tar.gz" -C "$tmpd" || {
msg_error "uv: extract failed"
rm -rf "$tmpd"
return 1
return 251
}
# tar contains ./uv
if [ -x "$tmpd/uv" ]; then
install -m 0755 "$tmpd/uv" "$UV_BIN"
/usr/bin/install -m 0755 "$tmpd/uv" "$UV_BIN"
else
# fallback: in subfolder
install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || {
/usr/bin/install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || {
msg_error "uv binary not found in tar"
rm -rf "$tmpd"
return 1
return 252
}
fi
rm -rf "$tmpd"
@@ -395,13 +395,13 @@ setup_uv() {
$0 ~ "^cpython-"maj"\\." { print $0 }' | awk -F- '{print $2}' | sort -V | tail -n1)"
[ -z "$match" ] && {
msg_error "No matching Python for $PYTHON_VERSION"
return 1
return 250
}
if ! uv python list | grep -q "cpython-${match}-linux"; then
msg_info "Installing Python $match via uv"
uv python install "$match" || {
msg_error "uv python install failed"
return 1
return 150
}
msg_ok "Python $match installed (uv)"
fi
@@ -421,7 +421,7 @@ setup_java() {
msg_info "Setup Java (OpenJDK $JAVA_VERSION)"
apk add --no-cache "$pkg" >/dev/null 2>&1 || {
msg_error "apk add $pkg failed"
return 1
return 100
}
# set JAVA_HOME
local prof="/etc/profile.d/20-java.sh"
@@ -441,32 +441,32 @@ setup_go() {
msg_info "Setup Go (apk)"
apk add --no-cache go >/dev/null 2>&1 || {
msg_error "apk add go failed"
return 1
return 100
}
msg_ok "Go ready: $(go version 2>/dev/null)"
return 0
fi
need_tool curl tar || return 1
need_tool curl tar || return 127
local ARCH TARBALL URL TMP
case "$(uname -m)" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
*)
msg_error "Unsupported arch for Go: $(uname -m)"
return 1
return 238
;;
esac
TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz"
URL="https://go.dev/dl/${TARBALL}"
msg_info "Setup Go $GO_VERSION (tarball)"
TMP="$(mktemp)"
download_with_progress "$URL" "$TMP" || return 1
download_with_progress "$URL" "$TMP" || return 250
rm -rf /usr/local/go
tar -C /usr/local -xzf "$TMP" || {
msg_error "extract go failed"
rm -f "$TMP"
return 1
return 251
}
rm -f "$TMP"
ln -sf /usr/local/go/bin/go /usr/local/bin/go
@@ -488,7 +488,7 @@ setup_composer() {
# Fallback to generic php if 83 not available
apk add --no-cache php-cli php-openssl php-phar php-iconv >/dev/null 2>&1 || {
msg_error "Failed to install php-cli for composer"
return 1
return 100
}
}
msg_ok "PHP CLI ready: $(php -v | head -n1)"
@@ -500,14 +500,14 @@ setup_composer() {
msg_info "Setup Composer"
fi
need_tool curl || return 1
need_tool curl || return 127
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
msg_error "composer installer download failed"
return 1
return 250
}
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 || {
msg_error "composer install failed"
return 1
return 150
}
rm -f /tmp/composer-setup.php
ensure_usr_local_bin_persist

View File

@@ -348,10 +348,10 @@ explain_exit_code() {
json_escape() {
# Escape a string for safe JSON embedding using awk (handles any input size).
# Pipeline: strip ANSI → remove control chars → escape \ " TAB → join lines with \n
printf '%s' "$1" \
| sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
| tr -d '\000-\010\013\014\016-\037\177\r' \
| awk '
printf '%s' "$1" |
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' |
tr -d '\000-\010\013\014\016-\037\177\r' |
awk '
BEGIN { ORS = "" }
{
gsub(/\\/, "\\\\") # backslash → \\
@@ -504,7 +504,7 @@ detect_gpu() {
GPU_PASSTHROUGH="unknown"
local gpu_line
gpu_line=$(lspci 2>/dev/null | grep -iE "VGA|3D|Display" | head -1)
gpu_line=$(lspci 2>/dev/null | grep -iE "VGA|3D|Display" | head -1 || true)
if [[ -n "$gpu_line" ]]; then
# Extract model: everything after the colon, clean up
@@ -543,7 +543,7 @@ detect_cpu() {
if [[ -f /proc/cpuinfo ]]; then
local vendor_id
vendor_id=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | tr -d ' ')
vendor_id=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | tr -d ' ' || true)
case "$vendor_id" in
GenuineIntel) CPU_VENDOR="intel" ;;
@@ -557,7 +557,7 @@ detect_cpu() {
esac
# Extract model name and clean it up
CPU_MODEL=$(grep -m1 "model name" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | sed 's/^ *//' | sed 's/(R)//g' | sed 's/(TM)//g' | sed 's/ */ /g' | cut -c1-64)
CPU_MODEL=$(grep -m1 "model name" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | sed 's/^ *//' | sed 's/(R)//g' | sed 's/(TM)//g' | sed 's/ */ /g' | cut -c1-64 || true)
fi
export CPU_VENDOR CPU_MODEL
@@ -627,8 +627,8 @@ post_to_api() {
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] post_to_api() DIAGNOSTICS=$DIAGNOSTICS RANDOM_UUID=$RANDOM_UUID NSAPP=$NSAPP" >&2
# Set type for later status updates
TELEMETRY_TYPE="lxc"
# Set type for later status updates (preserve if already set, e.g. turnkey)
TELEMETRY_TYPE="${TELEMETRY_TYPE:-lxc}"
local pve_version=""
if command -v pveversion &>/dev/null; then
@@ -664,7 +664,7 @@ post_to_api() {
{
"random_id": "${RANDOM_UUID}",
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
"type": "lxc",
"type": "${TELEMETRY_TYPE}",
"nsapp": "${NSAPP:-unknown}",
"status": "installing",
"ct_type": ${CT_TYPE:-1},
@@ -692,6 +692,7 @@ EOF
# Send initial "installing" record with retry.
# This record MUST exist for all subsequent updates to succeed.
local http_code="" attempt
local _post_success=false
for attempt in 1 2 3; do
if [[ "${DEV_MODE:-}" == "true" ]]; then
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
@@ -703,11 +704,19 @@ EOF
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
fi
[[ "$http_code" =~ ^2[0-9]{2}$ ]] && break
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
_post_success=true
break
fi
[[ "$attempt" -lt 3 ]] && sleep 1
done
POST_TO_API_DONE=true
# Only mark done if at least one attempt succeeded.
# If all 3 failed, POST_TO_API_DONE stays false so post_update_to_api
# and on_exit() know the initial record was never created.
# The server has fallback logic to create a new record on status updates,
# so subsequent calls can still succeed even without the initial record.
POST_TO_API_DONE=${_post_success}
}
# ------------------------------------------------------------------------------
@@ -798,15 +807,19 @@ EOF
# Send initial "installing" record with retry (must succeed for updates to work)
local http_code="" attempt
local _post_success=false
for attempt in 1 2 3; do
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
[[ "$http_code" =~ ^2[0-9]{2}$ ]] && break
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
_post_success=true
break
fi
[[ "$attempt" -lt 3 ]] && sleep 1
done
POST_TO_API_DONE=true
POST_TO_API_DONE=${_post_success}
}
# ------------------------------------------------------------------------------
@@ -1083,6 +1096,12 @@ EOF
# - Used to group errors in dashboard
# ------------------------------------------------------------------------------
categorize_error() {
# Allow build.func to override category based on log analysis (exit code 1 subclassification)
if [[ -n "${ERROR_CATEGORY_OVERRIDE:-}" ]]; then
echo "$ERROR_CATEGORY_OVERRIDE"
return
fi
local code="$1"
case "$code" in
# Network errors (curl/wget)
@@ -1328,8 +1347,8 @@ post_addon_to_api() {
# Detect OS info
local os_type="" os_version=""
if [[ -f /etc/os-release ]]; then
os_type=$(grep "^ID=" /etc/os-release | cut -d= -f2 | tr -d '"')
os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d= -f2 | tr -d '"')
os_type=$(grep "^ID=" /etc/os-release | cut -d= -f2 | tr -d '"' || true)
os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d= -f2 | tr -d '"' || true)
fi
local JSON_PAYLOAD

View File

@@ -173,10 +173,10 @@ get_current_ip() {
# Check for Debian/Ubuntu (uses hostname -I)
if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
# Try IPv4 first
CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)
CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)
# Fallback to IPv6 if no IPv4
if [[ -z "$CURRENT_IP" ]]; then
CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1)
CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1 || true)
fi
# Check for Alpine (uses ip command)
elif grep -q 'ID=alpine' /etc/os-release; then
@@ -222,9 +222,12 @@ update_motd_ip() {
local current_ip="$(hostname -I | awk '{print $1}')"
# Escape sed special chars in replacement strings (& \ |)
current_os="${current_os//\\/\\\\}"; current_os="${current_os//&/\\&}"
current_hostname="${current_hostname//\\/\\\\}"; current_hostname="${current_hostname//&/\\&}"
current_ip="${current_ip//\\/\\\\}"; current_ip="${current_ip//&/\\&}"
current_os="${current_os//\\/\\\\}"
current_os="${current_os//&/\\&}"
current_hostname="${current_hostname//\\/\\\\}"
current_hostname="${current_hostname//&/\\&}"
current_ip="${current_ip//\\/\\\\}"
current_ip="${current_ip//&/\\&}"
# Update only if values actually changed
if ! grep -q "OS:.*$current_os" "$PROFILE_FILE" 2>/dev/null; then
@@ -264,12 +267,12 @@ install_ssh_keys_into_ct() {
msg_info "Installing selected SSH keys into CT ${CTID}"
pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || {
msg_error "prepare /root/.ssh failed"
return 1
return 252
}
pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 ||
pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || {
msg_error "write authorized_keys failed"
return 1
return 252
}
pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
msg_ok "Installed SSH keys into CT ${CTID}"
@@ -836,7 +839,7 @@ choose_and_set_storage_for_file() {
template) key="var_template_storage" ;;
*)
msg_error "Unknown storage class: $class"
return 1
return 65
;;
esac
@@ -859,7 +862,7 @@ choose_and_set_storage_for_file() {
fi
else
# If the current value is preselectable, we could show it, but per your requirement we always offer selection
select_storage "$class" || return 1
select_storage "$class" || return 150
fi
_write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
@@ -1261,7 +1264,7 @@ default_var_settings() {
return 0
}
done
return 1
return 252
}
# Allow override of storages via env (for non-interactive use cases)
[ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
@@ -1354,7 +1357,7 @@ EOF
local dv
dv="$(_find_default_vars)" || {
msg_error "default.vars not found after ensure step"
return 1
return 252
}
load_vars_file "$dv"
@@ -1639,7 +1642,7 @@ maybe_offer_save_app_defaults() {
if whiptail --backtitle "Proxmox VE Helper Scripts" \
--yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then
mkdir -p "$(dirname "$app_vars_path")"
install -m 0644 "$new_tmp" "$app_vars_path"
/usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
msg_ok "Saved app defaults: ${app_vars_path}"
fi
rm -f "$new_tmp" "$diff_tmp"
@@ -1673,7 +1676,7 @@ maybe_offer_save_app_defaults() {
case "$sel" in
"Update Defaults")
install -m 0644 "$new_tmp" "$app_vars_path"
/usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
msg_ok "Updated app defaults: ${app_vars_path}"
break
;;
@@ -1701,8 +1704,8 @@ ensure_storage_selection_for_vars_file() {
# Read stored values (if any)
local tpl ct
tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-)
ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-)
tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2- || true)
ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2- || true)
if [[ -n "$tpl" && -n "$ct" ]]; then
TEMPLATE_STORAGE="$tpl"
@@ -1837,7 +1840,7 @@ advanced_settings() {
if [[ -n "$BRIDGES" ]]; then
while IFS= read -r bridge; do
if [[ -n "$bridge" ]]; then
local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//;s/^[- ]*//')
local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//;s/^[- ]*//' || true)
BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }")
fi
done <<<"$BRIDGES"
@@ -3319,7 +3322,7 @@ configure_ssh_settings() {
tag="${tag%\"}"
tag="${tag#\"}"
local line
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2- || true)
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
done
;;
@@ -3346,7 +3349,7 @@ configure_ssh_settings() {
tag="${tag%\"}"
tag="${tag#\"}"
local line
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2- || true)
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
done
else
@@ -4047,7 +4050,7 @@ EOF
# Fix Debian 13 LXC template bug where / is owned by nobody:nogroup
# This must be done from the host as unprivileged containers cannot chown /
local rootfs
rootfs=$(pct config "$CTID" | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1)
rootfs=$(pct config "$CTID" | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1 || true)
if [[ -n "$rootfs" ]]; then
local mount_point="/var/lib/lxc/${CTID}/rootfs"
if [[ -d "$mount_point" ]] && [[ "$(stat -c '%U' "$mount_point")" != "root" ]]; then
@@ -4085,8 +4088,31 @@ https://dl-cdn.alpinelinux.org/alpine/latest-stable/main
https://dl-cdn.alpinelinux.org/alpine/latest-stable/community
EOF'
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq" >>"$BUILD_LOG" 2>&1 || {
msg_error "Failed to install base packages in Alpine container"
install_exit_code=1
msg_warn "apk install failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
local alpine_exit=0
pct exec "$CTID" -- ash -c '
ALPINE_MIRRORS="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
for m in $(printf "%s\n" $ALPINE_MIRRORS | shuf); do
if wget -q --spider --timeout=2 "http://$m/alpine/latest-stable/main/" 2>/dev/null; then
echo " Attempting mirror: $m"
cat <<EOF >/etc/apk/repositories
http://$m/alpine/latest-stable/main
http://$m/alpine/latest-stable/community
EOF
if apk update >/dev/null 2>&1 && apk add bash newt curl openssh nano mc ncurses jq >/dev/null 2>&1; then
echo " CDN set to $m: tests passed"
exit 0
else
echo " Mirror $m failed"
fi
fi
done
exit 2
' && alpine_exit=0 || alpine_exit=$?
if [[ $alpine_exit -ne 0 ]]; then
msg_error "Failed to install base packages in Alpine container"
install_exit_code=1
fi
}
else
sleep 3
@@ -4112,9 +4138,140 @@ EOF'
msg_warn "Skipping timezone setup zone '$tz' not found in container"
fi
# Detect broken DNS resolver (e.g. Tailscale MagicDNS) and inject public DNS
if ! pct exec "$CTID" -- bash -c "getent hosts deb.debian.org >/dev/null 2>&1 && getent hosts archive.ubuntu.com >/dev/null 2>&1"; then
msg_warn "APT repository DNS resolution failed in container, injecting public DNS servers"
pct exec "$CTID" -- bash -c "echo -e 'nameserver 8.8.8.8\nnameserver 1.1.1.1' >/etc/resolv.conf"
fi
pct exec "$CTID" -- bash -c "apt-get update 2>&1 && apt-get install -y sudo curl mc gnupg2 jq 2>&1" >>"$BUILD_LOG" 2>&1 || {
msg_error "apt-get base packages installation failed"
install_exit_code=1
local failed_mirror
failed_mirror=$(pct exec "$CTID" -- bash -c "grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null" 2>/dev/null || echo "unknown")
msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..."
local mirror_exit=0
pct exec "$CTID" -- bash -c '
APT_BASE="sudo curl mc gnupg2 jq"
DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")
if [ "$DISTRO" = "ubuntu" ]; then
EU_MIRRORS="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de"
US_MIRRORS="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu"
AP_MIRRORS="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au"
else
EU_MIRRORS="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
US_MIRRORS="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
AP_MIRRORS="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
fi
TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC")
case "$TZ" in
Europe/*|Arctic/*) REGIONAL="$EU_MIRRORS"; OTHERS="$US_MIRRORS $AP_MIRRORS" ;;
America/*) REGIONAL="$US_MIRRORS"; OTHERS="$EU_MIRRORS $AP_MIRRORS" ;;
Asia/*|Australia/*|Pacific/*) REGIONAL="$AP_MIRRORS"; OTHERS="$EU_MIRRORS $US_MIRRORS" ;;
*) REGIONAL=""; OTHERS="$EU_MIRRORS $US_MIRRORS $AP_MIRRORS" ;;
esac
echo "Acquire::By-Hash \"no\";" >/etc/apt/apt.conf.d/99no-by-hash
try_mirrors() {
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
[ -f "$src" ] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${1}/|g; s|deb http[s]*://[^/]*/|deb http://${1}/|g" "$src"
done
rm -rf /var/lib/apt/lists/*
APT_OUT=$(apt-get update 2>&1)
APT_RC=$?
if echo "$APT_OUT" | grep -qi "hashsum\|hash sum"; then
echo " Mirror $1 failed (hash mismatch)"
return 1
elif echo "$APT_OUT" | grep -qi "SSL\|certificate"; then
echo " Mirror $1 failed (SSL/certificate error)"
return 1
elif [ $APT_RC -ne 0 ]; then
echo " Mirror $1 failed (apt-get update error)"
return 1
elif apt-get install -y $APT_BASE >/dev/null 2>&1; then
echo " CDN set to $1: tests passed"
return 0
else
echo " Mirror $1 failed (package install error)"
return 1
fi
}
scan_reachable() {
local result=""
for m in $1; do
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
result="$result $m"
fi
done
echo "$result" | xargs
}
# Phase 1: Scan global mirrors first (independent of local CDN issues)
OTHERS_OK=$(scan_reachable "$OTHERS")
OTHERS_PICK=$(printf "%s\n" $OTHERS_OK | shuf | head -3 | xargs)
for mirror in $OTHERS_PICK; do
echo " Attempting mirror: $mirror"
try_mirrors "$mirror" && exit 0
done
# Phase 2: Try primary mirror
if [ "$DISTRO" = "ubuntu" ]; then
PRIMARY="archive.ubuntu.com"
else
PRIMARY="ftp.debian.org"
fi
if timeout 2 bash -c "echo >/dev/tcp/$PRIMARY/80" 2>/dev/null; then
echo " Attempting mirror: $PRIMARY"
try_mirrors "$PRIMARY" && exit 0
fi
# Phase 3: Fall back to regional mirrors
REGIONAL_OK=$(scan_reachable "$REGIONAL")
REGIONAL_PICK=$(printf "%s\n" $REGIONAL_OK | shuf | head -3 | xargs)
for mirror in $REGIONAL_PICK; do
echo " Attempting mirror: $mirror"
try_mirrors "$mirror" && exit 0
done
exit 2
' && mirror_exit=0 || mirror_exit=$?
if [[ $mirror_exit -eq 2 ]]; then
msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
if [[ "$var_os" == "ubuntu" ]]; then
msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors"
else
msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list"
fi
local custom_mirror=""
while true; do
read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror </dev/tty
[[ -z "$custom_mirror" ]] && continue
[[ "$custom_mirror" == "skip" ]] && break
[[ ! "$custom_mirror" =~ ^[a-zA-Z0-9._-]+$ ]] && {
msg_warn "Invalid hostname format."
continue
}
pct exec "$CTID" -- bash -c "
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
[ -f \"\$src\" ] && sed -i \"s|URIs: http[s]*://[^/]*/|URIs: http://${custom_mirror}/|g; s|deb http[s]*://[^/]*/|deb http://${custom_mirror}/|g\" \"\$src\"
done
rm -rf /var/lib/apt/lists/*
apt-get update >/dev/null 2>&1 && apt-get install -y sudo curl mc gnupg2 jq >/dev/null 2>&1
" && break
msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
done
if [[ "$custom_mirror" == "skip" ]]; then
msg_error "apt-get base packages installation failed"
install_exit_code=1
fi
elif [[ $mirror_exit -ne 0 ]]; then
msg_error "apt-get base packages installation failed"
install_exit_code=1
fi
}
fi
@@ -4223,6 +4380,53 @@ EOF'
fi
fi
# Defense-in-depth: Ensure error handling stays disabled during recovery.
# Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail
# and trap 'error_handler' ERR. If any code path above called such a function,
# the grep/sed pipelines below would trigger error_handler on non-match (exit 1).
set +Eeuo pipefail
trap - ERR
# --- Exit code 1 subclassification: analyze logs BEFORE telemetry call ---
# Exit code 1 is generic ("General error"). Analyze logs to determine the
# real error category so telemetry gets a useful classification instead of "shell".
local is_oom=false
local is_network_issue=false
local is_apt_issue=false
local is_cmd_not_found=false
local is_disk_full=false
if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then
if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then
is_apt_issue=true
fi
if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then
is_oom=true
fi
if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then
is_network_issue=true
fi
if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then
is_cmd_not_found=true
fi
if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
is_disk_full=true
fi
fi
# Set override for categorize_error() so telemetry gets the real category
if [[ "$is_apt_issue" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="dependency"
elif [[ "$is_oom" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="resource"
elif [[ "$is_network_issue" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="network"
elif [[ "$is_disk_full" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="storage"
elif [[ "$is_cmd_not_found" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="dependency"
fi
# Report failure to telemetry API (now with log available on host)
# NOTE: Do NOT use msg_info/spinner here — the background spinner process
# causes SIGTSTP in non-interactive shells (bash -c "$(curl ...)"), which
@@ -4231,13 +4435,6 @@ EOF'
post_update_to_api "failed" "$install_exit_code"
$STD echo -e "${TAB}${CM:-} Failure reported"
# Defense-in-depth: Ensure error handling stays disabled during recovery.
# Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail
# and trap 'error_handler' ERR. If any code path above called such a function,
# the grep/sed pipelines below would trigger error_handler on non-match (exit 1).
set +Eeuo pipefail
trap - ERR
# Show combined log location
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
msg_custom "📋" "${YW}" "Installation log: ${combined_log}"
@@ -4266,12 +4463,9 @@ EOF'
# Prompt user for cleanup with 60s timeout
echo ""
# Detect error type for smart recovery options
local is_oom=false
local is_network_issue=false
local is_apt_issue=false
local is_cmd_not_found=false
local is_disk_full=false
# Extend error detection for non-exit-1 codes (exit 1 was already analyzed above)
# The is_* flags were set above for exit code 1 log analysis; here we add
# exit-code-specific detections for other codes.
local error_explanation=""
if declare -f explain_exit_code >/dev/null 2>&1; then
error_explanation="$(explain_exit_code "$install_exit_code")"
@@ -4321,26 +4515,6 @@ EOF'
;;
esac
# Exit 1 subclassification: analyze logs to identify actual root cause
# Many exit 1 errors are actually APT, OOM, network, or command-not-found issues
if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then
if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then
is_apt_issue=true
fi
if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then
is_oom=true
fi
if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then
is_network_issue=true
fi
if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then
is_cmd_not_found=true
fi
if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
is_disk_full=true
fi
fi
# Show error explanation if available
if [[ -n "$error_explanation" ]]; then
echo -e "${TAB}${RD}Error: ${error_explanation}${CL}"
@@ -4542,6 +4716,7 @@ EOF'
if [[ $apt_retry_code -eq 0 ]]; then
msg_ok "Installation completed successfully after APT repair!"
INSTALL_COMPLETE=true
post_update_to_api "done" "0" "force"
return 0
else
@@ -4669,7 +4844,7 @@ EOF'
destroy_lxc() {
if [[ -z "$CT_ID" ]]; then
msg_error "No CT_ID found. Nothing to remove."
return 1
return 65
fi
# Abort on Ctrl-C / Ctrl-D / ESC
@@ -4708,12 +4883,12 @@ resolve_storage_preselect() {
case "$class" in
template) required_content="vztmpl" ;;
container) required_content="rootdir" ;;
*) return 1 ;;
*) return 65 ;;
esac
[[ -z "$preselect" ]] && return 1
if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then
msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)"
return 1
return 238
fi
local line total used free
@@ -4837,7 +5012,7 @@ select_storage() {
;;
*)
msg_error "Invalid storage class '$CLASS'"
return 1
return 65
;;
esac
@@ -4919,7 +5094,7 @@ validate_storage_space() {
# Check if storage exists and is active
if [[ -z "$storage_line" ]]; then
[[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' not found!\n\nThe storage may be unavailable or disabled." 10 60
return 2
return 236
fi
# Check storage status (column 3)
@@ -4927,7 +5102,7 @@ validate_storage_space() {
status=$(awk '{print $3}' <<<"$storage_line")
if [[ "$status" == "disabled" ]]; then
[[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' is disabled!\n\nPlease enable the storage first." 10 60
return 2
return 236
fi
# Get storage type and free space (column 6)
@@ -4950,7 +5125,7 @@ validate_storage_space() {
if [[ "$show_dialog" == "yes" ]]; then
whiptail --msgbox "⚠️ Warning: Storage '$storage' may not have enough space!\n\nStorage Type: ${storage_type}\nRequired: ${required_gb}GB\nAvailable: ${free_gb_fmt}\n\nYou can continue, but creation might fail." 14 70
fi
return 1
return 236
fi
return 0
@@ -5121,7 +5296,7 @@ create_lxc_container() {
fi
msg_info "Validating storage '$CONTAINER_STORAGE'"
STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1)
STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1 || true)
if [[ -z "$STORAGE_TYPE" ]]; then
msg_error "Storage '$CONTAINER_STORAGE' not found in /etc/pve/storage.cfg"
@@ -5160,7 +5335,7 @@ create_lxc_container() {
msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated"
msg_info "Validating template storage '$TEMPLATE_STORAGE'"
TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1)
TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 || true)
if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then
msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'"
@@ -5683,7 +5858,7 @@ description() {
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>
@@ -5716,6 +5891,7 @@ EOF
systemctl start ping-instances.service
fi
INSTALL_COMPLETE=true
post_update_to_api "done" "none"
}

View File

@@ -319,11 +319,11 @@ function setup_cloud_init() {
if [ "$network_mode" = "static" ]; then
if [ -n "$static_ip" ] && ! validate_ip_cidr "$static_ip"; then
_ci_msg_error "Invalid static IP format: $static_ip (expected: x.x.x.x/xx)"
return 1
return 65
fi
if [ -n "$gateway" ] && ! validate_ip "$gateway"; then
_ci_msg_error "Invalid gateway IP format: $gateway"
return 1
return 65
fi
fi
@@ -433,7 +433,7 @@ function configure_cloud_init_interactive() {
if ! command -v whiptail >/dev/null 2>&1; then
echo "Warning: whiptail not available, skipping interactive configuration"
export CLOUDINIT_ENABLE="no"
return 1
return 127
fi
# Ask if user wants to enable Cloud-Init
@@ -603,7 +603,7 @@ function get_vm_ip() {
elapsed=$((elapsed + 2))
done
return 1
return 7
}
# ------------------------------------------------------------------------------
@@ -621,7 +621,7 @@ function wait_for_cloud_init() {
if [ -z "$vm_ip" ]; then
_ci_msg_warn "Unable to determine VM IP address"
return 1
return 7
fi
_ci_msg_info "Waiting for Cloud-Init to complete on ${vm_ip}"
@@ -638,7 +638,7 @@ function wait_for_cloud_init() {
done
_ci_msg_warn "Cloud-Init did not complete within ${timeout}s"
return 1
return 150
}
# ==============================================================================

View File

@@ -858,7 +858,7 @@ get_header() {
if [ ! -s "$local_header_path" ]; then
if ! curl -fsSL "$header_url" -o "$local_header_path"; then
msg_warn "Failed to download header: $header_url"
return 1
return 250
fi
fi
@@ -1358,7 +1358,7 @@ prompt_select() {
if [[ $num_options -eq 0 ]]; then
msg_warn "prompt_select called with no options"
echo "" >&2
return 1
return 65
fi
# Validate default
@@ -1600,7 +1600,7 @@ check_or_create_swap() {
swap_size_mb=$(prompt_input "Enter swap size in MB (e.g., 2048 for 2GB):" "2048" 60)
if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then
msg_error "Invalid swap size: '${swap_size_mb}' (must be a number in MB)"
return 1
return 65
fi
local swap_file="/swapfile"
@@ -1608,19 +1608,19 @@ check_or_create_swap() {
msg_info "Creating ${swap_size_mb}MB swap file at $swap_file"
if ! dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress; then
msg_error "Failed to allocate swap file (dd failed)"
return 1
return 150
fi
if ! chmod 600 "$swap_file"; then
msg_error "Failed to set permissions on $swap_file"
return 1
return 150
fi
if ! mkswap "$swap_file"; then
msg_error "Failed to format swap file (mkswap failed)"
return 1
return 150
fi
if ! swapon "$swap_file"; then
msg_error "Failed to activate swap (swapon failed)"
return 1
return 150
fi
msg_ok "Swap file created and activated successfully"
}
@@ -1699,13 +1699,13 @@ function get_lxc_ip() {
fi
done
return 1
return 6
}
LOCAL_IP="$(get_current_ip || true)"
if [[ -z "$LOCAL_IP" ]]; then
msg_error "Could not determine LOCAL_IP (checked: eth0, hostname -I, ip route, IPv6 targets)"
return 1
return 6
fi
fi

View File

@@ -507,14 +507,23 @@ _stop_container_if_installing() {
on_exit() {
local exit_code=$?
# Report orphaned "installing" records to telemetry API
# Catches ALL exit paths: errors, signals, AND clean exits where
# post_to_api was called but post_update_to_api was never called
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
if [[ $exit_code -ne 0 ]]; then
_send_abort_telemetry "$exit_code"
elif declare -f post_update_to_api >/dev/null 2>&1; then
post_update_to_api "done" "0" 2>/dev/null || true
# Report orphaned telemetry records
# Two scenarios handled:
# 1. POST_TO_API_DONE=true but POST_UPDATE_DONE=false: Record was created but
# never got a final status update → send abort/done now.
# 2. POST_TO_API_DONE=false but DIAGNOSTICS=yes: Initial post failed (server
# unreachable/timeout), but the server has fallback create-on-update logic,
# so a status update can still create the record. Worth one last try.
if [[ "${POST_UPDATE_DONE:-}" != "true" ]]; then
if [[ "${POST_TO_API_DONE:-}" == "true" || "${DIAGNOSTICS:-no}" == "yes" ]]; then
if [[ $exit_code -ne 0 ]]; then
_send_abort_telemetry "$exit_code"
elif [[ "${INSTALL_COMPLETE:-}" == "true" ]] && declare -f post_update_to_api >/dev/null 2>&1; then
# Only report success if the install was explicitly marked complete.
# Without this guard, early bailouts (e.g. user cancelled) with exit 0
# would be falsely reported as successful installations.
post_update_to_api "done" "0" 2>/dev/null || true
fi
fi
fi

View File

@@ -210,6 +210,173 @@ network_check() {
# SECTION 3: OS UPDATE & PACKAGE MANAGEMENT
# ==============================================================================
# ------------------------------------------------------------------------------
# apt_update_safe()
#
# - Runs apt-get update with CDN mirror fallback
# - On failure, detects distro (Debian/Ubuntu) and tries alternate mirrors
# - Three-phase approach: global mirrors → primary mirror → regional mirrors
# - Falls back to manual user prompt if all auto mirrors fail
# - Detects hash mismatch, SSL errors, and generic apt failures
# ------------------------------------------------------------------------------
apt_update_safe() {
if $STD apt-get update; then
return 0
fi
local failed_mirror
failed_mirror=$(grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null || echo "unknown")
msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..."
local distro
distro=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")
local eu_mirrors us_mirrors ap_mirrors
if [[ "$distro" == "ubuntu" ]]; then
eu_mirrors="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de"
us_mirrors="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu"
ap_mirrors="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au"
else
eu_mirrors="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
us_mirrors="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
ap_mirrors="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
fi
local tz regional others
tz=$(cat /etc/timezone 2>/dev/null || echo "UTC")
case "$tz" in
Europe/* | Arctic/*)
regional="$eu_mirrors"
others="$us_mirrors $ap_mirrors"
;;
America/*)
regional="$us_mirrors"
others="$eu_mirrors $ap_mirrors"
;;
Asia/* | Australia/* | Pacific/*)
regional="$ap_mirrors"
others="$eu_mirrors $us_mirrors"
;;
*)
regional=""
others="$eu_mirrors $us_mirrors $ap_mirrors"
;;
esac
echo 'Acquire::By-Hash "no";' >/etc/apt/apt.conf.d/99no-by-hash
_try_apt_mirror() {
local m=$1
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
[[ -f "$src" ]] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${m}/|g; s|deb http[s]*://[^/]*/|deb http://${m}/|g" "$src"
done
rm -rf /var/lib/apt/lists/*
local out
out=$(apt-get update 2>&1)
if echo "$out" | grep -qi "hashsum\|hash sum"; then
msg_warn "Mirror ${m} failed (hash mismatch)"
return 1
elif echo "$out" | grep -qi "SSL\|certificate"; then
msg_warn "Mirror ${m} failed (SSL/certificate error)"
return 1
elif echo "$out" | grep -q "^E:"; then
msg_warn "Mirror ${m} failed (apt-get update error)"
return 1
else
msg_ok "CDN set to ${m}: tests passed"
return 0
fi
}
_scan_reachable() {
local result=""
for m in $1; do
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
result="$result $m"
fi
done
echo "$result" | xargs
}
local apt_ok=false
# Phase 1: Scan global mirrors first (independent of local CDN issues)
local others_ok
others_ok=$(_scan_reachable "$others")
local others_pick
others_pick=$(printf '%s\n' $others_ok | shuf | head -3 | xargs)
for mirror in $others_pick; do
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}"
if _try_apt_mirror "$mirror"; then
apt_ok=true
break
fi
done
# Phase 2: Try primary mirror
if [[ "$apt_ok" != true ]]; then
local primary
if [[ "$distro" == "ubuntu" ]]; then
primary="archive.ubuntu.com"
else
primary="ftp.debian.org"
fi
if timeout 2 bash -c "echo >/dev/tcp/$primary/80" 2>/dev/null; then
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${primary}"
if _try_apt_mirror "$primary"; then
apt_ok=true
fi
fi
fi
# Phase 3: Fall back to regional mirrors
if [[ "$apt_ok" != true ]]; then
local regional_ok
regional_ok=$(_scan_reachable "$regional")
local regional_pick
regional_pick=$(printf '%s\n' $regional_ok | shuf | head -3 | xargs)
for mirror in $regional_pick; do
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}"
if _try_apt_mirror "$mirror"; then
apt_ok=true
break
fi
done
fi
# Phase 4: All auto mirrors failed, prompt user
if [[ "$apt_ok" != true ]]; then
msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
if [[ "$distro" == "ubuntu" ]]; then
msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors"
else
msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list"
fi
local custom_mirror
while true; do
read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror </dev/tty
[[ -z "$custom_mirror" ]] && continue
[[ "$custom_mirror" == "skip" ]] && break
[[ ! "$custom_mirror" =~ ^[a-zA-Z0-9._-]+$ ]] && {
msg_warn "Invalid hostname format."
continue
}
if _try_apt_mirror "$custom_mirror"; then
apt_ok=true
break
fi
msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
done
fi
if [[ "$apt_ok" != true ]]; then
msg_error "All mirrors failed. Check network or try again later."
return 1
fi
}
# ------------------------------------------------------------------------------
# update_os()
#

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,7 @@ get_header() {
if [ ! -s "$local_header_path" ]; then
if ! curl -fsSL "$header_url" -o "$local_header_path"; then
return 1
return 250
fi
fi
@@ -594,7 +594,7 @@ set_description() {
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -116,7 +116,7 @@ fi
PCT_OPTIONS="
-features keyctl=1,nesting=1
-hostname $NAME
-tags proxmox-helper-scripts
-tags community-script
-onboot 0
-cores 2
-memory 2048

View File

@@ -165,9 +165,9 @@ function install() {
else
read -rp "${TAB}Set admin username [admin]: " admin_user
admin_user=${admin_user:-admin}
read -rsp "${TAB}Set admin password [helper-scripts.com]: " admin_pass
read -rsp "${TAB}Set admin password [community-scripts.org]: " admin_pass
echo ""
admin_pass=${admin_pass:-helper-scripts.com}
admin_pass=${admin_pass:-community-scripts.org}
msg_ok "Configured with admin user: ${admin_user}"
fi

View File

@@ -201,9 +201,9 @@ server:
- neverWatchPath: "/lost+found"
auth:
adminUsername: admin
adminPassword: helper-scripts.com
adminPassword: community-scripts.org
EOF
msg_ok "Configured with default admin (admin / helper-scripts.com)"
msg_ok "Configured with default admin (admin / community-scripts.org)"
fi
msg_info "Creating service"

View File

@@ -140,8 +140,8 @@ if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then
cd /usr/local/community-scripts
filebrowser config init -a '0.0.0.0' -p "$PORT" -d "$DB_PATH" &>/dev/null
filebrowser config set -a '0.0.0.0' -p "$PORT" -d "$DB_PATH" &>/dev/null
filebrowser users add admin helper-scripts.com --perm.admin --database "$DB_PATH" &>/dev/null
msg_ok "Default authentication configured (admin:helper-scripts.com)"
filebrowser users add admin community-scripts.org --perm.admin --database "$DB_PATH" &>/dev/null
msg_ok "Default authentication configured (admin:community-scripts.org)"
fi
msg_info "Creating service"

173
tools/addon/homebrew.sh Normal file
View File

@@ -0,0 +1,173 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MorganCSIT | MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://brew.sh | Github: https://github.com/Homebrew/brew
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)
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) 2>/dev/null || true
# Enable error handling
set -Eeuo pipefail
trap 'error_handler' ERR
load_functions
init_tool_telemetry "" "addon"
# ==============================================================================
# CONFIGURATION
# ==============================================================================
VERBOSE=${var_verbose:-no}
APP="homebrew"
APP_TYPE="tools"
INSTALL_PATH="/home/linuxbrew/.linuxbrew"
# ==============================================================================
# OS DETECTION
# ==============================================================================
if [[ -f "/etc/alpine-release" ]]; then
echo -e "${CROSS} Alpine is not supported by Homebrew. Exiting."
exit 1
elif grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
OS="Debian"
else
echo -e "${CROSS} Unsupported OS detected. Exiting."
exit 1
fi
# ==============================================================================
# UNINSTALL
# ==============================================================================
function uninstall() {
msg_info "Uninstalling Homebrew"
BREW_USER=$(awk -F: '$3 >= 1000 && $3 < 65534 { print $1; exit }' /etc/passwd)
if [[ -n "$BREW_USER" ]]; then
BREW_USER_HOME=$(getent passwd "$BREW_USER" | cut -d: -f6)
for rc_file in "$BREW_USER_HOME/.bashrc" "$BREW_USER_HOME/.profile"; do
if [[ -f "$rc_file" ]]; then
sed -i '/# Homebrew (Linuxbrew)/,/^fi$/d' "$rc_file"
fi
done
fi
rm -rf /home/linuxbrew
rm -f /etc/profile.d/homebrew.sh
groupdel linuxbrew &>/dev/null || true
msg_ok "Homebrew has been uninstalled"
}
# ==============================================================================
# INSTALL
# ==============================================================================
function install() {
msg_info "Detecting Non-Root User"
BREW_USER=$(awk -F: '$3 >= 1000 && $3 < 65534 { print $1; exit }' /etc/passwd)
if [[ -z "$BREW_USER" ]]; then
msg_warn "No non-root user found (uid >= 1000). Homebrew cannot run as root."
read -r -p "${TAB}Create a 'brew' user automatically? (y/N): " create_user_prompt
if [[ "${create_user_prompt,,}" =~ ^(y|yes)$ ]]; then
msg_info "Creating user 'brew'"
useradd -m -s /bin/bash brew
BREW_USER="brew"
msg_ok "Created user 'brew'"
else
msg_error "Cannot install Homebrew without a non-root user. Exiting."
exit 1
fi
fi
msg_ok "Detected User: $BREW_USER"
msg_info "Installing Dependencies"
$STD apt update
$STD apt install -y build-essential git file procps
msg_ok "Installed Dependencies"
msg_info "Setting Up Homebrew Prefix"
export PATH="/usr/sbin:$PATH"
groupadd -f linuxbrew
mkdir -p /home/linuxbrew/.linuxbrew
chown -R "$BREW_USER":linuxbrew /home/linuxbrew
chmod 2775 /home/linuxbrew
chmod 2775 /home/linuxbrew/.linuxbrew
usermod -aG linuxbrew "$BREW_USER"
msg_ok "Set Up Homebrew Prefix"
msg_info "Installing Homebrew"
$STD su - "$BREW_USER" -c 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
msg_ok "Installed Homebrew"
msg_info "Configuring Shell Integration"
cat <<'EOF' >/etc/profile.d/homebrew.sh
#!/bin/bash
if [ -d "/home/linuxbrew/.linuxbrew" ]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
fi
EOF
chmod +x /etc/profile.d/homebrew.sh
BREW_USER_HOME=$(getent passwd "$BREW_USER" | cut -d: -f6)
BREW_SHELL_BLOCK='\n# Homebrew (Linuxbrew)\nif [ -d "/home/linuxbrew/.linuxbrew" ]; then\n eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"\nfi'
for rc_file in "$BREW_USER_HOME/.bashrc" "$BREW_USER_HOME/.profile"; do
if ! grep -q 'linuxbrew' "$rc_file" 2>/dev/null; then
echo -e "$BREW_SHELL_BLOCK" >>"$rc_file"
fi
done
msg_ok "Configured Shell Integration"
msg_info "Verifying Installation"
$STD su - "$BREW_USER" -c 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew --version'
msg_ok "Homebrew Verified"
echo ""
msg_ok "Homebrew installed successfully"
msg_ok "Ready for user: ${BL}${BREW_USER}${CL}"
echo ""
echo -e "${TAB}${INFO} Usage: Switch to the brew user with a login shell:"
echo -e "${TAB} ${BL}su - ${BREW_USER}${CL}"
echo -e "${TAB} Then run: ${BL}brew install <package>${CL}"
echo -e "${TAB} Update with: ${BL}brew update${CL}"
}
# ==============================================================================
# MAIN
# ==============================================================================
header_info
if [[ -d "$INSTALL_PATH" ]]; then
msg_warn "Homebrew is already installed."
echo ""
read -r -p "${TAB}Uninstall Homebrew? (y/N): " uninstall_prompt
if [[ "${uninstall_prompt,,}" =~ ^(y|yes)$ ]]; then
uninstall
exit 0
fi
msg_warn "No action selected. Exiting."
exit 0
fi
# Fresh installation
msg_warn "Homebrew is not installed."
echo ""
echo -e "${TAB}${INFO} This will install:"
echo -e "${TAB} - Homebrew (Linuxbrew) package manager"
echo -e "${TAB} - Shell integration for the detected non-root user"
echo ""
read -r -p "${TAB}Install Homebrew? (y/N): " install_prompt
if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then
install
else
msg_warn "Installation cancelled. Exiting."
exit 0
fi

View File

@@ -3,7 +3,7 @@
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://komo.do/ | Github: https://github.com/mbecker20/komodo
# Source: https://komo.do/ | Github: https://github.com/moghtech/komodo
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 || apk update >/dev/null 2>&1
@@ -82,6 +82,7 @@ function update() {
msg_error "Failed to create backup of ${COMPOSE_BASENAME}!"
exit 235
}
cp "$COMPOSE_ENV" "${COMPOSE_ENV}.bak_$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
GITHUB_URL="https://raw.githubusercontent.com/moghtech/komodo/main/compose/${COMPOSE_BASENAME}"
if ! curl -fsSL "$GITHUB_URL" -o "$COMPOSE_FILE"; then
@@ -90,8 +91,29 @@ function update() {
exit 115
fi
if ! grep -qxF 'COMPOSE_KOMODO_BACKUPS_PATH=/etc/komodo/backups' "$COMPOSE_ENV"; then
sed -i '/^COMPOSE_KOMODO_IMAGE_TAG=latest$/a COMPOSE_KOMODO_BACKUPS_PATH=/etc/komodo/backups' "$COMPOSE_ENV"
# === v2 migration: image tag (latest is deprecated) ===
if grep -q '^COMPOSE_KOMODO_IMAGE_TAG=latest' "$COMPOSE_ENV"; then
msg_info "Migrating to Komodo v2 image tag"
sed -i 's/^COMPOSE_KOMODO_IMAGE_TAG=latest/COMPOSE_KOMODO_IMAGE_TAG=2/' "$COMPOSE_ENV"
msg_ok "Migrated image tag to :2"
fi
# === v2 migration: DB credential variable names ===
if grep -q '^KOMODO_DB_USERNAME=' "$COMPOSE_ENV"; then
msg_info "Migrating database credential variables"
sed -i 's/^KOMODO_DB_USERNAME=/KOMODO_DATABASE_USERNAME=/' "$COMPOSE_ENV"
sed -i 's/^KOMODO_DB_PASSWORD=/KOMODO_DATABASE_PASSWORD=/' "$COMPOSE_ENV"
msg_ok "Migrated DB credential variables"
fi
# === v2 migration: remove deprecated passkey (replaced by PKI) ===
if grep -q '^KOMODO_PASSKEY=' "$COMPOSE_ENV"; then
sed -i '/^KOMODO_PASSKEY=/d' "$COMPOSE_ENV"
fi
# === ensure backups path is set ===
if ! grep -q 'COMPOSE_KOMODO_BACKUPS_PATH=' "$COMPOSE_ENV"; then
echo 'COMPOSE_KOMODO_BACKUPS_PATH=/etc/komodo/backups' >>"$COMPOSE_ENV"
fi
$STD docker compose -p komodo -f "$COMPOSE_FILE" --env-file "$COMPOSE_ENV" pull
@@ -192,14 +214,12 @@ function install() {
DB_PASSWORD=$(openssl rand -base64 16 | tr -d '/+=')
ADMIN_PASSWORD=$(openssl rand -base64 8 | tr -d '/+=')
PASSKEY=$(openssl rand -base64 24 | tr -d '/+=')
WEBHOOK_SECRET=$(openssl rand -base64 24 | tr -d '/+=')
JWT_SECRET=$(openssl rand -base64 24 | tr -d '/+=')
sed -i "s/^KOMODO_DB_USERNAME=.*/KOMODO_DB_USERNAME=komodo_admin/" "$COMPOSE_ENV"
sed -i "s/^KOMODO_DB_PASSWORD=.*/KOMODO_DB_PASSWORD=${DB_PASSWORD}/" "$COMPOSE_ENV"
sed -i "s/^KOMODO_DATABASE_USERNAME=.*/KOMODO_DATABASE_USERNAME=komodo_admin/" "$COMPOSE_ENV"
sed -i "s/^KOMODO_DATABASE_PASSWORD=.*/KOMODO_DATABASE_PASSWORD=${DB_PASSWORD}/" "$COMPOSE_ENV"
sed -i "s/^KOMODO_INIT_ADMIN_PASSWORD=changeme/KOMODO_INIT_ADMIN_PASSWORD=${ADMIN_PASSWORD}/" "$COMPOSE_ENV"
sed -i "s/^KOMODO_PASSKEY=.*/KOMODO_PASSKEY=${PASSKEY}/" "$COMPOSE_ENV"
sed -i "s/^KOMODO_WEBHOOK_SECRET=.*/KOMODO_WEBHOOK_SECRET=${WEBHOOK_SECRET}/" "$COMPOSE_ENV"
sed -i "s/^KOMODO_JWT_SECRET=.*/KOMODO_JWT_SECRET=${JWT_SECRET}/" "$COMPOSE_ENV"
msg_ok "Configured environment"

View File

@@ -0,0 +1,173 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Tom Frenzel (tomfrenzel)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/CodeWithCJ/SparkyFitness
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)
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) 2>/dev/null || true
declare -f init_tool_telemetry &>/dev/null && init_tool_telemetry "sparkyfitness-garmin" "addon"
# Enable error handling
set -Eeuo pipefail
trap 'error_handler' ERR
load_functions
# ==============================================================================
# CONFIGURATION
# ==============================================================================
APP="SparkyFitness Garmin Microservice"
APP_TYPE="addon"
INSTALL_PATH="/opt/sparkyfitness-garmin"
CONFIG_PATH="/etc/sparkyfitness-garmin/.env"
SERVICE_PATH="/etc/systemd/system/sparkyfitness-garmin.service"
DEFAULT_PORT=8000
# ==============================================================================
# OS DETECTION
# ==============================================================================
if ! grep -qE 'ID=debian|ID=ubuntu' /etc/os-release 2>/dev/null; then
echo -e "${CROSS} Unsupported OS detected. This script only supports Debian and Ubuntu."
exit 238
fi
# ==============================================================================
# SparkyFitness LXC DETECTION
# ==============================================================================
if [[ ! -d /opt/sparkyfitness ]]; then
echo -e "${CROSS} No SparkyFitness installation detected. This addon must be installed within a container that already has SparkyFitness installed."
exit 238
fi
# ==============================================================================
# UNINSTALL
# ==============================================================================
function uninstall() {
msg_info "Uninstalling ${APP}"
systemctl disable --now sparkyfitness-garmin.service &>/dev/null || true
rm -rf "$SERVICE_PATH" "$CONFIG_PATH" "$INSTALL_PATH" ~/.sparkyfitness-garmin
msg_ok "${APP} has been uninstalled"
}
# ==============================================================================
# UPDATE
# ==============================================================================
function update() {
if check_for_gh_release "sparkyfitness-garmin" "CodeWithCJ/SparkyFitness"; then
PYTHON_VERSION="3.13" setup_uv
msg_info "Stopping service"
systemctl stop sparkyfitness-garmin.service &>/dev/null || true
msg_ok "Stopped service"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "sparkyfitness-garmin" "CodeWithCJ/SparkyFitness" "tarball" "latest" $INSTALL_PATH
msg_info "Starting service"
systemctl start sparkyfitness-garmin
msg_ok "Started service"
msg_ok "Updated successfully"
exit
fi
}
# ==============================================================================
# INSTALL
# ==============================================================================
function install() {
PYTHON_VERSION="3.13" setup_uv
fetch_and_deploy_gh_release "sparkyfitness-garmin" "CodeWithCJ/SparkyFitness" "tarball" "latest" $INSTALL_PATH
msg_info "Setting up ${APP}"
mkdir -p "/etc/sparkyfitness-garmin"
cp "/opt/sparkyfitness-garmin/docker/.env.example" $CONFIG_PATH
cd $INSTALL_PATH/SparkyFitnessGarmin
$STD uv venv --clear .venv
$STD uv pip install -r requirements.txt
sed -i -e "s|^#\?GARMIN_MICROSERVICE_URL=.*|GARMIN_MICROSERVICE_URL=http://${LOCAL_IP}:${DEFAULT_PORT}|" $CONFIG_PATH
cat <<EOF >/etc/systemd/system/sparkyfitness-garmin.service
[Unit]
Description=${APP}
After=network.target sparkyfitness-server.service
Requires=sparkyfitness-server.service
[Service]
Type=simple
WorkingDirectory=$INSTALL_PATH/SparkyFitnessGarmin
EnvironmentFile=$CONFIG_PATH
ExecStart=$INSTALL_PATH/SparkyFitnessGarmin/.venv/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port ${DEFAULT_PORT}
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now sparkyfitness-garmin
msg_ok "Set up ${APP} - reachable at http://${LOCAL_IP}:${DEFAULT_PORT}"
msg_ok "You might need to update the GARMIN_MICROSERVICE_URL in your SparkyFitness .env file to http://${LOCAL_IP}:${DEFAULT_PORT}"
}
# ==============================================================================
# MAIN
# ==============================================================================
header_info
ensure_usr_local_bin_persist
get_lxc_ip
# Handle type=update (called from update script)
if [[ "${type:-}" == "update" ]]; then
if [[ -d "$INSTALL_PATH" ]]; then
update
else
msg_error "${APP} is not installed. Nothing to update."
exit 233
fi
exit 0
fi
# 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} - UV (Python Version Manager)"
echo -e "${TAB} - SparkyFitness Garmin Microservice"
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

6
tools/headers/homebrew Normal file
View File

@@ -0,0 +1,6 @@
__ __
/ /_ ____ ____ ___ ___ / /_ ________ _ __
/ __ \/ __ \/ __ `__ \/ _ \/ __ \/ ___/ _ \ | /| / /
/ / / / /_/ / / / / / / __/ /_/ / / / __/ |/ |/ /
/_/ /_/\____/_/ /_/ /_/\___/_.___/_/ \___/|__/|__/

View File

@@ -0,0 +1,6 @@
_____ __ _______ __ ______ _ __ ____ _
/ ___/____ ____ ______/ /____ __/ ____(_) /_____ ___ __________ / ____/___ __________ ___ (_)___ / |/ (_)_____________ ________ ______ __(_)_______
\__ \/ __ \/ __ `/ ___/ //_/ / / / /_ / / __/ __ \/ _ \/ ___/ ___/ / / __/ __ `/ ___/ __ `__ \/ / __ \ / /|_/ / / ___/ ___/ __ \/ ___/ _ \/ ___/ | / / / ___/ _ \
___/ / /_/ / /_/ / / / ,< / /_/ / __/ / / /_/ / / / __(__ |__ ) / /_/ / /_/ / / / / / / / / / / / / / / / / / /__/ / / /_/ (__ ) __/ / | |/ / / /__/ __/
/____/ .___/\__,_/_/ /_/|_|\__, /_/ /_/\__/_/ /_/\___/____/____/ \____/\__,_/_/ /_/ /_/ /_/_/_/ /_/ /_/ /_/_/\___/_/ \____/____/\___/_/ |___/_/\___/\___/
/_/ /____/

View File

@@ -1,10 +1,23 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 tteck
# Copyright (c) 2021-2026 community-scripts ORG
# Author: tteck (tteckster)
# License: MIT
# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
function header_info {
# Source shared libraries
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
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/error_handler.func)
load_functions
catch_errors
APP="TurnKey LXC"
NSAPP="turnkey"
DIAGNOSTICS="no"
METHOD="default"
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
EXECUTION_ID="${RANDOM_UUID}"
header_info() {
clear
cat <<"EOF"
______ __ __ __ _ _______
@@ -15,281 +28,345 @@ function header_info {
EOF
}
set -euo pipefail
shopt -s expand_aliases
alias die='EXIT=$? LINE=$LINENO error_exit'
trap die ERR
function error_exit() {
trap - ERR
local DEFAULT='Unknown failure occured.'
local REASON="\e[97m${1:-$DEFAULT}\e[39m"
local FLAG="\e[91m[ERROR] \e[93m$EXIT@$LINE"
msg "$FLAG $REASON" 1>&2
[ ! -z ${CTID-} ] && cleanup_ctid
exit $EXIT
}
function warn() {
local REASON="\e[97m$1\e[39m"
local FLAG="\e[93m[WARNING]\e[39m"
msg "$FLAG $REASON"
}
function info() {
local REASON="$1"
local FLAG="\e[36m[INFO]\e[39m"
msg "$FLAG $REASON"
}
function msg() {
local TEXT="$1"
echo -e "$TEXT"
}
function validate_container_id() {
# Validate if a container ID is available (cluster-aware)
validate_container_id() {
local ctid="$1"
# Check if ID is numeric
if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then
return 1
[[ "$ctid" =~ ^[0-9]+$ ]] || return 1
# Cluster-wide check via pvesh
if command -v pvesh &>/dev/null; then
local cluster_ids
cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true)
if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then
return 1
fi
fi
# Check if config file exists for VM or LXC
# Local fallback
if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
return 1
fi
# Check if ID is used in LVM logical volumes
# Check all cluster nodes
if [[ -d "/etc/pve/nodes" ]]; then
for node_dir in /etc/pve/nodes/*/; do
if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then
return 1
fi
done
fi
# Check LVM volumes
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
return 1
fi
return 0
}
function get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid)}"
get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}"
while ! validate_container_id "$suggested_id"; do
suggested_id=$((suggested_id + 1))
done
echo "$suggested_id"
}
function cleanup_ctid() {
if pct status $CTID &>/dev/null; then
if [ "$(pct status $CTID | awk '{print $2}')" == "running" ]; then
pct stop $CTID
cleanup_ctid() {
if pct status "$CTID" &>/dev/null; then
if [[ "$(pct status "$CTID" | awk '{print $2}')" == "running" ]]; then
pct stop "$CTID"
fi
pct destroy $CTID
pct destroy "$CTID"
fi
}
select_storage() {
local class="$1" content content_label
case "$class" in
container)
content='rootdir'
content_label='Container'
;;
template)
content='vztmpl'
content_label='Container template'
;;
*)
msg_error "Invalid storage class '$class'"
return 1
;;
esac
local -a MENU=()
local MSG_MAX_LENGTH=0
while read -r line; do
local TAG TYPE FREE ITEM OFFSET=2
TAG=$(echo "$line" | awk '{print $1}')
TYPE=$(echo "$line" | awk '{printf "%-10s", $2}')
FREE=$(echo "$line" | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}')
ITEM=" Type: $TYPE Free: $FREE "
((${#ITEM} + OFFSET > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=$((${#ITEM} + OFFSET))
MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content "$content" | awk 'NR>1')
if [[ $((${#MENU[@]} / 3)) -eq 0 ]]; then
msg_error "'$content_label' needs to be selected for at least one storage location."
return 1
elif [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
printf '%s' "${MENU[0]}"
else
local STORAGE
while [[ -z "${STORAGE:+x}" ]]; do
STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
"Which storage pool for the ${content_label,,}?\n\n" \
16 $((MSG_MAX_LENGTH + 23)) 6 \
"${MENU[@]}" 3>&1 1>&2 2>&3) || exit_script
done
printf '%s' "$STORAGE"
fi
}
# ==============================================================================
# MAIN
# ==============================================================================
# Cleanup on error: destroy container, report telemetry, and restart monitor
turnkey_cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
# Report failure to telemetry
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
post_update_to_api "failed" "$exit_code" 2>/dev/null || true
fi
# Destroy failed container
if [[ -n "${CTID:-}" ]]; then
cleanup_ctid 2>/dev/null || true
fi
fi
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
systemctl start ping-instances.service 2>/dev/null || true
fi
}
trap turnkey_cleanup EXIT
# Stop Proxmox VE Monitor-All if running
if systemctl is-active -q ping-instances.service; then
systemctl stop ping-instances.service
fi
pve_check
shell_check
root_check
# Read diagnostics preference (same logic as build.func diagnostics_check)
DIAG_CONFIG="/usr/local/community-scripts/diagnostics"
if [[ -f "$DIAG_CONFIG" ]]; then
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' "$DIAG_CONFIG") || true
DIAGNOSTICS="${DIAGNOSTICS:-no}"
fi
header_info
whiptail --backtitle "Proxmox VE Helper Scripts" --title "TurnKey LXCs" --yesno "This will allow for the creation of one of the many TurnKey LXC Containers. Proceed?" 10 68
whiptail --backtitle "Proxmox VE Helper Scripts" --title "TurnKey LXCs" --yesno \
"This will allow for the creation of one of the many TurnKey LXC Containers. Proceed?" 10 68 || exit_script
# Update template catalog early so the menu reflects the latest available templates
msg_info "Updating LXC template list"
pveam update >/dev/null
msg_ok "Updated LXC template list"
# Build TurnKey selection menu dynamically from available templates
# Requires gawk for regex capture groups in match()
command -v gawk &>/dev/null || apt-get install -y gawk &>/dev/null
declare -A TURNKEY_TEMPLATES
TURNKEY_MENU=()
MSG_MAX_LENGTH=0
while read -r TAG ITEM; do
while IFS=$'\t' read -r TEMPLATE_FILE TAG ITEM; do
TURNKEY_TEMPLATES["$TAG"]="$TEMPLATE_FILE"
OFFSET=2
((${#ITEM} + OFFSET > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=${#ITEM}+OFFSET
((${#ITEM} + OFFSET > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=$((${#ITEM} + OFFSET))
TURNKEY_MENU+=("$TAG" "$ITEM " "OFF")
done < <(
cat <<EOF
ansible Ansible
bookstack BookStack
core Core
faveo-helpdesk Faveo Helpdesk
fileserver File Server
gallery Gallery
gameserver Game Server
gitea Gitea
gitlab GitLab
invoice-ninja Invoice Ninja
mediaserver Media Server
nextcloud Nextcloud
observium Observium
odoo Odoo
openldap OpenLDAP
openvpn OpenVPN
owncloud ownCloud
phpbb phpBB
torrentserver Torrent Server
wireguard WireGuard
wordpress Wordpress
zoneminder ZoneMinder
EOF
)
turnkey=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "TurnKey LXCs" --radiolist "\nSelect a TurnKey LXC to create:\n" 16 $((MSG_MAX_LENGTH + 58)) 6 "${TURNKEY_MENU[@]}" 3>&1 1>&2 2>&3 | tr -d '"')
[ -z "$turnkey" ] && {
whiptail --backtitle "Proxmox VE Helper Scripts" --title "No TurnKey LXC Selected" --msgbox "It appears that no TurnKey LXC container was selected" 10 68
msg "Done"
exit
}
done < <(pveam available -section turnkeylinux | gawk '{
tpl = $2
if (match(tpl, /debian-([0-9]+)-turnkey-([^_]+)_([^_]+)_/, m)) {
app = m[2]; deb = m[1]; ver = m[3]
display = app
gsub(/-/, " ", display)
n = split(display, words, " ")
display = ""
for (i = 1; i <= n; i++) {
words[i] = toupper(substr(words[i], 1, 1)) substr(words[i], 2)
display = display (i > 1 ? " " : "") words[i]
}
tag = app "-" deb
printf "%s\t%s\t%s | Debian %s | %s\n", tpl, tag, display, deb, ver
}
}' | sort -t$'\t' -k2,2)
# Setup script environment
if [[ ${#TURNKEY_MENU[@]} -eq 0 ]]; then
msg_error "No TurnKey templates found. Check your internet connection or template repository."
exit 1
fi
selected=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "TurnKey LXCs" --radiolist \
"\nSelect a TurnKey LXC to create:\n" 20 $((MSG_MAX_LENGTH + 58)) 12 \
"${TURNKEY_MENU[@]}" 3>&1 1>&2 2>&3 | tr -d '"') || exit_script
if [[ -z "$selected" ]]; then
whiptail --backtitle "Proxmox VE Helper Scripts" --title "No TurnKey LXC Selected" \
--msgbox "It appears that no TurnKey LXC container was selected" 10 68
exit_script
fi
# Extract template filename and app name from selection
TEMPLATE="${TURNKEY_TEMPLATES[$selected]}"
turnkey="${selected%-*}"
# Generate random password
PASS="$(openssl rand -base64 8)"
# Prompt user to confirm container ID
# Prompt for Container ID
NEXT_ID=$(pvesh get /cluster/nextid 2>/dev/null || echo 100)
while true; do
CTID=$(whiptail --backtitle "Container ID" --title "Choose the Container ID" --inputbox "Enter the container ID..." 8 40 $(pvesh get /cluster/nextid) 3>&1 1>&2 2>&3)
CTID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Container ID" \
--inputbox "Enter the container ID..." 8 40 "$NEXT_ID" 3>&1 1>&2 2>&3) || exit_script
# Check if user cancelled
[ -z "$CTID" ] && die "No Container ID selected"
if [[ -z "$CTID" ]]; then
msg_error "No Container ID selected"
exit_script
fi
# Validate Container ID
if ! validate_container_id "$CTID"; then
SUGGESTED_ID=$(get_valid_container_id "$CTID")
if whiptail --backtitle "Container ID" --title "ID Already In Use" --yesno "Container/VM ID $CTID is already in use.\n\nWould you like to use the next available ID ($SUGGESTED_ID)?" 10 58; then
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" --yesno \
"Container/VM ID $CTID is already in use.\n\nWould you like to use the next available ID ($SUGGESTED_ID)?" 10 58; then
CTID="$SUGGESTED_ID"
break
fi
# User declined, loop back to input
else
break
fi
done
# Prompt user to confirm Hostname
HOST_NAME=$(whiptail --backtitle "Hostname" --title "Choose the Hostname" --inputbox "Enter the containers Hostname..." 8 40 "turnkey-${turnkey}" 3>&1 1>&2 2>&3)
PCT_OPTIONS="
-features keyctl=1,nesting=1
-hostname $HOST_NAME
-tags community-script
-onboot 1
-cores 2
-memory 2048
-password $PASS
-net0 name=eth0,bridge=vmbr0,ip=dhcp
-unprivileged 1
"
DEFAULT_PCT_OPTIONS=(
-arch $(dpkg --print-architecture)
# Prompt for Hostname
HOST_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Hostname" \
--inputbox "Enter the container hostname..." 8 40 "turnkey-${turnkey}" 3>&1 1>&2 2>&3) || exit_script
# Container options
PCT_OPTIONS=(
-features keyctl=1,nesting=1
-hostname "$HOST_NAME"
-tags community-script
-onboot 1
-cores 2
-memory 2048
-password "$PASS"
-net0 name=eth0,bridge=vmbr0,ip=dhcp
-unprivileged 1
-arch "$(dpkg --print-architecture)"
)
# Set the CONTENT and CONTENT_LABEL variables
function select_storage() {
local CLASS=$1
local CONTENT
local CONTENT_LABEL
case $CLASS in
container)
CONTENT='rootdir'
CONTENT_LABEL='Container'
;;
template)
CONTENT='vztmpl'
CONTENT_LABEL='Container template'
;;
*) false || die "Invalid storage class." ;;
esac
# Query all storage locations
local -a MENU
while read -r line; do
local TAG=$(echo $line | awk '{print $1}')
local TYPE=$(echo $line | awk '{printf "%-10s", $2}')
local FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}')
local ITEM=" Type: $TYPE Free: $FREE "
local OFFSET=2
if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
local MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
fi
MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content $CONTENT | awk 'NR>1')
# Select storage location
if [ $((${#MENU[@]} / 3)) -eq 0 ]; then
warn "'$CONTENT_LABEL' needs to be selected for at least one storage location."
die "Unable to detect valid storage location."
elif [ $((${#MENU[@]} / 3)) -eq 1 ]; then
printf ${MENU[0]}
else
local STORAGE
while [ -z "${STORAGE:+x}" ]; do
STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
"Which storage pool would you like to use for the ${CONTENT_LABEL,,}?\n\n" \
16 $(($MSG_MAX_LENGTH + 23)) 6 \
"${MENU[@]}" 3>&1 1>&2 2>&3) || die "Menu aborted."
done
printf $STORAGE
fi
# Storage selection
TEMPLATE_STORAGE=$(select_storage template) || {
msg_error "Failed to select template storage"
exit 1
}
msg_ok "Using '${BL}${TEMPLATE_STORAGE}${CL}' for template storage"
# Get template storage
TEMPLATE_STORAGE=$(select_storage template)
info "Using '$TEMPLATE_STORAGE' for template storage."
CONTAINER_STORAGE=$(select_storage container) || {
msg_error "Failed to select container storage"
exit 1
}
msg_ok "Using '${BL}${CONTAINER_STORAGE}${CL}' for container storage"
# Get container storage
CONTAINER_STORAGE=$(select_storage container)
info "Using '$CONTAINER_STORAGE' for container storage."
# Update LXC template list
msg "Updating LXC template list..."
pveam update >/dev/null
# Get LXC template string
mapfile -t TEMPLATES < <(pveam available -section turnkeylinux | awk -v turnkey="${turnkey}" '$0 ~ turnkey {print $2}' | sort -t - -k 2 -V)
[ ${#TEMPLATES[@]} -gt 0 ] || die "Unable to find a template when searching for '${turnkey}'."
TEMPLATE="${TEMPLATES[-1]}"
# Download LXC template
if ! pveam list $TEMPLATE_STORAGE | grep -q $TEMPLATE; then
msg "Downloading LXC template (Patience)..."
pveam download $TEMPLATE_STORAGE $TEMPLATE >/dev/null ||
die "A problem occured while downloading the LXC template."
# Download template if not already cached
if ! pveam list "$TEMPLATE_STORAGE" | grep -q "$TEMPLATE"; then
msg_info "Downloading LXC template"
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null || {
msg_error "Failed to download LXC template '${TEMPLATE}'"
exit 1
}
msg_ok "Downloaded LXC template"
fi
# Create variable for 'pct' options
PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
[[ " ${PCT_OPTIONS[@]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8})
# Add rootfs if not specified
[[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "${CONTAINER_STORAGE}:${PCT_DISK_SIZE:-8}")
# Create LXC
msg "Creating LXC container..."
pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[@]} >/dev/null ||
die "A problem occured while trying to create container."
# Set telemetry variables for the selected turnkey
TELEMETRY_TYPE="turnkey"
NSAPP="turnkey-${turnkey}"
CT_TYPE=1
DISK_SIZE="${PCT_DISK_SIZE:-8}"
CORE_COUNT=2
RAM_SIZE=2048
var_os="turnkey"
var_version="${turnkey}"
# Save password
echo "TurnKey ${turnkey} password: ${PASS}" >>~/turnkey-${turnkey}.creds # file is located in the Proxmox root directory
# Report installation start to telemetry
post_to_api
# If turnkey is "OpenVPN", add access to the tun device
TUN_DEVICE_REQUIRED=("openvpn") # Setup this way in case future turnkeys also need tun access
# Create LXC container
msg_info "Creating LXC container"
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >/dev/null || {
msg_error "Failed to create container"
exit 1
}
msg_ok "Created LXC container (ID: ${BL}${CTID}${CL})"
# Save credentials securely
CREDS_FILE=~/turnkey-${turnkey}.creds
echo "TurnKey ${turnkey} password: ${PASS}" >>"$CREDS_FILE"
chmod 600 "$CREDS_FILE"
# Configure TUN device access for VPN-based turnkeys
TUN_DEVICE_REQUIRED=("openvpn")
if printf '%s\n' "${TUN_DEVICE_REQUIRED[@]}" | grep -qw "${turnkey}"; then
info "${turnkey} requires access to /dev/net/tun on the host. Modifying the container configuration to allow this."
echo "lxc.cgroup2.devices.allow: c 10:200 rwm" >>/etc/pve/lxc/${CTID}.conf
echo "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0" >>/etc/pve/lxc/${CTID}.conf
msg_info "Configuring TUN device access for ${turnkey}"
{
echo "lxc.cgroup2.devices.allow: c 10:200 rwm"
echo "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0"
} >>"/etc/pve/lxc/${CTID}.conf"
msg_ok "TUN device access configured"
sleep 5
fi
# Start container
msg "Starting LXC Container..."
msg_info "Starting LXC container"
pct start "$CTID"
msg_ok "Started LXC container"
sleep 10
# Get container IP
set +euo pipefail # Turn off error checking
max_attempts=5
attempt=1
# Detect container IP
msg_info "Detecting IP address"
IP=""
while [[ $attempt -le $max_attempts ]]; do
IP=$(pct exec $CTID ip a show dev eth0 | grep -oP 'inet \K[^/]+')
if [[ -n $IP ]]; then
for attempt in $(seq 1 5); do
IP=$(pct exec "$CTID" -- ip -4 a show dev eth0 2>/dev/null | grep -oP 'inet \K[^/]+' || true)
if [[ -n "$IP" ]]; then
break
else
warn "Attempt $attempt: IP address not found. Pausing for 5 seconds..."
sleep 5
((attempt++))
fi
[[ $attempt -lt 5 ]] && sleep 5
done
if [[ -z $IP ]]; then
warn "Maximum number of attempts reached. IP address not found."
if [[ -z "$IP" ]]; then
msg_warn "IP address not found after 5 attempts"
IP="NOT FOUND"
else
msg_ok "IP address: ${BL}${IP}${CL}"
fi
# Start Proxmox VE Monitor-All if available
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
systemctl start ping-instances.service
fi
# Report success to telemetry
post_update_to_api "done" "none"
# Success message
# Success summary
header_info
echo
info "LXC container '$CTID' was successfully created, and its IP address is ${IP}."
msg_ok "TurnKey ${BL}${turnkey}${CL} LXC container '${BL}${CTID}${CL}' was successfully created."
echo
info "Proceed to the LXC console to complete the setup."
echo -e " ${TAB}${YW}IP Address:${CL} ${BL}${IP}${CL}"
echo -e " ${TAB}${YW}Login:${CL} ${GN}root${CL}"
echo -e " ${TAB}${YW}Password:${CL} ${GN}${PASS}${CL}"
echo
info "login: root"
info "password: $PASS"
info "(credentials also stored in the root user's root directory in the 'turnkey-${turnkey}.creds' file.)"
echo -e " ${TAB}Proceed to the LXC console to complete the TurnKey setup."
echo -e " ${TAB}Credentials stored in: ${BL}~/turnkey-${turnkey}.creds${CL}"
echo

View File

@@ -551,7 +551,7 @@ qm set $VMID \
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -631,7 +631,7 @@ rm -f "$WORK_FILE"
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -568,7 +568,7 @@ fi
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -639,7 +639,7 @@ msg_ok "Resized disk"
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -622,7 +622,7 @@ qm set $VMID \
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -546,7 +546,7 @@ qm set $VMID \
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -605,7 +605,7 @@ msg_ok "Resized disk to ${DISK_SIZE}"
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -737,7 +737,7 @@ done
msg_info "Creating a OPNsense VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
-name $HN -tags proxmox-helper-scripts -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
-name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
pvesm alloc $STORAGE $VMID $DISK0 4M 1>&/dev/null
qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null
qm set $VMID \
@@ -750,7 +750,7 @@ qm resize $VMID scsi0 20G >/dev/null
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/michelroegl-brunner/ProxmoxVE/refs/heads/develop/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -560,7 +560,7 @@ qm set $VMID \
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -462,7 +462,7 @@ qm set $VMID \
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -610,7 +610,7 @@ fi
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -542,7 +542,7 @@ qm set $VMID \
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -544,7 +544,7 @@ qm set $VMID \
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -543,7 +543,7 @@ qm set $VMID \
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>

View File

@@ -590,7 +590,7 @@ qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>