Compare commits

..

9 Commits

Author SHA1 Message Date
CanbiZ (MickLesk)
3b44a8eb59 Remove: Booklore 2026-03-24 17:43:44 +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
9 changed files with 470 additions and 422 deletions

View File

@@ -428,6 +428,17 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
## 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))
## 2026-03-23
### 🚀 Updated Scripts

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 @@
____ __ __
/ __ )____ ____ / /__/ / ____ ________
/ __ / __ \/ __ \/ //_/ / / __ \/ ___/ _ \
/ /_/ / /_/ / /_/ / ,< / /___/ /_/ / / / __/
/_____/\____/\____/_/|_/_____/\____/_/ \___/

View File

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

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

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

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

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

@@ -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,343 @@ 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
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 | awk '{
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