diff --git a/.github/workflows/revision-bump.yml b/.github/workflows/revision-bump.yml.bak
similarity index 100%
rename from .github/workflows/revision-bump.yml
rename to .github/workflows/revision-bump.yml.bak
diff --git a/api/go.mod b/api/go.mod
index 9a800e283..044bc8428 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -1,6 +1,6 @@
module proxmox-api
-go 1.23.2
+go 1.24.0
require (
github.com/gorilla/mux v1.8.1
@@ -17,7 +17,7 @@ require (
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
- golang.org/x/crypto v0.35.0 // indirect
- golang.org/x/sync v0.11.0 // indirect
- golang.org/x/text v0.22.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
+ golang.org/x/sync v0.18.0 // indirect
+ golang.org/x/text v0.31.0 // indirect
)
diff --git a/api/go.sum b/api/go.sum
index f0a92be40..cb111bdb8 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -27,16 +27,16 @@ go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793Sqyh
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -48,8 +48,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/ct/alpine.sh b/ct/alpine.sh
index ea946a60e..51767cb01 100644
--- a/ct/alpine.sh
+++ b/ct/alpine.sh
@@ -7,9 +7,9 @@ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxV
APP="Alpine"
var_tags="${var_tags:-os;alpine}"
-var_cpu="${var_cpu:-1}"
-var_ram="${var_ram:-512}"
-var_disk="${var_disk:-1}"
+var_cpu="${var_cpu:-4}"
+var_ram="${var_ram:-4096}"
+var_disk="${var_disk:-5}"
var_os="${var_os:-alpine}"
var_version="${var_version:-3.22}"
var_unprivileged="${var_unprivileged:-1}"
diff --git a/ct/bookstack.sh b/ct/bookstack.sh
deleted file mode 100644
index 2034adbbc..000000000
--- a/ct/bookstack.sh
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: MickLesk (Canbiz)
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/BookStackApp/BookStack
-
-APP="Bookstack"
-var_tags="${var_tags:-organizer}"
-var_cpu="${var_cpu:-1}"
-var_ram="${var_ram:-1024}"
-var_disk="${var_disk:-4}"
-var_os="${var_os:-debian}"
-var_version="${var_version:-12}"
-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/bookstack ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
- if check_for_gh_release "bookstack" "BookStackApp/BookStack"; then
- msg_info "Stopping Apache2"
- systemctl stop apache2
- msg_ok "Services Stopped"
-
- msg_info "Backing up data"
- mv /opt/bookstack /opt/bookstack-backup
- msg_ok "Backup finished"
-
- setup_mariadb
- fetch_and_deploy_gh_release "bookstack" "BookStackApp/BookStack"
- PHP_MODULE="ldap,tidy,bz2,mysqli" PHP_FPM="YES" PHP_APACHE="YES" PHP_VERSION="8.3" setup_php
- setup_composer
-
- msg_info "Restoring backup"
- cp /opt/bookstack-backup/.env /opt/bookstack/.env
- [[ -d /opt/bookstack-backup/public/uploads ]] && cp -a /opt/bookstack-backup/public/uploads/. /opt/bookstack/public/uploads/
- [[ -d /opt/bookstack-backup/storage/uploads ]] && cp -a /opt/bookstack-backup/storage/uploads/. /opt/bookstack/storage/uploads/
- [[ -d /opt/bookstack-backup/themes ]] && cp -a /opt/bookstack-backup/themes/. /opt/bookstack/themes/
- msg_ok "Backup restored"
-
- msg_info "Configuring BookStack"
- cd /opt/bookstack
- export COMPOSER_ALLOW_SUPERUSER=1
- $STD composer install --no-dev
- $STD php artisan migrate --force
- chown www-data:www-data -R /opt/bookstack /opt/bookstack/bootstrap/cache /opt/bookstack/public/uploads /opt/bookstack/storage
- chmod -R 755 /opt/bookstack /opt/bookstack/bootstrap/cache /opt/bookstack/public/uploads /opt/bookstack/storage
- chmod -R 775 /opt/bookstack/storage /opt/bookstack/bootstrap/cache /opt/bookstack/public/uploads
- chmod -R 640 /opt/bookstack/.env
- msg_ok "Configured BookStack"
-
- msg_info "Starting Apache2"
- systemctl start apache2
- msg_ok "Started Apache2"
-
- msg_info "Cleaning Up"
- rm -rf /opt/bookstack-backup
- msg_ok "Cleaned"
- msg_ok "Updated Successfully"
- fi
- exit
-}
-start
-build_container
-description
-
-msg_ok "Completed Successfully!\n"
-echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
-echo -e "${INFO}${YW} Access it using the following URL:${CL}"
-echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"
diff --git a/ct/comfyui.sh b/ct/comfyui.sh
deleted file mode 100644
index 52354e45a..000000000
--- a/ct/comfyui.sh
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func)
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: jdacode
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/comfyanonymous/ComfyUI
-
-APP="ComfyUI"
-var_tags="${var_tags:-ai}"
-var_cpu="${var_cpu:-4}"
-var_ram="${var_ram:-8192}"
-var_disk="${var_disk:-25}"
-var_os="${var_os:-debian}"
-var_version="${var_version:-13}"
-var_unprivileged="${var_unprivileged:-1}"
-
-header_info "$APP"
-variables
-color
-catch_errors
-
-function update_script() {
- header_info
- check_container_storage
- check_container_resources
-
- if [[ ! -f /opt/${APP} ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
- msg_error "To update use the ${APP} Manager."
- 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}:8188${CL}"
diff --git a/ct/debian.sh b/ct/debian.sh
index 78010dc3d..198a0bf01 100644
--- a/ct/debian.sh
+++ b/ct/debian.sh
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func)
-# Copyright (c) 2021-2025 tteck
-# Author: tteck (tteckster)
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
-# Source: https://www.debian.org/
+# Source:
APP="Debian"
var_tags="${var_tags:-}"
@@ -30,9 +30,10 @@ function update_script() {
exit
fi
msg_info "Updating $APP LXC"
- $STD apt-get update
- $STD apt-get -y upgrade
+ $STD apt update
+ $STD apt upgrade -y
msg_ok "Updated $APP LXC"
+ cleanup_lxc
exit
}
diff --git a/ct/kanba.sh b/ct/deferred/kanba.sh
similarity index 100%
rename from ct/kanba.sh
rename to ct/deferred/kanba.sh
diff --git a/ct/dispatcharr.sh b/ct/dispatcharr.sh
deleted file mode 100644
index de33d7928..000000000
--- a/ct/dispatcharr.sh
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: ekke85
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/Dispatcharr/Dispatcharr
-
-APP="Dispatcharr"
-APP_NAME=${APP,,}
-var_tags="${var_tags:-media;arr}"
-var_cpu="${var_cpu:-1}"
-var_ram="${var_ram:-2048}"
-var_disk="${var_disk:-8}"
-var_os="${var_os:-debian}"
-var_version="${var_version:-13}"
-var_unprivileged="${var_unprivileged:-1}"
-
-header_info "$APP"
-variables
-color
-catch_errors
-
-function update_script() {
- header_info
- check_container_storage
- check_container_resources
-
- if [[ ! -d "/opt/dispatcharr" ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
-
- RELEASE=$(curl -fsSL https://api.github.com/repos/Dispatcharr/Dispatcharr/releases/latest | jq -r '.tag_name' | sed 's/^v//')
- if [[ "${RELEASE}" != "$(cat /opt/${APP}_version.txt)" ]] || [[ ! -f /opt/${APP}_version.txt ]]; then
- msg_ok "Starting update"
- APP_DIR="/opt/dispatcharr"
- APP_USER="dispatcharr"
- APP_GROUP="dispatcharr"
-
- msg_info "Stopping $APP"
- systemctl stop dispatcharr-celery
- systemctl stop dispatcharr-celerybeat
- systemctl stop dispatcharr-daphne
- systemctl stop dispatcharr
- msg_ok "Stopped $APP"
-
- msg_info "Creating Backup"
- BACKUP_FILE="/opt/dispatcharr_$(date +%F).tar.gz"
- msg_info "Source and Database backup"
- set -o allexport
- source /etc/$APP_NAME/$APP_NAME.env
- set +o allexport
- PGPASSWORD=$POSTGRES_PASSWORD pg_dump -U $POSTGRES_USER -h $POSTGRES_HOST $POSTGRES_DB >/opt/$POSTGRES_DB-$(date +%F).sql
- $STD tar -czf "$BACKUP_FILE" /opt/dispatcharr /opt/Dispatcharr_version.txt /opt/$POSTGRES_DB-$(date +%F).sql &>/dev/null
- msg_ok "Backup Created"
-
- msg_info "Updating $APP to v${RELEASE}"
- rm -rf /opt/dispatcharr
- fetch_and_deploy_gh_release "dispatcharr" "Dispatcharr/Dispatcharr"
- chown -R "$APP_USER:$APP_GROUP" "$APP_DIR"
- sed -i 's/program\[\x27channel_id\x27\]/program["channel_id"]/g' "${APP_DIR}/apps/output/views.py"
-
- msg_ok "Dispatcharr Updated to $RELEASE"
-
- msg_info "Creating Python Virtual Environment"
- cd $APP_DIR
- python3 -m venv env
- source env/bin/activate
- $STD pip install --upgrade pip
- $STD pip install -r requirements.txt
- $STD pip install gunicorn
- ln -sf /usr/bin/ffmpeg $APP_DIR/env/bin/ffmpeg
- msg_ok "Python Environment Setup"
-
- msg_info "Building Frontend"
- cd $APP_DIR/frontend
- $STD npm install --legacy-peer-deps
- $STD npm run build
- msg_ok "Built Frontend"
-
- msg_info "Running Django Migrations"
- cd $APP_DIR
- source env/bin/activate
- set -o allexport
- source /etc/$APP_NAME/$APP_NAME.env
- set +o allexport
- $STD python manage.py migrate --noinput
- $STD python manage.py collectstatic --noinput
- msg_ok "Migrations Complete"
-
- msg_info "Starting $APP"
- systemctl start dispatcharr-celery
- systemctl start dispatcharr-celerybeat
- systemctl start dispatcharr-daphne
- systemctl start dispatcharr
- msg_ok "Started $APP"
- echo "${RELEASE}" >"/opt/${APP}_version.txt"
-
- msg_info "Cleaning Up"
- rm -rf /opt/$POSTGRES_DB-$(date +%F).sql
- msg_ok "Cleanup Completed"
-
- msg_ok "Update Successful, Backup saved to $BACKUP_FILE"
-
- else
- msg_ok "No update required. ${APP} is already at v${RELEASE}"
- fi
- exit
-}
-
-start
-build_container
-description
-
-msg_ok "Completed Successfully!\n"
-echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
-echo -e "${INFO}${YW} Access it using the following URL:${CL}"
-echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"
diff --git a/ct/docker.sh b/ct/docker.sh
new file mode 100644
index 000000000..645114b41
--- /dev/null
+++ b/ct/docker.sh
@@ -0,0 +1,95 @@
+#!/usr/bin/env bash
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
+# Copyright (c) 2021-2025 tteck
+# Author: tteck (tteckster)
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://www.docker.com/
+
+APP="Docker"
+var_tags="${var_tags:-docker}"
+var_cpu="${var_cpu:-2}"
+var_ram="${var_ram:-2048}"
+var_disk="${var_disk:-4}"
+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
+
+ get_latest_release() {
+ curl -fsSL https://api.github.com/repos/"$1"/releases/latest | grep '"tag_name":' | cut -d'"' -f4
+ }
+
+ msg_info "Updating base system"
+ $STD apt update
+ $STD apt -y upgrade
+ msg_ok "Base system updated"
+
+ msg_info "Updating Docker Engine"
+ $STD apt install --only-upgrade -y docker-ce docker-ce-cli containerd.io
+ msg_ok "Docker Engine updated"
+
+ if [[ -f /usr/local/lib/docker/cli-plugins/docker-compose ]]; then
+ COMPOSE_BIN="/usr/local/lib/docker/cli-plugins/docker-compose"
+ COMPOSE_NEW_VERSION=$(get_latest_release "docker/compose")
+ msg_info "Updating Docker Compose to $COMPOSE_NEW_VERSION"
+ curl -fsSL "https://github.com/docker/compose/releases/download/${COMPOSE_NEW_VERSION}/docker-compose-$(uname -s)-$(uname -m)" \
+ -o "$COMPOSE_BIN"
+ chmod +x "$COMPOSE_BIN"
+ msg_ok "Docker Compose updated"
+ fi
+
+ if docker ps -a --format '{{.Names}}' | grep -q '^portainer$'; then
+ msg_info "Updating Portainer"
+ $STD docker pull portainer/portainer-ce:latest
+ $STD docker stop portainer && docker rm portainer
+ $STD docker volume create portainer_data >/dev/null 2>&1
+ $STD docker run -d \
+ -p 8000:8000 \
+ -p 9443:9443 \
+ --name=portainer \
+ --restart=always \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v portainer_data:/data \
+ portainer/portainer-ce:latest
+ msg_ok "Updated Portainer"
+ fi
+
+ if docker ps -a --format '{{.Names}}' | grep -q '^portainer_agent$'; then
+ msg_info "Updating Portainer Agent"
+ $STD docker pull portainer/agent:latest
+ $STD docker stop portainer_agent && docker rm portainer_agent
+ $STD docker run -d \
+ -p 9001:9001 \
+ --name=portainer_agent \
+ --restart=always \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v /var/lib/docker/volumes:/var/lib/docker/volumes \
+ portainer/agent
+ msg_ok "Updated Portainer Agent"
+ fi
+
+ msg_info "Cleaning up"
+ $STD apt-get -y autoremove
+ $STD apt-get -y autoclean
+ msg_ok "Cleanup complete"
+ msg_ok "Updated successfully!"
+ 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} If you installed Portainer, access it at the following URL:${CL}"
+echo -e "${TAB}${GATEWAY}${BGN}https://${IP}:9443${CL}"
diff --git a/ct/freepbx.sh b/ct/freepbx.sh
index 0552674bf..fd2395c91 100644
--- a/ct/freepbx.sh
+++ b/ct/freepbx.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-source <(curl -s https://raw.githubusercontent.com/vsc55/community-scripts-ProxmoxVED/refs/heads/freepbx/misc/build.func)
+source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/refs/heads/freepbx/misc/build.func)
# Copyright (c) 2021-2025 community-scripts ORG
# Author: Arian Nasr (arian-nasr)
# Updated by: Javier Pastor (vsc55)
diff --git a/ct/gitea-mirror.sh b/ct/gitea-mirror.sh
index 4c5fca198..ac227bcca 100644
--- a/ct/gitea-mirror.sh
+++ b/ct/gitea-mirror.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
# Copyright (c) 2021-2025 community-scripts ORG
# Author: CrazyWolf13
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
@@ -13,7 +13,6 @@ var_disk="${var_disk:-6}"
var_os="${var_os:-debian}"
var_version="${var_version:-12}"
var_unprivileged="${var_unprivileged:-1}"
-var_app_version="${var_app_version:-latest}"
header_info "$APP"
@@ -48,10 +47,10 @@ function update_script() {
fi
if [[ ! -f /opt/gitea-mirror.env ]]; then
- msg_info "Detected old Enviroment, updating files"
- APP_SECRET=$(openssl rand -base64 32)
- HOST_IP=$(hostname -I | awk '{print $1}')
- cat </opt/gitea-mirror.env
+ msg_info "Detected old Enviroment, updating files"
+ APP_SECRET=$(openssl rand -base64 32)
+ HOST_IP=$(hostname -I | awk '{print $1}')
+ cat </opt/gitea-mirror.env
# See here for config options: https://github.com/RayLabsHQ/gitea-mirror/blob/main/docs/ENVIRONMENT_VARIABLES.md
NODE_ENV=production
HOST=0.0.0.0
@@ -78,7 +77,7 @@ WantedBy=multi-user.target
EOF
systemctl daemon-reload
msg_ok "Old Enviroment fixed"
-fi
+ fi
if check_for_gh_release "gitea-mirror" "RayLabsHQ/gitea-mirror"; then
msg_info "Stopping Services"
@@ -90,15 +89,10 @@ fi
cp /opt/gitea-mirror/data/* /opt/gitea-mirror-backup/data/
msg_ok "Backup Data"
- msg_info "Installing Bun"
- export BUN_INSTALL=/opt/bun
- curl -fsSL https://bun.sh/install | $STD bash
- ln -sf /opt/bun/bin/bun /usr/local/bin/bun
- ln -sf /opt/bun/bin/bun /usr/local/bin/bunx
- msg_ok "Installed Bun"
+ NODE_VERSION="22" NODE_MODULES="bun" setup_nodejs
rm -rf /opt/gitea-mirror
- fetch_and_deploy_gh_release "gitea-mirror" "RayLabsHQ/gitea-mirror" "tarball" $var_app_version
+ fetch_and_deploy_gh_release "gitea-mirror" "RayLabsHQ/gitea-mirror"
msg_info "Updating and rebuilding ${APP}"
cd /opt/gitea-mirror
@@ -116,7 +110,7 @@ fi
msg_info "Starting Service"
systemctl start gitea-mirror
msg_ok "Service Started"
- msg_ok "Update Successfully"
+ msg_ok "Updated successfully!"
fi
exit
}
diff --git a/ct/hanko.sh b/ct/hanko.sh
deleted file mode 100644
index de3079c26..000000000
--- a/ct/hanko.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func)
-# Copyright (c) 2021-2025 tteck
-# Author: tteck (tteckster)
-# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
-# Source: https://www.debian.org/
-
-APP="Hanko"
-var_tags="${var_tags:-os}"
-var_cpu="${var_cpu:-1}"
-var_ram="${var_ram:-1024}"
-var_disk="${var_disk:-4}"
-var_os="${var_os:-debian}"
-var_version="${var_version:-12}"
-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 /var ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
- msg_info "Updating $APP LXC"
- $STD apt-get update
- $STD apt-get -y upgrade
- msg_ok "Updated $APP LXC"
- 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}:8000${CL}"
diff --git a/ct/headers/asterisk b/ct/headers/asterisk
new file mode 100644
index 000000000..ed4356862
--- /dev/null
+++ b/ct/headers/asterisk
@@ -0,0 +1,6 @@
+ ___ __ _ __
+ / | _____/ /____ _____(_)____/ /__
+ / /| | / ___/ __/ _ \/ ___/ / ___/ //_/
+ / ___ |(__ ) /_/ __/ / / (__ ) ,<
+/_/ |_/____/\__/\___/_/ /_/____/_/|_|
+
diff --git a/ct/headers/bookstack b/ct/headers/bookstack
deleted file mode 100644
index f68646662..000000000
--- a/ct/headers/bookstack
+++ /dev/null
@@ -1,6 +0,0 @@
- ____ __ __ __
- / __ )____ ____ / /_______/ /_____ ______/ /__
- / __ / __ \/ __ \/ //_/ ___/ __/ __ `/ ___/ //_/
- / /_/ / /_/ / /_/ / ,< (__ ) /_/ /_/ / /__/ ,<
-/_____/\____/\____/_/|_/____/\__/\__,_/\___/_/|_|
-
diff --git a/ct/headers/comfyui b/ct/headers/comfyui
deleted file mode 100644
index 5f3bfd60a..000000000
--- a/ct/headers/comfyui
+++ /dev/null
@@ -1,6 +0,0 @@
- ______ ____ __ ______
- / ____/___ ____ ___ / __/_ __/ / / / _/
- / / / __ \/ __ `__ \/ /_/ / / / / / // /
-/ /___/ /_/ / / / / / / __/ /_/ / /_/ // /
-\____/\____/_/ /_/ /_/_/ \__, /\____/___/
- /____/
diff --git a/ct/headers/dispatcharr b/ct/headers/dispatcharr
deleted file mode 100644
index a8ad53965..000000000
--- a/ct/headers/dispatcharr
+++ /dev/null
@@ -1,6 +0,0 @@
- ____ _ __ __
- / __ \(_)________ ____ _/ /______/ /_ ____ ___________
- / / / / / ___/ __ \/ __ `/ __/ ___/ __ \/ __ `/ ___/ ___/
- / /_/ / (__ ) /_/ / /_/ / /_/ /__/ / / / /_/ / / / /
-/_____/_/____/ .___/\__,_/\__/\___/_/ /_/\__,_/_/ /_/
- /_/
diff --git a/ct/headers/docker b/ct/headers/docker
new file mode 100644
index 000000000..907ffbaef
--- /dev/null
+++ b/ct/headers/docker
@@ -0,0 +1,6 @@
+ ____ __
+ / __ \____ _____/ /_____ _____
+ / / / / __ \/ ___/ //_/ _ \/ ___/
+ / /_/ / /_/ / /__/ ,< / __/ /
+/_____/\____/\___/_/|_|\___/_/
+
diff --git a/ct/headers/domain-locker b/ct/headers/domain-locker
new file mode 100644
index 000000000..0ab1a9b74
--- /dev/null
+++ b/ct/headers/domain-locker
@@ -0,0 +1,6 @@
+ ____ _ __ __
+ / __ \____ ____ ___ ____ _(_)___ / / ____ _____/ /_____ _____
+ / / / / __ \/ __ `__ \/ __ `/ / __ \______/ / / __ \/ ___/ //_/ _ \/ ___/
+ / /_/ / /_/ / / / / / / /_/ / / / / /_____/ /___/ /_/ / /__/ ,< / __/ /
+/_____/\____/_/ /_/ /_/\__,_/_/_/ /_/ /_____/\____/\___/_/|_|\___/_/
+
diff --git a/ct/headers/hanko b/ct/headers/hanko
deleted file mode 100644
index e823d45dc..000000000
--- a/ct/headers/hanko
+++ /dev/null
@@ -1,6 +0,0 @@
- __ __ __
- / / / /___ _____ / /______
- / /_/ / __ `/ __ \/ //_/ __ \
- / __ / /_/ / / / / ,< / /_/ /
-/_/ /_/\__,_/_/ /_/_/|_|\____/
-
diff --git a/ct/headers/jellyfin b/ct/headers/jellyfin
deleted file mode 100644
index d905c4dba..000000000
--- a/ct/headers/jellyfin
+++ /dev/null
@@ -1,6 +0,0 @@
- __ ____ _____
- / /__ / / /_ __/ __(_)___
- __ / / _ \/ / / / / / /_/ / __ \
-/ /_/ / __/ / / /_/ / __/ / / / /
-\____/\___/_/_/\__, /_/ /_/_/ /_/
- /____/
diff --git a/ct/headers/livebook b/ct/headers/livebook
deleted file mode 100644
index 6ff6b47ef..000000000
--- a/ct/headers/livebook
+++ /dev/null
@@ -1,6 +0,0 @@
- __ _ __ __
- / / (_) _____ / /_ ____ ____ / /__
- / / / / | / / _ \/ __ \/ __ \/ __ \/ //_/
- / /___/ /| |/ / __/ /_/ / /_/ / /_/ / ,<
-/_____/_/ |___/\___/_.___/\____/\____/_/|_|
-
diff --git a/ct/headers/metabase b/ct/headers/metabase
new file mode 100644
index 000000000..a98c3c699
--- /dev/null
+++ b/ct/headers/metabase
@@ -0,0 +1,6 @@
+ __ ___ __ __
+ / |/ /__ / /_____ _/ /_ ____ _________
+ / /|_/ / _ \/ __/ __ `/ __ \/ __ `/ ___/ _ \
+ / / / / __/ /_/ /_/ / /_/ / /_/ (__ ) __/
+/_/ /_/\___/\__/\__,_/_.___/\__,_/____/\___/
+
diff --git a/ct/headers/notesnook b/ct/headers/notesnook
deleted file mode 100644
index c5dc22073..000000000
--- a/ct/headers/notesnook
+++ /dev/null
@@ -1,6 +0,0 @@
- __ __
- ____ ____ / /____ _________ ____ ____ / /__
- / __ \/ __ \/ __/ _ \/ ___/ __ \/ __ \/ __ \/ //_/
- / / / / /_/ / /_/ __(__ ) / / / /_/ / /_/ / ,<
-/_/ /_/\____/\__/\___/____/_/ /_/\____/\____/_/|_|
-
diff --git a/ct/headers/omada b/ct/headers/omada
new file mode 100644
index 000000000..3629b79d1
--- /dev/null
+++ b/ct/headers/omada
@@ -0,0 +1,6 @@
+ ____ __
+ / __ \____ ___ ____ _____/ /___ _
+ / / / / __ `__ \/ __ `/ __ / __ `/
+/ /_/ / / / / / / /_/ / /_/ / /_/ /
+\____/_/ /_/ /_/\__,_/\__,_/\__,_/
+
diff --git a/ct/headers/passbolt b/ct/headers/passbolt
new file mode 100644
index 000000000..91f0ab71d
--- /dev/null
+++ b/ct/headers/passbolt
@@ -0,0 +1,6 @@
+ ____ __ ____
+ / __ \____ ___________/ /_ ____ / / /_
+ / /_/ / __ `/ ___/ ___/ __ \/ __ \/ / __/
+ / ____/ /_/ (__ |__ ) /_/ / /_/ / / /_
+/_/ \__,_/____/____/_.___/\____/_/\__/
+
diff --git a/ct/headers/patchmon b/ct/headers/patchmon
deleted file mode 100644
index 87d928deb..000000000
--- a/ct/headers/patchmon
+++ /dev/null
@@ -1,6 +0,0 @@
- ____ __ __ __ ___
- / __ \____ _/ /______/ /_ / |/ /___ ____
- / /_/ / __ `/ __/ ___/ __ \/ /|_/ / __ \/ __ \
- / ____/ /_/ / /_/ /__/ / / / / / / /_/ / / / /
-/_/ \__,_/\__/\___/_/ /_/_/ /_/\____/_/ /_/
-
diff --git a/ct/headers/postiz b/ct/headers/postiz
deleted file mode 100644
index 2c5c0ba81..000000000
--- a/ct/headers/postiz
+++ /dev/null
@@ -1,6 +0,0 @@
- ____ __ _
- / __ \____ _____/ /_(_)___
- / /_/ / __ \/ ___/ __/ /_ /
- / ____/ /_/ (__ ) /_/ / / /_
-/_/ \____/____/\__/_/ /___/
-
diff --git a/ct/headers/snowshare b/ct/headers/snowshare
new file mode 100644
index 000000000..160614e0c
--- /dev/null
+++ b/ct/headers/snowshare
@@ -0,0 +1,6 @@
+ _____ _____ __
+ / ___/____ ____ _ __/ ___// /_ ____ _________
+ \__ \/ __ \/ __ \ | /| / /\__ \/ __ \/ __ `/ ___/ _ \
+ ___/ / / / / /_/ / |/ |/ /___/ / / / / /_/ / / / __/
+/____/_/ /_/\____/|__/|__//____/_/ /_/\__,_/_/ \___/
+
diff --git a/ct/headers/upgopher b/ct/headers/upgopher
new file mode 100644
index 000000000..e1126d09c
--- /dev/null
+++ b/ct/headers/upgopher
@@ -0,0 +1,6 @@
+ __ __ __
+ / / / /___ ____ _____ ____ / /_ ___ _____
+ / / / / __ \/ __ `/ __ \/ __ \/ __ \/ _ \/ ___/
+/ /_/ / /_/ / /_/ / /_/ / /_/ / / / / __/ /
+\____/ .___/\__, /\____/ .___/_/ /_/\___/_/
+ /_/ /____/ /_/
diff --git a/ct/headers/web-check b/ct/headers/web-check
new file mode 100644
index 000000000..371062cb9
--- /dev/null
+++ b/ct/headers/web-check
@@ -0,0 +1,6 @@
+ __ __ __
+ _ _____ / /_ _____/ /_ ___ _____/ /__
+| | /| / / _ \/ __ \______/ ___/ __ \/ _ \/ ___/ //_/
+| |/ |/ / __/ /_/ /_____/ /__/ / / / __/ /__/ ,<
+|__/|__/\___/_.___/ \___/_/ /_/\___/\___/_/|_|
+
diff --git a/ct/jellyfin.sh b/ct/jellyfin.sh
deleted file mode 100644
index 4593ab21c..000000000
--- a/ct/jellyfin.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
-# Copyright (c) 2021-2025 tteck
-# Author: tteck (tteckster)
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://jellyfin.org/
-
-APP="Jellyfin"
-var_tags="${var_tags:-media}"
-var_cpu="${var_cpu:-2}"
-var_ram="${var_ram:-2048}"
-var_disk="${var_disk:-8}"
-var_os="${var_os:-ubuntu}"
-var_version="${var_version:-24.10}"
-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 /usr/lib/jellyfin ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
-
- msg_info "Updating Intel Dependencies"
- fetch_and_deploy_gh_release "intel-igc-core-2" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb"
- fetch_and_deploy_gh_release "intel-igc-opencl-2" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb"
- fetch_and_deploy_gh_release "intel-libgdgmm12" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb"
- fetch_and_deploy_gh_release "intel-opencl-icd" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb"
- msg_ok "Updated Intel Dependencies"
-
- msg_info "Updating ${APP} LXC"
- $STD apt-get update
- $STD apt-get -y upgrade
- $STD apt-get -y --with-new-pkgs upgrade jellyfin jellyfin-server
- msg_ok "Updated ${APP} LXC"
- 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}:8096${CL}"
diff --git a/ct/librenms.sh b/ct/librenms.sh
deleted file mode 100644
index aaa71a1af..000000000
--- a/ct/librenms.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func)
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: michelroegl-brunner
-# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
-# Source: https://librenms.org
-
-APP="Librenms"
-var_tags="${var_tags:-monitoring}"
-var_cpu="${var_cpu:-2}"
-var_ram="${var_ram:-2048}"
-var_disk="${var_disk:-4}"
-var_os="${var_os:-debian}"
-var_version="${var_version:-12}"
-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/librenms ]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
- msg_info "Updating ${APP} Installation"
- su librenms
- cd /opt/librenms
- ./daily.sh
- msg_ok "Updated ${APP} Installation"
-
- exit
-}
-
-start
-build_container
-desiption
-
-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}/openproject${CL}"
diff --git a/ct/livebook.sh b/ct/livebook.sh
deleted file mode 100755
index 3afc43531..000000000
--- a/ct/livebook.sh
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/refs/heads/main/misc/build.func)
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: dkuku
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/livebook-dev/livebook
-
-APP="Livebook"
-var_tags="${var_tags:-development}"
-var_disk="${var_disk:-4}"
-var_cpu="${var_cpu:-2}"
-var_ram="${var_ram:-1024}"
-var_os="${var_os:-ubuntu}"
-var_version="${var_version:-24.04}"
-var_unprivileged="${var_unprivileged:-1}"
-
-header_info "$APP"
-variables
-color
-catch_errors
-
-function update_script() {
- header_info
- check_container_storage
- check_container_resources
-
- if [[ ! -f /opt/livebook/.mix/escripts/livebook ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
-
- if check_for_gh_release "livebook" "livebook-dev/livebook"; then
- msg_info "Stopping ${APP}"
- systemctl stop livebook
- msg_info "Service stopped"
-
- msg_info "Updating container"
- $STD apt-get update
- $STD apt-get -y upgrade
- msg_ok "Updated container"
-
- msg_info "Updating ${APP}"
- source /opt/livebook/.env
- cd /opt/livebook
- $STD mix escript.install hex livebook --force
-
- chown -R livebook:livebook /opt/livebook /data
- systemctl start livebook
- msg_ok "Updated ${APP}"
- fi
- exit
-}
-
-start
-build_container
-description
-
-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}:8080${CL}"
diff --git a/ct/mealie.sh b/ct/mealie.sh
new file mode 100644
index 000000000..3829845d0
--- /dev/null
+++ b/ct/mealie.sh
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: MickLesk (CanbiZ)
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://mealie.io
+
+APP="Mealie"
+var_tags="${var_tags:-recipes}"
+var_cpu="${var_cpu:-5}"
+var_ram="${var_ram:-4096}"
+var_disk="${var_disk:-10}"
+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/mealie ]]; then
+ msg_error "No ${APP} Installation Found!"
+ exit
+ fi
+ if check_for_gh_release "mealie" "mealie-recipes/mealie"; then
+ PYTHON_VERSION="3.12" setup_uv
+ NODE_MODULE="yarn" NODE_VERSION="24" setup_nodejs
+
+ msg_info "Stopping Service"
+ systemctl stop mealie
+ msg_ok "Stopped Service"
+
+ msg_info "Backing up configuration"
+ mkdir -p /opt/mealie_bak
+ cp -f /opt/mealie/mealie.env /opt/mealie_bak/mealie.env.bak
+ cp -f /opt/mealie/start.sh /opt/mealie_bak/start.sh.bak
+ msg_ok "Backup completed"
+
+ CLEAN_INSTALL=1 fetch_and_deploy_gh_release "mealie" "mealie-recipes/mealie" "tarball" "latest" "/opt/mealie"
+
+ msg_info "Rebuilding Frontend"
+ export NUXT_TELEMETRY_DISABLED=1
+ cd /opt/mealie/frontend
+ $STD yarn install --prefer-offline --frozen-lockfile --non-interactive --production=false --network-timeout 1000000
+ $STD yarn generate
+ cp -r /opt/mealie/frontend/dist/* /opt/mealie/mealie/frontend/
+ msg_ok "Frontend rebuilt"
+
+ msg_info "Updating Python Dependencies"
+ cd /opt/mealie
+ $STD uv sync --frozen --extra pgsql
+ msg_ok "Dependencies updated"
+
+ msg_info "Restoring configuration"
+ grep -q "^SECRET=" /opt/mealie_bak/mealie.env.bak || echo "SECRET=$(openssl rand -hex 32)" >>/opt/mealie_bak/mealie.env.bak
+ grep -q "^MEALIE_HOME=" /opt/mealie_bak/mealie.env.bak || echo "MEALIE_HOME=/opt/mealie" >>/opt/mealie_bak/mealie.env.bak
+ grep -q "^NLTK_DATA=" /opt/mealie_bak/mealie.env.bak || echo "NLTK_DATA=/nltk_data" >>/opt/mealie_bak/mealie.env.bak
+
+ mv -f /opt/mealie_bak/mealie.env.bak /opt/mealie/mealie.env
+ mv -f /opt/mealie_bak/start.sh.bak /opt/mealie/start.sh
+ chmod +x /opt/mealie/start.sh
+ sed -i 's|exec .*|source /opt/mealie/.venv/bin/activate\nexec uv run mealie|' /opt/mealie/start.sh
+ msg_ok "Configuration restored"
+
+ msg_info "Starting Service"
+ systemctl start mealie
+ 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}:9000${CL}"
diff --git a/ct/metabase.sh b/ct/metabase.sh
new file mode 100644
index 000000000..d057b52b3
--- /dev/null
+++ b/ct/metabase.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: Slaviša Arežina (tremor021)
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://www.metabase.com/
+
+APP="Metabase"
+var_tags="${var_tags:-analytics}"
+var_cpu="${var_cpu:-2}"
+var_ram="${var_ram:-2048}"
+var_disk="${var_disk:-6}"
+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/metabase ]]; then
+ msg_error "No ${APP} Installation Found!"
+ exit
+ fi
+
+ if check_for_gh_release "metabase" "metabase/metabase"; then
+ msg_info "Stopping Service"
+ systemctl stop metabase
+ msg_info "Service stopped"
+
+ msg_info "Creating backup"
+ mv /opt/metabase/.env /opt
+ msg_ok "Created backup"
+
+ msg_info "Updating Metabase"
+ RELEASE=$(get_latest_github_release "metabase/metabase")
+ curl -fsSL "https://downloads.metabase.com/v${RELEASE}.x/metabase.jar" -o /opt/metabase/metabase.jar
+ echo $RELEASE >~/.metabase
+ msg_ok "Updated Metabase"
+
+ msg_info "Restoring backup"
+ mv /opt/.env /opt/metabase
+ msg_ok "Restored backup"
+
+ msg_info "Starting Service"
+ systemctl start metabase
+ 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}:3000${CL}"
diff --git a/ct/notesnook.sh b/ct/notesnook.sh
deleted file mode 100644
index 3d1fbb3cf..000000000
--- a/ct/notesnook.sh
+++ /dev/null
@@ -1,59 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: Slaviša Arežina (tremor021)
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/streetwriters/notesnook
-
-APP="notesnook"
-var_tags="${var_tags:-os}"
-var_cpu="${var_cpu:-2}"
-var_ram="${var_ram:-3072}"
-var_disk="${var_disk:-10}"
-var_os="${var_os:-debian}"
-var_version="${var_version:-12}"
-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/notesnook ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
-
- msg_info "Stopping Service"
- systemctl stop notesnook
- msg_ok "Stopped Service"
-
- msg_info "Updating ${APP} (Patience)"
- rm -rf /opt/notesnook
- fetch_and_deploy_gh_release "notesnook" "streetwriters/notesnook" "tarball"
- cd /opt/notesnook
- export NODE_OPTIONS="--max-old-space-size=2560"
- $STD npm install
- $STD npm run build:web
- msg_ok "Updated $APP"
-
- msg_info "Starting Service"
- systemctl start notesnook
- msg_ok "Started Service"
-
- msg_ok "Updated Successfully"
- 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}https://${IP}${CL}"
diff --git a/ct/patchmon.sh b/ct/patchmon.sh
deleted file mode 100644
index 0080df89c..000000000
--- a/ct/patchmon.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: vhsdream
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/PatchMon/PatchMon
-
-APP="PatchMon"
-APP_NAME=${APP,,}
-var_tags="${var_tags:-monitoring}"
-var_cpu="${var_cpu:-2}"
-var_ram="${var_ram:-2048}"
-var_disk="${var_disk:-4}"
-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/patchmon" ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
-
- NODE_VERSION="24" setup_nodejs
-
- if check_for_gh_release "PatchMon" "PatchMon/PatchMon"; then
-
- msg_info "Stopping $APP"
- systemctl stop patchmon-server
- msg_ok "Stopped $APP"
-
- msg_info "Creating Backup"
- cp /opt/patchmon/backend/.env /opt/backend.env
- cp /opt/patchmon/frontend/.env /opt/frontend.env
- msg_ok "Backup Created"
-
- rm -rf /opt/patchmon
- fetch_and_deploy_gh_release "PatchMon" "PatchMon/PatchMon" "tarball" "latest" "/opt/patchmon"
-
- msg_info "Updating ${APP}"
- cd /opt/patchmon
- export NODE_ENV=production
- $STD npm install --no-audit --no-fund --no-save --ignore-scripts
- cd /opt/patchmon/backend
- $STD npm install --no-audit --no-fund --no-save --ignore-scripts
- cd /opt/patchmon/frontend
- $STD npm install --include=dev --no-audit --no-fund --no-save --ignore-scripts
- $STD npm run build
- cd /opt/patchmon/backend
- mv /opt/backend.env /opt/patchmon/backend/.env
- mv /opt/frontend.env /opt/patchmon/frontend/.env
- $STD npx prisma migrate deploy
- $STD npx prisma generate
- msg_ok "Updated ${APP}"
-
- msg_info "Starting $APP"
- systemctl start patchmon-server
- msg_ok "Started $APP"
- fi
- exit
-}
-
-start
-build_container
-description
-
-msg_ok "Completed Successfully!\n"
-echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
-echo -e "${INFO}${YW} Access it using the following URL:${CL}"
-echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"
diff --git a/ct/postiz.sh b/ct/postiz.sh
deleted file mode 100644
index e53553677..000000000
--- a/ct/postiz.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env bash
-source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func)
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: Slaviša Arežina (tremor021)
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/Casvt/Kapowarr
-
-APP="Postiz"
-var_tags="${var_tags:-Arr}"
-var_cpu="${var_cpu:-2}"
-var_ram="${var_ram:-3072}"
-var_disk="${var_disk:-8}"
-var_os="${var_os:-debian}"
-var_version="${var_version:-12}"
-var_unprivileged="${var_unprivileged:-1}"
-
-header_info "$APP"
-variables
-color
-catch_errors
-
-function update_script() {
- header_info
- check_container_storage
- check_container_resources
-
- if [[ ! -f /etc/systemd/system/postiz.service ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
- RELEASE=$(curl -fsSL https://api.github.com/repos/Casvt/Kapowarr/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
- if [[ "${RELEASE}" != "$(cat $HOME/.kapowarr)" ]] || [[ ! -f $HOME/.kapowarr ]]; then
- msg_info "Stopping $APP"
- systemctl stop kapowarr
- msg_ok "Stopped $APP"
-
- msg_info "Creating Backup"
- mv /opt/kapowarr/db /opt/
- msg_ok "Backup Created"
-
- msg_info "Updating $APP to ${RELEASE}"
- fetch_and_deploy_gh_release "kapowarr" "Casvt/Kapowarr"
- mv /opt/db /opt/kapowarr
- msg_ok "Updated $APP to ${RELEASE}"
-
- msg_info "Starting $APP"
- systemctl start kapowarr
- msg_ok "Started $APP"
-
- msg_ok "Update Successful"
- else
- msg_ok "No update required. ${APP} is already at ${RELEASE}"
- 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}:5656${CL}"
diff --git a/ct/rybbit.sh b/ct/rybbit.sh
index e523bde2b..62447664c 100644
--- a/ct/rybbit.sh
+++ b/ct/rybbit.sh
@@ -11,7 +11,7 @@ var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-5}"
var_os="${var_os:-debian}"
-var_version="${var_version:-12}"
+var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
@@ -20,18 +20,18 @@ color
catch_errors
function update_script() {
- header_info
- check_container_storage
- check_container_resources
- if [[ ! -d /var ]]; then
- msg_error "No ${APP} Installation Found!"
- exit
- fi
- msg_info "Updating $APP LXC"
- $STD apt-get update
- $STD apt-get -y upgrade
- msg_ok "Updated $APP LXC"
+ header_info
+ check_container_storage
+ check_container_resources
+ if [[ ! -d /var ]]; then
+ msg_error "No ${APP} Installation Found!"
exit
+ fi
+ msg_info "Updating $APP LXC"
+ $STD apt-get update
+ $STD apt-get -y upgrade
+ msg_ok "Updated $APP LXC"
+ exit
}
start
diff --git a/ct/snowshare.sh b/ct/snowshare.sh
new file mode 100644
index 000000000..9241f44db
--- /dev/null
+++ b/ct/snowshare.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: TuroYT
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://github.com/TuroYT/snowshare
+
+APP="SnowShare"
+var_tags="${var_tags:-file-sharing}"
+var_cpu="${var_cpu:-1}"
+var_ram="${var_ram:-1024}"
+var_disk="${var_disk:-5}"
+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/snowshare ]]; then
+ msg_error "No ${APP} Installation Found!"
+ exit
+ fi
+
+ if check_for_gh_release "snowshare" "TuroYT/snowshare"; then
+ msg_info "Stopping Service"
+ systemctl stop snowshare
+ msg_ok "Stopped Service"
+
+ fetch_and_deploy_gh_release "snowshare" "TuroYT/snowshare"
+
+ msg_info "Updating Snowshare"
+ cd /opt/snowshare
+ $STD npm ci
+ $STD npx prisma generate
+ $STD npm run build
+ msg_ok "Updated Snowshare"
+
+ msg_info "Starting Service"
+ systemctl start snowshare
+ 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}:3000${CL}"
diff --git a/ct/web-check.sh b/ct/web-check.sh
new file mode 100644
index 000000000..bb6555503
--- /dev/null
+++ b/ct/web-check.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: CrazyWolf13
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://github.com/Lissy93/web-check
+
+APP="web-check"
+var_tags="${var_tags:-network;analysis}"
+var_cpu="${var_cpu:-2}"
+var_ram="${var_ram:-2048}"
+var_disk="${var_disk:-12}"
+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/web-check ]]; then
+ msg_error "No ${APP} Installation Found!"
+ exit
+ fi
+
+ if check_for_gh_release "web-check" "CrazyWolf13/web-check"; then
+ msg_info "Stopping Service"
+ systemctl stop web-check
+ msg_ok "Stopped Service"
+
+ msg_info "Creating backup"
+ mv /opt/web-check/.env /opt
+ msg_ok "Created backup"
+
+ NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs
+ CLEAN_INSTALL=1 fetch_and_deploy_gh_release "web-check" "CrazyWolf13/web-check"
+
+ msg_info "Building Web-Check"
+ cd /opt/web-check
+ $STD yarn install --frozen-lockfile --network-timeout 100000
+ $STD yarn build --production
+ rm -rf /var/lib/apt/lists/* /app/node_modules/.cache
+ msg_ok "Built Web-Check"
+
+ msg_info "Restoring backup"
+ mv /opt/.env /opt/web-check
+ msg_ok "Restored backup"
+
+ msg_info "Starting Service"
+ systemctl start web-check
+ 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}:3000${CL}"
diff --git a/docs/DEV_MODE.md b/docs/DEV_MODE.md
new file mode 100644
index 000000000..c9640d216
--- /dev/null
+++ b/docs/DEV_MODE.md
@@ -0,0 +1,532 @@
+# Dev Mode - Debugging & Development Guide
+
+Development modes provide powerful debugging and testing capabilities for container creation and installation processes.
+
+## Quick Start
+
+```bash
+# Single mode
+export dev_mode="motd"
+bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/ct/wallabag.sh)"
+
+# Multiple modes (comma-separated)
+export dev_mode="motd,keep,trace"
+bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/ct/wallabag.sh)"
+
+# Combine with verbose output
+export var_verbose="yes"
+export dev_mode="pause,logs"
+bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/ct/wallabag.sh)"
+```
+
+## Available Modes
+
+### 1. **motd** - Early SSH/MOTD Setup
+
+Sets up SSH access and MOTD **before** the main application installation.
+
+**Use Case**:
+
+- Quick access to container for manual debugging
+- Continue installation manually if something goes wrong
+- Verify container networking before main install
+
+**Behavior**:
+
+```
+✔ Container created
+✔ Network configured
+[DEV] Setting up MOTD and SSH before installation
+✔ [DEV] MOTD/SSH ready - container accessible
+# Container is now accessible via SSH while installation proceeds
+```
+
+**Combined with**: `keep`, `breakpoint`, `logs`
+
+---
+
+### 2. **keep** - Preserve Container on Failure
+
+Never delete the container when installation fails. Skips cleanup prompt.
+
+**Use Case**:
+
+- Repeated tests of the same installation
+- Debugging failed installations
+- Manual fix attempts
+
+**Behavior**:
+
+```
+✖ Installation failed in container 107 (exit code: 1)
+✔ Container creation log: /tmp/create-lxc-107-abc12345.log
+✔ Installation log: /tmp/install-lxc-107-abc12345.log
+
+🔧 [DEV] Keep mode active - container 107 preserved
+root@proxmox:~#
+```
+
+**Container remains**: `pct enter 107` to access and debug
+
+**Combined with**: `motd`, `trace`, `logs`
+
+---
+
+### 3. **trace** - Bash Command Tracing
+
+Enables `set -x` for complete command-line tracing. Shows every command before execution.
+
+**Use Case**:
+
+- Deep debugging of installation logic
+- Understanding script flow
+- Identifying where errors occur exactly
+
+**Behavior**:
+
+```
++(/opt/wallabag/bin/console): /opt/wallabag/bin/console cache:warmup
++(/opt/wallabag/bin/console): env APP_ENV=prod /opt/wallabag/bin/console cache:warmup
++(/opt/wallabag/bin/console): [[ -d /opt/wallabag/app/cache ]]
++(/opt/wallabag/bin/console): rm -rf /opt/wallabag/app/cache/*
+```
+
+**⚠️ Warning**: Exposes passwords and secrets in log output! Only use in isolated environments.
+
+**Log Output**: All trace output saved to logs (see `logs` mode)
+
+**Combined with**: `keep`, `pause`, `logs`
+
+---
+
+### 4. **pause** - Step-by-Step Execution
+
+Pauses after each major step (`msg_info`). Requires manual Enter press to continue.
+
+**Use Case**:
+
+- Inspect container state between steps
+- Understand what each step does
+- Identify which step causes problems
+
+**Behavior**:
+
+```
+⏳ Setting up Container OS
+[PAUSE] Press Enter to continue...
+⏳ Updating Container OS
+[PAUSE] Press Enter to continue...
+⏳ Installing Dependencies
+[PAUSE] Press Enter to continue...
+```
+
+**Between pauses**: You can open another terminal and inspect the container
+
+```bash
+# In another terminal while paused
+pct enter 107
+root@container:~# df -h # Check disk usage
+root@container:~# ps aux # Check running processes
+```
+
+**Combined with**: `motd`, `keep`, `logs`
+
+---
+
+### 5. **breakpoint** - Interactive Shell on Error
+
+Opens interactive shell inside the container when an error occurs instead of cleanup prompt.
+
+**Use Case**:
+
+- Live debugging in the actual container
+- Manual command testing
+- Inspect container state at point of failure
+
+**Behavior**:
+
+```
+✖ Installation failed in container 107 (exit code: 1)
+✔ Container creation log: /tmp/create-lxc-107-abc12345.log
+✔ Installation log: /tmp/install-lxc-107-abc12345.log
+
+🐛 [DEV] Breakpoint mode - opening shell in container 107
+Type 'exit' to return to host
+root@wallabag:~#
+
+# Now you can debug:
+root@wallabag:~# tail -f /root/.install-abc12345.log
+root@wallabag:~# mysql -u root -p$PASSWORD wallabag
+root@wallabag:~# apt-get install -y strace
+root@wallabag:~# exit
+
+Container 107 still running. Remove now? (y/N): n
+🔧 Container 107 kept for debugging
+```
+
+**Combined with**: `keep`, `logs`, `trace`
+
+---
+
+### 6. **logs** - Persistent Logging
+
+Saves all logs to `/var/log/community-scripts/` with timestamps. Logs persist even on successful installation.
+
+**Use Case**:
+
+- Post-mortem analysis
+- Performance analysis
+- Automated testing with log collection
+- CI/CD integration
+
+**Behavior**:
+
+```
+Logs location: /var/log/community-scripts/
+
+create-lxc-abc12345-20251117_143022.log (host-side creation)
+install-abc12345-20251117_143022.log (container-side installation)
+```
+
+**Access logs**:
+
+```bash
+# View creation log
+tail -f /var/log/community-scripts/create-lxc-*.log
+
+# Search for errors
+grep ERROR /var/log/community-scripts/*.log
+
+# Analyze performance
+grep "msg_info\|msg_ok" /var/log/community-scripts/create-*.log
+```
+
+**With trace mode**: Creates detailed trace of all commands
+
+```bash
+grep "^+" /var/log/community-scripts/install-*.log
+```
+
+**Combined with**: All other modes (recommended for CI/CD)
+
+---
+
+### 7. **dryrun** - Simulation Mode
+
+Shows all commands that would be executed without actually running them.
+
+**Use Case**:
+
+- Test script logic without making changes
+- Verify command syntax
+- Understand what will happen
+- Pre-flight checks
+
+**Behavior**:
+
+```
+[DRYRUN] apt-get update
+[DRYRUN] apt-get install -y curl
+[DRYRUN] mkdir -p /opt/wallabag
+[DRYRUN] cd /opt/wallabag
+[DRYRUN] git clone https://github.com/wallabag/wallabag.git .
+```
+
+**No actual changes made**: Container/system remains unchanged
+
+**Combined with**: `trace` (shows dryrun trace), `logs` (shows what would run)
+
+---
+
+## Mode Combinations
+
+### Development Workflow
+
+```bash
+# First test: See what would happen
+export dev_mode="dryrun,logs"
+bash -c "$(curl ...)"
+
+# Then test with tracing and pauses
+export dev_mode="pause,trace,logs"
+bash -c "$(curl ...)"
+
+# Finally full debug with early SSH access
+export dev_mode="motd,keep,breakpoint,logs"
+bash -c "$(curl ...)"
+```
+
+### CI/CD Integration
+
+```bash
+# Automated testing with full logging
+export dev_mode="logs"
+export var_verbose="yes"
+bash -c "$(curl ...)"
+
+# Capture logs for analysis
+tar czf installation-logs-$(date +%s).tar.gz /var/log/community-scripts/
+```
+
+### Production-like Testing
+
+```bash
+# Keep containers for manual verification
+export dev_mode="keep,logs"
+for i in {1..5}; do
+ bash -c "$(curl ...)"
+done
+
+# Inspect all created containers
+pct list
+pct enter 100
+```
+
+### Live Debugging
+
+```bash
+# SSH in early, step through installation, debug on error
+export dev_mode="motd,pause,breakpoint,keep"
+bash -c "$(curl ...)"
+```
+
+---
+
+## Environment Variables Reference
+
+### Dev Mode Variables
+
+- `dev_mode` (string): Comma-separated list of modes
+ - Format: `"motd,keep,trace"`
+ - Default: Empty (no dev modes)
+
+### Output Control
+
+- `var_verbose="yes"`: Show all command output (disables silent mode)
+ - Pairs well with: `trace`, `pause`, `logs`
+
+### Examples with vars
+
+```bash
+# Maximum verbosity and debugging
+export var_verbose="yes"
+export dev_mode="motd,trace,pause,logs"
+bash -c "$(curl ...)"
+
+# Silent debug (logs only)
+export dev_mode="keep,logs"
+bash -c "$(curl ...)"
+
+# Interactive debugging
+export var_verbose="yes"
+export dev_mode="motd,breakpoint"
+bash -c "$(curl ...)"
+```
+
+---
+
+## Troubleshooting with Dev Mode
+
+### "Installation failed at step X"
+
+```bash
+export dev_mode="pause,logs"
+# Step through until the failure point
+# Check container state between pauses
+pct enter 107
+```
+
+### "Password/credentials not working"
+
+```bash
+export dev_mode="motd,keep,trace"
+# With trace mode, see exact password handling (be careful with logs!)
+# Use motd to SSH in and test manually
+ssh root@container-ip
+```
+
+### "Permission denied errors"
+
+```bash
+export dev_mode="breakpoint,keep"
+# Get shell at failure point
+# Check file permissions, user context, SELinux status
+ls -la /path/to/file
+whoami
+```
+
+### "Networking issues"
+
+```bash
+export dev_mode="motd"
+# SSH in with motd mode before main install
+ssh root@container-ip
+ping 8.8.8.8
+nslookup example.com
+```
+
+### "Need to manually complete installation"
+
+```bash
+export dev_mode="motd,keep"
+# Container accessible via SSH while installation runs
+# After failure, SSH in and manually continue
+ssh root@container-ip
+# ... manual commands ...
+exit
+# Then use 'keep' mode to preserve container for inspection
+```
+
+---
+
+## Log Files Locations
+
+### Default (without `logs` mode)
+
+- Host creation: `/tmp/create-lxc-.log`
+- Container install: Copied to `/tmp/install-lxc--.log` on failure
+
+### With `logs` mode
+
+- Host creation: `/var/log/community-scripts/create-lxc--.log`
+- Container install: `/var/log/community-scripts/install--.log`
+
+### View logs
+
+```bash
+# Tail in real-time
+tail -f /var/log/community-scripts/*.log
+
+# Search for errors
+grep -r "exit code [1-9]" /var/log/community-scripts/
+
+# Filter by session
+grep "ed563b19" /var/log/community-scripts/*.log
+```
+
+---
+
+## Best Practices
+
+### ✅ DO
+
+- Use `logs` mode for CI/CD and automated testing
+- Use `motd` for early SSH access during long installations
+- Use `pause` when learning the installation flow
+- Use `trace` when debugging logic issues (watch for secrets!)
+- Combine modes for comprehensive debugging
+- Archive logs after successful tests
+
+### ❌ DON'T
+
+- Use `trace` in production or with untrusted networks (exposes secrets)
+- Leave `keep` mode enabled for unattended scripts (containers accumulate)
+- Use `dryrun` and expect actual changes
+- Commit `dev_mode` exports to production deployment scripts
+- Use `breakpoint` in non-interactive environments (will hang)
+
+---
+
+## Examples
+
+### Example 1: Debug a Failed Installation
+
+```bash
+# Initial test to see the failure
+export dev_mode="keep,logs"
+bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/ct/wallabag.sh)"
+
+# Container 107 kept, check logs
+tail /var/log/community-scripts/install-*.log
+
+# SSH in to debug
+pct enter 107
+root@wallabag:~# cat /root/.install-*.log | tail -100
+root@wallabag:~# apt-get update # Retry the failing command
+root@wallabag:~# exit
+
+# Re-run with manual step-through
+export dev_mode="motd,pause,keep"
+bash -c "$(curl ...)"
+```
+
+### Example 2: Verify Installation Steps
+
+```bash
+export dev_mode="pause,logs"
+export var_verbose="yes"
+bash -c "$(curl ...)"
+
+# Press Enter through each step
+# Monitor container in another terminal
+# pct enter 107
+# Review logs in real-time
+```
+
+### Example 3: CI/CD Pipeline Integration
+
+```bash
+#!/bin/bash
+export dev_mode="logs"
+export var_verbose="no"
+
+for app in wallabag nextcloud wordpress; do
+ echo "Testing $app installation..."
+ APP="$app" bash -c "$(curl ...)" || {
+ echo "FAILED: $app"
+ tar czf logs-$app.tar.gz /var/log/community-scripts/
+ exit 1
+ }
+ echo "SUCCESS: $app"
+done
+
+echo "All installations successful"
+tar czf all-logs.tar.gz /var/log/community-scripts/
+```
+
+---
+
+## Advanced Usage
+
+### Custom Log Analysis
+
+```bash
+# Extract all errors
+grep "ERROR\|exit code [1-9]" /var/log/community-scripts/*.log
+
+# Performance timeline
+grep "^$(date +%Y-%m-%d)" /var/log/community-scripts/*.log | grep "msg_"
+
+# Memory usage during install
+grep "free\|available" /var/log/community-scripts/*.log
+```
+
+### Integration with External Tools
+
+```bash
+# Send logs to Elasticsearch
+curl -X POST "localhost:9200/installation-logs/_doc" \
+ -H 'Content-Type: application/json' \
+ -d @/var/log/community-scripts/install-*.log
+
+# Archive for compliance
+tar czf installation-records-$(date +%Y%m).tar.gz \
+ /var/log/community-scripts/
+gpg --encrypt installation-records-*.tar.gz
+```
+
+---
+
+## Support & Issues
+
+When reporting installation issues, always include:
+
+```bash
+# Collect all relevant information
+export dev_mode="logs"
+# Run the failing installation
+# Then provide:
+tar czf debug-logs.tar.gz /var/log/community-scripts/
+```
+
+Include the `debug-logs.tar.gz` when reporting issues for better diagnostics.
diff --git a/docs/EXIT_CODES.md b/docs/EXIT_CODES.md
new file mode 100644
index 000000000..7916c4a1c
--- /dev/null
+++ b/docs/EXIT_CODES.md
@@ -0,0 +1,298 @@
+# Exit Code Reference
+
+Comprehensive documentation of all exit codes used in ProxmoxVED scripts.
+
+## Table of Contents
+
+- [Generic/Shell Errors (1-255)](#genericshell-errors)
+- [Package Manager Errors (100-101, 255)](#package-manager-errors)
+- [Node.js/npm Errors (243-254)](#nodejsnpm-errors)
+- [Python/pip Errors (210-212)](#pythonpip-errors)
+- [Database Errors (231-254)](#database-errors)
+- [Proxmox Custom Codes (200-231)](#proxmox-custom-codes)
+
+---
+
+## Generic/Shell Errors
+
+Standard Unix/Linux exit codes used across all scripts.
+
+| Code | Description | Common Causes | Solutions |
+| ------- | --------------------------------------- | ----------------------------------------- | ---------------------------------------------- |
+| **1** | General error / Operation not permitted | Permission denied, general failure | Check user permissions, run as root if needed |
+| **2** | Misuse of shell builtins | Syntax error in script | Review script syntax, check bash version |
+| **126** | Command cannot execute | Permission problem, not executable | `chmod +x script.sh` or check file permissions |
+| **127** | Command not found | Missing binary, wrong PATH | Install required package, check PATH variable |
+| **128** | Invalid argument to exit | Invalid exit code passed | Use exit codes 0-255 only |
+| **130** | Terminated by Ctrl+C (SIGINT) | User interrupted script | Expected behavior, no action needed |
+| **137** | Killed (SIGKILL) | Out of memory, forced termination | Check memory usage, increase RAM allocation |
+| **139** | Segmentation fault | Memory access violation, corrupted binary | Reinstall package, check system stability |
+| **143** | Terminated (SIGTERM) | Graceful shutdown signal | Expected during container stops |
+
+---
+
+## Package Manager Errors
+
+APT, DPKG, and package installation errors.
+
+| Code | Description | Common Causes | Solutions |
+| ------- | -------------------------- | --------------------------------------- | ------------------------------------------------- |
+| **100** | APT: Package manager error | Broken packages, dependency conflicts | `apt --fix-broken install`, `dpkg --configure -a` |
+| **101** | APT: Configuration error | Malformed sources.list, bad repo config | Check `/etc/apt/sources.list`, run `apt update` |
+| **255** | DPKG: Fatal internal error | Corrupted package database | `dpkg --configure -a`, restore from backup |
+
+---
+
+## Node.js/npm Errors
+
+Node.js runtime and package manager errors.
+
+| Code | Description | Common Causes | Solutions |
+| ------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------- |
+| **243** | Node.js: Out of memory | JavaScript heap exhausted | Increase `--max-old-space-size`, optimize code |
+| **245** | Node.js: Invalid command-line option | Wrong Node.js flags | Check Node.js version, verify CLI options |
+| **246** | Node.js: Internal JavaScript Parse Error | Syntax error in JS code | Review JavaScript syntax, check dependencies |
+| **247** | Node.js: Fatal internal error | Node.js runtime crash | Update Node.js, check for known bugs |
+| **248** | Node.js: Invalid C++ addon / N-API failure | Native module incompatibility | Rebuild native modules, update packages |
+| **249** | Node.js: Inspector error | Debug/inspect protocol failure | Disable inspector, check port conflicts |
+| **254** | npm/pnpm/yarn: Unknown fatal error | Package manager crash | Clear cache, reinstall package manager |
+
+---
+
+## Python/pip Errors
+
+Python runtime and package installation errors.
+
+| Code | Description | Common Causes | Solutions |
+| ------- | ------------------------------------ | --------------------------------------- | -------------------------------------------------------- |
+| **210** | Python: Virtualenv missing or broken | venv not created, corrupted environment | `python3 -m venv venv`, recreate virtualenv |
+| **211** | Python: Dependency resolution failed | Conflicting package versions | Use `pip install --upgrade`, check requirements.txt |
+| **212** | Python: Installation aborted | EXTERNALLY-MANAGED, permission denied | Use `--break-system-packages` or venv, check permissions |
+
+---
+
+## Database Errors
+
+### PostgreSQL (231-234)
+
+| Code | Description | Common Causes | Solutions |
+| ------- | ----------------------- | ---------------------------------- | ----------------------------------------------------- |
+| **231** | Connection failed | Server not running, wrong socket | `systemctl start postgresql`, check connection string |
+| **232** | Authentication failed | Wrong credentials | Verify username/password, check `pg_hba.conf` |
+| **233** | Database does not exist | Database not created | `CREATE DATABASE`, restore from backup |
+| **234** | Fatal error in query | Syntax error, constraint violation | Review SQL syntax, check constraints |
+
+### MySQL/MariaDB (241-244)
+
+| Code | Description | Common Causes | Solutions |
+| ------- | ----------------------- | ---------------------------------- | ---------------------------------------------------- |
+| **241** | Connection failed | Server not running, wrong socket | `systemctl start mysql`, check connection parameters |
+| **242** | Authentication failed | Wrong credentials | Verify username/password, grant privileges |
+| **243** | Database does not exist | Database not created | `CREATE DATABASE`, restore from backup |
+| **244** | Fatal error in query | Syntax error, constraint violation | Review SQL syntax, check constraints |
+
+### MongoDB (251-254)
+
+| Code | Description | Common Causes | Solutions |
+| ------- | --------------------- | -------------------- | ------------------------------------------ |
+| **251** | Connection failed | Server not running | `systemctl start mongod`, check port 27017 |
+| **252** | Authentication failed | Wrong credentials | Verify username/password, create user |
+| **253** | Database not found | Database not created | Database auto-created on first write |
+| **254** | Fatal query error | Invalid query syntax | Review MongoDB query syntax |
+
+---
+
+## Proxmox Custom Codes
+
+Custom exit codes specific to ProxmoxVED scripts.
+
+### Container Creation Errors (200-209)
+
+| Code | Description | Common Causes | Solutions |
+| ------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
+| **200** | Failed to create lock file | Permission denied, disk full | Check `/tmp` permissions, free disk space |
+| **203** | Missing CTID variable | Script configuration error | Set CTID in script or via prompt |
+| **204** | Missing PCT_OSTYPE variable | Template selection failed | Verify template availability |
+| **205** | Invalid CTID (<100) | CTID below minimum value | Use CTID ≥ 100 (1-99 reserved for Proxmox) |
+| **206** | CTID already in use | Container/VM with same ID exists | Check `pct list` and `/etc/pve/lxc/`, use different ID |
+| **207** | Password contains unescaped special characters | Special chars like `-`, `/`, `\`, `*` at start/end | Avoid leading special chars, use alphanumeric passwords |
+| **208** | Invalid configuration | DNS format (`.home` vs `home`), MAC format (`-` vs `:`) | Remove leading dots from DNS, use `:` in MAC addresses |
+| **209** | Container creation failed | Multiple possible causes | Check logs in `/tmp/pct_create_*.log`, verify template |
+
+### Cluster & Storage Errors (210, 214, 217)
+
+| Code | Description | Common Causes | Solutions |
+| ------- | --------------------------------- | ---------------------------------- | ----------------------------------------------------------- |
+| **210** | Cluster not quorate | Cluster nodes down, network issues | Check cluster status: `pvecm status`, fix node connectivity |
+| **211** | Timeout waiting for template lock | Concurrent download in progress | Wait for other download to complete (60s timeout) |
+| **214** | Not enough storage space | Disk full, quota exceeded | Free disk space, increase storage allocation |
+| **217** | Storage does not support rootdir | Wrong storage type selected | Use storage supporting containers (dir, zfspool, lvm-thin) |
+
+### Container Verification Errors (215-216)
+
+| Code | Description | Common Causes | Solutions |
+| ------- | -------------------------------- | -------------------------------- | --------------------------------------------------------- |
+| **215** | Container created but not listed | Ghost state, incomplete creation | Check `/etc/pve/lxc/CTID.conf`, remove manually if needed |
+| **216** | RootFS entry missing in config | Incomplete container creation | Delete container, retry creation |
+
+### Template Errors (218, 220-223, 225)
+
+| Code | Description | Common Causes | Solutions |
+| ------- | ----------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------- |
+| **218** | Template file corrupted or incomplete | Download interrupted, file <1MB, invalid archive | Delete template, run `pveam update && pveam download` |
+| **220** | Unable to resolve template path | Template storage not accessible | Check storage availability, verify permissions |
+| **221** | Template file exists but not readable | Permission denied | `chmod 644 template.tar.zst`, check storage permissions |
+| **222** | Template download failed after 3 attempts | Network issues, storage problems | Check internet connectivity, verify storage space |
+| **223** | Template not available after download | Storage sync issue, I/O delay | Wait a few seconds, verify storage is mounted |
+| **225** | No template available for OS/Version | Unsupported OS version, catalog outdated | Run `pveam update`, check `pveam available -section system` |
+
+### LXC Stack Errors (231)
+
+| Code | Description | Common Causes | Solutions |
+| ------- | ------------------------------ | ------------------------------------------- | -------------------------------------------- |
+| **231** | LXC stack upgrade/retry failed | Outdated `pve-container`, Debian 13.1 issue | See [Debian 13.1 Fix Guide](#debian-131-fix) |
+
+---
+
+## Special Case: Debian 13.1 "unsupported version" Error
+
+### Problem
+
+```
+TASK ERROR: unable to create CT 129 - unsupported debian version '13.1'
+```
+
+### Root Cause
+
+Outdated `pve-container` package doesn't recognize Debian 13 (Trixie).
+
+### Solutions
+
+#### Option 1: Full System Upgrade (Recommended)
+
+```bash
+apt update
+apt full-upgrade -y
+reboot
+```
+
+Verify fix:
+
+```bash
+dpkg -l pve-container
+# PVE 8: Should show 5.3.3+
+# PVE 9: Should show 6.0.13+
+```
+
+#### Option 2: Update Only pve-container
+
+```bash
+apt update
+apt install --only-upgrade pve-container -y
+```
+
+**Warning:** If Proxmox fails to boot after this, your system was inconsistent. Perform Option 1 instead.
+
+#### Option 3: Verify Repository Configuration
+
+Many users disable Enterprise repos but forget to add no-subscription repos.
+
+**For PVE 9 (Trixie):**
+
+```bash
+cat /etc/apt/sources.list.d/pve-no-subscription.list
+```
+
+Should contain:
+
+```
+deb http://download.proxmox.com/debian/pve trixie pve-no-subscription
+deb http://download.proxmox.com/debian/ceph-squid trixie no-subscription
+```
+
+**For PVE 8 (Bookworm):**
+
+```
+deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription
+deb http://download.proxmox.com/debian/ceph-quincy bookworm no-subscription
+```
+
+Then:
+
+```bash
+apt update
+apt full-upgrade -y
+```
+
+### Reference
+
+Official discussion: [GitHub #8126](https://github.com/community-scripts/ProxmoxVE/discussions/8126)
+
+---
+
+## Troubleshooting Tips
+
+### Finding Error Details
+
+1. **Check logs:**
+
+ ```bash
+ tail -n 50 /tmp/pct_create_*.log
+ ```
+
+2. **Enable verbose mode:**
+
+ ```bash
+ bash -x script.sh # Shows every command executed
+ ```
+
+3. **Check container status:**
+
+ ```bash
+ pct list
+ pct status CTID
+ ```
+
+4. **Verify storage:**
+ ```bash
+ pvesm status
+ df -h
+ ```
+
+### Common Patterns
+
+- **Exit 0 with error message:** Configuration validation failed (check DNS, MAC, password format)
+- **Exit 206 but container not visible:** Ghost container state - check `/etc/pve/lxc/` manually
+- **Exit 209 generic error:** Check `/tmp/pct_create_*.log` for specific `pct create` failure reason
+- **Exit 218 or 222:** Template issues - delete and re-download template
+
+---
+
+## Quick Reference Chart
+
+| Exit Code Range | Category | Typical Issue |
+| --------------- | ------------------ | ------------------------------------------- |
+| 1-2, 126-143 | Shell/System | Permissions, signals, missing commands |
+| 100-101, 255 | Package Manager | APT/DPKG errors, broken packages |
+| 200-209 | Container Creation | CTID, password, configuration |
+| 210-217 | Storage/Cluster | Disk space, quorum, storage type |
+| 218-225 | Templates | Download, corruption, availability |
+| 231-254 | Databases/Runtime | PostgreSQL, MySQL, MongoDB, Node.js, Python |
+
+---
+
+## Contributing
+
+Found an undocumented exit code or have a solution to share? Please:
+
+1. Open an issue on [GitHub](https://github.com/community-scripts/ProxmoxVED/issues)
+2. Include:
+ - Exit code number
+ - Error message
+ - Steps to reproduce
+ - Solution that worked for you
+
+---
+
+_Last updated: November 2025_
+_ProxmoxVED Version: 2.x_
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 04b1ca67d..132da5f05 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10202,9 +10202,9 @@
"license": "MIT"
},
"node_modules/vite": {
- "version": "6.3.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
- "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/frontend/public/json/comfyui.json b/frontend/public/json/comfyui.json
deleted file mode 100644
index 95e3bd381..000000000
--- a/frontend/public/json/comfyui.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "name": "ComfyUI",
- "slug": "comfyui",
- "categories": [
- 20
- ],
- "date_created": "2025-08-01",
- "type": "ct",
- "updateable": true,
- "privileged": false,
- "config_path": "/opt",
- "interface_port": 8188,
- "documentation": "https://github.com/comfyanonymous/ComfyUI",
- "website": "https://www.comfy.org/",
- "logo": "https://framerusercontent.com/images/3cNQMWKzIhIrQ5KErBm7dSmbd2w.png",
- "description": "ComfyUI is a node-based interface and inference engine for generative AI. Users can combine various AI models and operations through nodes to achieve highly customizable and controllable content generation.",
- "install_methods": [
- {
- "type": "default",
- "script": "ct/comfyui.sh",
- "resources": {
- "cpu": 4,
- "ram": 8192,
- "hdd": 25,
- "os": "debian",
- "version": "13"
- }
- }
- ],
- "default_credentials": {
- "username": null,
- "password": null
- },
- "notes": [
- {
- "text": "Application takes long time to install. Please be patient!",
- "type": "warning"
- },
- {
- "text": "Please check that you have installed the drivers for your GPU.",
- "type": "info"
- }
- ]
-}
diff --git a/frontend/public/json/dispatcharr.json b/frontend/public/json/dispatcharr.json
deleted file mode 100644
index 46abd5550..000000000
--- a/frontend/public/json/dispatcharr.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "name": "Dispatcharr",
- "slug": "dispatcharr",
- "categories": [
- 14
- ],
- "date_created": "2025-07-01",
- "type": "ct",
- "updateable": true,
- "privileged": false,
- "interface_port": 9191,
- "documentation": "https://dispatcharr.github.io/Dispatcharr-Docs/",
- "website": "https://dispatcharr.github.io/Dispatcharr-Docs/",
- "logo": "https://raw.githubusercontent.com/Dispatcharr/Dispatcharr/refs/heads/main/frontend/src/images/logo.png",
- "config_path": "",
- "description": "Dispatcharr is an open-source powerhouse for managing IPTV streams and EPG data with elegance and control. Born from necessity and built with passion, it started as a personal project by OkinawaBoss and evolved with contributions from legends like dekzter, SergeantPanda and Bucatini.",
- "install_methods": [
- {
- "type": "default",
- "script": "ct/dispatcharr.sh",
- "resources": {
- "cpu": 1,
- "ram": 1024,
- "hdd": 8,
- "os": "debian",
- "version": "12"
- }
- }
- ],
- "default_credentials": {
- "username": null,
- "password": null
- },
- "notes": []
-}
diff --git a/frontend/public/json/hanko.json b/frontend/public/json/hanko.json
deleted file mode 100644
index d8628ad87..000000000
--- a/frontend/public/json/hanko.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "name": "Hanko",
- "slug": "hanko",
- "categories": [
- 21
- ],
- "date_created": "2025-07-02",
- "type": "ct",
- "updateable": true,
- "privileged": false,
- "config_path": "/opt/hanko/.env",
- "interface_port": 3000,
- "documentation": "https://docs.hanko.io/",
- "website": "https://hanko.io/",
- "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/hanko.svg",
- "description": "Hanko is an open-source authentication solution providing passkey-first login with support for WebAuthn/FIDO2, biometrics and modern identity flows. Easy to self-host and integrate via API or widget.",
- "install_methods": [
- {
- "type": "default",
- "script": "ct/hanko.sh",
- "resources": {
- "cpu": 1,
- "ram": 1024,
- "hdd": 2,
- "os": "Debian",
- "version": "12"
- }
- }
- ],
- "default_credentials": {
- "username": null,
- "password": null
- },
- "notes": []
-}
diff --git a/frontend/public/json/librenms.json b/frontend/public/json/librenms.json
deleted file mode 100644
index 39f738c12..000000000
--- a/frontend/public/json/librenms.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "name": "LibreNMS",
- "slug": "librenms",
- "categories": [
- 9
- ],
- "date_created": "2025-03-24",
- "type": "ct",
- "updateable": false,
- "privileged": false,
- "interface_port": 80,
- "documentation": "https://docs.librenms.org/",
- "website": "https://librenms.org/",
- "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/librenms.webp",
- "config_path": "/opt/librenms/config.php and /opt/librenms/.env",
- "description": "LibreNMS is an open-source, community-driven network monitoring system that provides automatic discovery, alerting, and performance tracking for network devices. It supports a wide range of hardware and integrates with various notification and logging platforms.",
- "install_methods": [
- {
- "type": "default",
- "script": "ct/librenms.sh",
- "resources": {
- "cpu": 2,
- "ram": 2048,
- "hdd": 4,
- "os": "Debian",
- "version": "12"
- }
- }
- ],
- "default_credentials": {
- "username": "admin",
- "password": "admin"
- },
- "notes": []
-}
diff --git a/frontend/public/json/livebook.json b/frontend/public/json/livebook.json
deleted file mode 100644
index 8b0a3589d..000000000
--- a/frontend/public/json/livebook.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "name": "Livebook",
- "slug": "livebook",
- "categories": [
- 20
- ],
- "date_created": "2025-08-12",
- "type": "ct",
- "updateable": true,
- "privileged": false,
- "interface_port": 8080,
- "documentation": null,
- "config_path": "/opt/.env",
- "website": "https://livebook.dev",
- "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/livebook.svg",
- "description": "Elixir Livebook is an interactive, web-based notebook platform for Elixir that combines code, documentation, and visualizations in a single document. Similar to Jupyter notebooks, it allows developers to write and execute Elixir code in real-time, making it ideal for data exploration, prototyping, learning, and collaborative development. Livebook features rich markdown support, built-in charting capabilities, and seamless integration with the Elixir ecosystem.",
- "install_methods": [
- {
- "type": "default",
- "script": "ct/livebook.sh",
- "resources": {
- "cpu": 1,
- "ram": 1024,
- "hdd": 4,
- "os": "Ubuntu",
- "version": "24.04"
- }
- }
- ],
- "default_credentials": {
- "username": null,
- "password": null
- },
- "notes": [
- {
- "text": "Show Livebook password: `cat /opt/livebook.creds`",
- "type": "info"
- }
- ]
-}
diff --git a/frontend/public/json/metabase.json b/frontend/public/json/metabase.json
new file mode 100644
index 000000000..1cb744078
--- /dev/null
+++ b/frontend/public/json/metabase.json
@@ -0,0 +1,35 @@
+{
+ "name": "Metabase",
+ "slug": "metabase",
+ "categories": [
+ 9
+ ],
+ "date_created": "2025-09-04",
+ "type": "ct",
+ "updateable": true,
+ "privileged": false,
+ "interface_port": 3000,
+ "documentation": "https://www.metabase.com/docs/latest/",
+ "config_path": "/opt/metabase/.env",
+ "website": "https://www.metabase.com/",
+ "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/metabase.webp",
+ "description": "Metabase is an open-source business intelligence platform. You can use Metabase to ask questions about your data, or embed Metabase in your app to let your customers explore their data on their own.",
+ "install_methods": [
+ {
+ "type": "default",
+ "script": "ct/metabase.sh",
+ "resources": {
+ "cpu": 2,
+ "ram": 2048,
+ "hdd": 6,
+ "os": "Debian",
+ "version": "13"
+ }
+ }
+ ],
+ "default_credentials": {
+ "username": null,
+ "password": null
+ },
+ "notes": []
+}
diff --git a/frontend/public/json/notesnook.json b/frontend/public/json/notesnook.json
deleted file mode 100644
index 335520b05..000000000
--- a/frontend/public/json/notesnook.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "name": "Notesnook",
- "slug": "notesnook",
- "categories": [
- 12
- ],
- "date_created": "2025-05-27",
- "type": "ct",
- "updateable": true,
- "privileged": false,
- "interface_port": 443,
- "documentation": null,
- "config_path": "/",
- "website": "https://notesnook.com/",
- "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/notesnook.webp",
- "description": "Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using XChaCha20-Poly1305 & Argon2.",
- "install_methods": [
- {
- "type": "default",
- "script": "ct/notesnook.sh",
- "resources": {
- "cpu": 2,
- "ram": 3072,
- "hdd": 10,
- "os": "Debian",
- "version": "12"
- }
- }
- ],
- "default_credentials": {
- "username": null,
- "password": null
- },
- "notes": [
- {
- "text": "Before doing update of the app, please make a backup in the application Web UI. You will need to restore this backup after update finishes!",
- "type": "warning"
- }
- ]
-}
diff --git a/frontend/public/json/patchmon.json b/frontend/public/json/patchmon.json
deleted file mode 100644
index 9b78f66f6..000000000
--- a/frontend/public/json/patchmon.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "name": "PatchMon",
- "slug": "patchmon",
- "categories": [
- 9
- ],
- "date_created": "2025-10-23",
- "type": "ct",
- "updateable": true,
- "privileged": false,
- "interface_port": 3399,
- "documentation": "https://docs.patchmon.net",
- "website": "https://patchmon.net",
- "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/patchmon.webp",
- "config_path": "/opt/patchmon/backend/.env, /opt/patchmon/frontend/.env",
- "description": "Monitor Linux patches across all your hosts with real-time visibility, security update tracking, and comprehensive package management.",
- "install_methods": [
- {
- "type": "default",
- "script": "ct/patchmon.sh",
- "resources": {
- "cpu": 2,
- "ram": 2048,
- "hdd": 4,
- "os": "debian",
- "version": "13"
- }
- }
- ],
- "default_credentials": {
- "username": null,
- "password": null
- },
- "notes": []
-}
diff --git a/frontend/public/json/postiz.json b/frontend/public/json/postiz.json
deleted file mode 100644
index dfcb0f045..000000000
--- a/frontend/public/json/postiz.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "name": "Postiz",
- "slug": "postiz",
- "categories": [
- 20
- ],
- "date_created": "2025-07-02",
- "type": "ct",
- "updateable": true,
- "privileged": false,
- "config_path": "/opt/postiz/.env",
- "interface_port": 3000,
- "documentation": "https://postiz.io/",
- "website": "https://postiz.io/",
- "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/postiz.svg",
- "description": "Postiz is an open-source self-hosted application.",
- "install_methods": [
- {
- "type": "default",
- "script": "ct/postiz.sh",
- "resources": {
- "cpu": 1,
- "ram": 1024,
- "hdd": 2,
- "os": "Debian",
- "version": "12"
- }
- }
- ],
- "default_credentials": {
- "username": null,
- "password": null
- },
- "notes": []
-}
\ No newline at end of file
diff --git a/frontend/public/json/qbittorrent-exporter.json b/frontend/public/json/qbittorrent-exporter.json
new file mode 100644
index 000000000..0c4ba9c38
--- /dev/null
+++ b/frontend/public/json/qbittorrent-exporter.json
@@ -0,0 +1,46 @@
+{
+ "name": "qbittorrent Exporter",
+ "slug": "qbittorrent-exporter",
+ "categories": [
+ 9
+ ],
+ "date_created": "2025-11-21",
+ "type": "addon",
+ "updateable": true,
+ "privileged": false,
+ "interface_port": 8090,
+ "documentation": "https://github.com/martabal/qbittorrent-exporter",
+ "website": "https://github.com/martabal/qbittorrent-exporter",
+ "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/qbittorrent.webp",
+ "config_path": "/opt/qbittorrent-exporter.env",
+ "description": "A fast and lightweight prometheus exporter for qBittorrent ",
+ "install_methods": [
+ {
+ "type": "default",
+ "script": "tools/addon/qbittorrent-exporter.sh",
+ "resources": {
+ "cpu": null,
+ "ram": null,
+ "hdd": null,
+ "os": null,
+ "version": null
+ }
+ },
+ {
+ "type": "alpine",
+ "script": "tools/addon/qbittorrent-exporter.sh",
+ "resources": {
+ "cpu": null,
+ "ram": null,
+ "hdd": null,
+ "os": null,
+ "version": null
+ }
+ }
+ ],
+ "default_credentials": {
+ "username": null,
+ "password": null
+ },
+ "notes": []
+}
diff --git a/frontend/public/json/snowshare.json b/frontend/public/json/snowshare.json
new file mode 100644
index 000000000..40433298c
--- /dev/null
+++ b/frontend/public/json/snowshare.json
@@ -0,0 +1,35 @@
+{
+ "name": "SnowShare",
+ "slug": "snowshare",
+ "categories": [
+ 11
+ ],
+ "date_created": "2025-09-24",
+ "type": "ct",
+ "updateable": true,
+ "privileged": false,
+ "interface_port": 3000,
+ "documentation": "https://github.com/TuroYT/snowshare",
+ "config_path": "/opt/snowshare/.env",
+ "website": "https://github.com/TuroYT/snowshare",
+ "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/png/snowshare.png",
+ "description": "A modern, secure file and link sharing platform built with Next.js, Prisma, and NextAuth. Share URLs, code snippets, and files with customizable expiration, privacy, and QR codes.",
+ "install_methods": [
+ {
+ "type": "default",
+ "script": "ct/snowshare.sh",
+ "resources": {
+ "cpu": 1,
+ "ram": 1024,
+ "hdd": 5,
+ "os": "Debian",
+ "version": "13"
+ }
+ }
+ ],
+ "default_credentials": {
+ "username": null,
+ "password": null
+ },
+ "notes": []
+}
diff --git a/frontend/src/config/siteConfig.tsx b/frontend/src/config/siteConfig.tsx
index fabc0ed20..6ee43d1b0 100644
--- a/frontend/src/config/siteConfig.tsx
+++ b/frontend/src/config/siteConfig.tsx
@@ -58,8 +58,8 @@ export const OperatingSystems: OperatingSystem[] = [
{
name: "Debian",
versions: [
- { name: "11", slug: "bullseye" },
{ name: "12", slug: "bookworm" },
+ { name: "13", slug: "trixie" },
],
},
{
diff --git a/install/alpine-garage-install.sh b/install/alpine-garage-install.sh
deleted file mode 100644
index 5138656a8..000000000
--- a/install/alpine-garage-install.sh
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: MickLesk (CanbiZ)
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://garagehq.deuxfleurs.fr/
-
-source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
-color
-verb_ip6
-catch_errors
-setting_up_container
-network_check
-update_os
-
-msg_info "Preparing directories"
-mkdir -p /var/lib/garage/meta /var/lib/garage/data /var/lib/garage/snapshots
-msg_ok "Prepared directories"
-
-msg_info "Setup Garage packages"
-$STD apk add --no-cache garage garage-openrc openssl
-msg_ok "Setup Garage packages"
-
-# msg_info "Generating RPC secret"
-# if [[ ! -s /etc/garage.rpc_secret ]]; then
-# openssl rand -hex 32 | tr -d '\n' >/etc/garage.rpc_secret
-# chmod 600 /etc/garage.rpc_secret
-# fi
-# msg_ok "Generated RPC secret"
-
-# msg_info "Generating tokens"
-# if [[ ! -s /etc/garage.tokens.env ]]; then
-# ADMIN_TOKEN="$(openssl rand -base64 32)"
-# METRICS_TOKEN="$(openssl rand -base64 32)"
-# cat >/etc/garage.tokens.env </etc/garage.toml </etc/systemd/system/comfyui.service
-[Unit]
-Description=ComfyUI Service
-After=network.target
-
-[Service]
-Type=simple
-User=root
-WorkingDirectory=/opt/ComfyUI
-ExecStart=/opt/ComfyUI/venv/bin/python /opt/ComfyUI/main.py --listen --port 8188 --cpu
-Restart=on-failure
-
-[Install]
-WantedBy=multi-user.target
-EOF
-systemctl enable -q --now comfyui
-msg_ok "Created Service"
-
-motd_ssh
-customize
-
-msg_info "Cleaning up"
-$STD apt -y autoremove
-$STD apt -y autoclean
-$STD apt -y clean
-msg_ok "Cleaned"
diff --git a/install/cronmaster-install.sh b/install/cronmaster-install.sh
new file mode 100644
index 000000000..738e05199
--- /dev/null
+++ b/install/cronmaster-install.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: Slaviša Arežina (tremor021)
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# Source: https://github.com/fccview/cronmaster
+
+source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
+color
+verb_ip6
+catch_errors
+setting_up_container
+network_check
+update_os
+
+msg_info "Installing dependencies"
+$STD apt install -y pciutils
+msg_ok "Installed dependencies"
+
+NODE_VERSION="24" NODE_MODULE="yarn" setup_nodejs
+
+setup_deb822_repo \
+ "docker" \
+ "https://download.docker.com/linux/debian/gpg" \
+ "https://download.docker.com/linux/debian" \
+ "trixie" \
+ "stable"
+$STD apt install -y docker-ce-cli
+fetch_and_deploy_gh_release "cronmaster" "fccview/cronmaster" "tarball"
+
+msg_info "Setting up CronMaster"
+AUTH_PASS="$(openssl rand -base64 18 | cut -c1-13)"
+cd /opt/cronmaster
+$STD yarn --frozen-lockfile
+export NEXT_TELEMETRY_DISABLED=1
+$STD yarn build
+cat </opt/cronmaster/.env
+NODE_ENV=production
+APP_URL=
+LOCALE=
+HOME=
+AUTH_PASSWORD=${AUTH_PASS}
+PORT=3000
+HOSTNAME="0.0.0.0"
+NEXT_TELEMETRY_DISABLED=1
+EOF
+{
+ echo "CronMaster Credentials:"
+ echo ""
+ echo "Password: $AUTH_PASS"
+}>>~/cronmaster.creds
+msg_ok "Setup CronMaster"
+
+msg_info "Creating Service"
+cat </etc/systemd/system/cronmaster.service
+[Unit]
+Description=CronMaster Service
+After=network.target
+
+[Service]
+EnvironmentFile=/opt/cronmaster/.env
+WorkingDirectory=/opt/cronmaster
+ExecStart=/usr/bin/yarn start
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+EOF
+systemctl start --now -q cronmaster
+msg_info "Created Service"
+
+motd_ssh
+customize
+cleanup_lxc
diff --git a/install/debian-install.sh b/install/debian-install.sh
index b5864b09c..aedf72fc0 100644
--- a/install/debian-install.sh
+++ b/install/debian-install.sh
@@ -1,10 +1,9 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2025 community-scripts ORG
-# Author: Test Suite for tools.func
-# License: MIT
-# https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
-# Purpose: Run comprehensive test suite for all setup_* functions from tools.func
+# Author: MickLesk (CanbiZ)
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# Source:
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
@@ -18,14 +17,10 @@ msg_info "Installing Base Dependencies"
$STD apt-get install -y curl wget ca-certificates
msg_ok "Installed Base Dependencies"
-msg_info "Downloading and executing tools.func test suite"
-bash <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/test-tools-func.sh)
-msg_ok "Test suite completed"
+# msg_info "Downloading and executing tools.func test suite"
+# bash <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/test-tools-func.sh)
+# msg_ok "Test suite completed"
motd_ssh
customize
-
-msg_info "Cleaning up"
-$STD apt-get -y autoremove
-$STD apt-get -y autoclean
-msg_ok "Cleaned"
+cleanup_lxc
diff --git a/install/deferred/hanko-install.sh b/install/deferred/hanko-install.sh
deleted file mode 100644
index e1ec43c9d..000000000
--- a/install/deferred/hanko-install.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: MickLesk (CanbiZ)
-# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
-# Source: https://hanko.io/
-
-source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
-color
-verb_ip6
-catch_errors
-setting_up_container
-network_check
-update_os
-
-setup_yq
-PG_VERSION="16" setup_postgresql
-NODE_VERSION=22 NODE_MODULE="yarn@latest,npm@latest" setup_nodejs
-
-msg_info "Setting up PostgreSQL Database"
-DB_NAME=hanko
-DB_USER=hanko
-DB_PASS="$(openssl rand -base64 18 | cut -c1-13)"
-APP_SECRET=$(openssl rand -base64 32)
-$STD sudo -u postgres psql -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PASS';"
-$STD sudo -u postgres psql -c "CREATE DATABASE $DB_NAME WITH OWNER $DB_USER ENCODING 'UTF8' TEMPLATE template0;"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET client_encoding TO 'utf8';"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET default_transaction_isolation TO 'read committed';"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET timezone TO 'UTC'"
-{
- echo "Hanko-Credentials"
- echo "Hanko Database User: $DB_USER"
- echo "Hanko Database Password: $DB_PASS"
- echo "Hanko Database Name: $DB_NAME"
-} >>~/hanko.creds
-msg_ok "Set up PostgreSQL Database"
-
-msg_info "Setup Hanko"
-fetch_and_deploy_gh_release "hanko" "teamhanko/hanko" "prebuild" "latest" "/opt/hanko" "hanko_Linux_x86_64.tar.gz"
-curl -fsSL https://raw.githubusercontent.com/teamhanko/hanko/refs/heads/main/backend/config/config.yaml -o /opt/hanko/config.yaml
-env DB_USER="$DB_USER" DB_PASS="$DB_PASS" APP_SECRET="$APP_SECRET" \
- yq eval '
- .database.user = strenv(DB_USER) |
- .database.password = strenv(DB_PASS) |
- .database.host = "localhost" |
- .database.port = "5432" |
- .database.dialect = "postgres" |
- .app.secret = strenv(APP_SECRET)
-' -i /opt/hanko/config.yaml
-$STD /opt/hanko/hanko --config /opt/hanko/config.yaml migrate up
-yarn add @teamhanko/hanko-elements
-msg_ok "Setup Hanko"
-
-msg_info "Setup Service"
-cat </etc/systemd/system/hanko.service
-[Unit]
-Description=Hanko Service
-After=network.target
-
-[Service]
-Type=simple
-ExecStart=/opt/hanko/hanko serve all --config /opt/hanko/config.yaml
-Restart=on-failure
-RestartSec=5
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-systemctl enable -q --now hanko
-msg_ok "Service Setup"
-
-motd_ssh
-customize
-
-msg_info "Cleaning up"
-$STD apt-get -y autoremove
-$STD apt-get -y autoclean
-msg_ok "Cleaned"
diff --git a/install/dispatcharr-install.sh b/install/dispatcharr-install.sh
deleted file mode 100644
index 09529d68d..000000000
--- a/install/dispatcharr-install.sh
+++ /dev/null
@@ -1,266 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: ekke85
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/Dispatcharr/Dispatcharr
-
-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 \
- build-essential \
- gcc \
- python3-dev \
- libpq-dev \
- nginx \
- redis-server \
- ffmpeg \
- procps \
- streamlink
-msg_ok "Installed Dependencies"
-
-setup_uv
-NODE_VERSION="24" setup_nodejs
-PG_VERSION="16" setup_postgresql
-
-msg_info "Creating PostgreSQL Database"
-DB_NAME=dispatcharr_db
-DB_USER=dispatcharr_usr
-DB_PASS="$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | cut -c1-13)"
-$STD sudo -u postgres psql -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PASS';"
-$STD sudo -u postgres psql -c "CREATE DATABASE $DB_NAME WITH OWNER $DB_USER ENCODING 'UTF8' TEMPLATE template0;"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET client_encoding TO 'utf8';"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET default_transaction_isolation TO 'read committed';"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET timezone TO 'UTC';"
-
-cat <~/dispatcharr.creds
-Dispatcharr-Credentials
-Dispatcharr Database Name: $DB_NAME
-Dispatcharr Database User: $DB_USER
-Dispatcharr Database Password: $DB_PASS
-EOF
-msg_ok "Created PostgreSQL Database"
-
-fetch_and_deploy_gh_release "dispatcharr" "Dispatcharr/Dispatcharr"
-
-msg_info "Installing Python Dependencies with uv"
-cd /opt/dispatcharr || exit
-
-$STD uv venv
-$STD uv pip install -r requirements.txt --index-strategy unsafe-best-match
-$STD uv pip install gunicorn gevent celery redis daphne
-msg_ok "Installed Python Dependencies"
-
-msg_info "Configuring Dispatcharr"
-export DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:5432/${DB_NAME}"
-export POSTGRES_DB=$DB_NAME
-export POSTGRES_USER=$DB_USER
-export POSTGRES_PASSWORD=$DB_PASS
-export POSTGRES_HOST=localhost
-$STD uv run python manage.py migrate --noinput
-$STD uv run python manage.py collectstatic --noinput
-cat </opt/dispatcharr/.env
-DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@localhost:5432/${DB_NAME}
-POSTGRES_DB=$DB_NAME
-POSTGRES_USER=$DB_USER
-POSTGRES_PASSWORD=$DB_PASS
-POSTGRES_HOST=localhost
-CELERY_BROKER_URL=redis://localhost:6379/0
-EOF
-cd /opt/dispatcharr/frontend || exit
-$STD npm install --legacy-peer-deps
-$STD npm run build
-msg_ok "Configured Dispatcharr"
-
-msg_info "Configuring Nginx"
-cat </etc/nginx/sites-available/dispatcharr.conf
-server {
- listen 80;
- server_name _;
-
- # Serve static assets with correct MIME types
- location /assets/ {
- alias /opt/dispatcharr/frontend/dist/assets/;
- expires 30d;
- add_header Cache-Control "public, immutable";
-
- # Explicitly set MIME types for webpack-built assets
- types {
- text/javascript js;
- text/css css;
- image/png png;
- image/svg+xml svg svgz;
- font/woff2 woff2;
- font/woff woff;
- font/ttf ttf;
- }
- }
-
- location /static/ {
- alias /opt/dispatcharr/static/;
- expires 30d;
- add_header Cache-Control "public, immutable";
- }
-
- location /media/ {
- alias /opt/dispatcharr/media/;
- }
-
- location /ws/ {
- proxy_pass http://127.0.0.1:8001;
- proxy_http_version 1.1;
- proxy_set_header Upgrade \$http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host \$host;
- proxy_set_header X-Real-IP \$remote_addr;
- proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto \$scheme;
- }
-
- # All other requests proxy to Gunicorn
- location / {
- include proxy_params;
- proxy_pass http://127.0.0.1:5656;
- }
-}
-EOF
-
-ln -sf /etc/nginx/sites-available/dispatcharr.conf /etc/nginx/sites-enabled/dispatcharr.conf
-rm -f /etc/nginx/sites-enabled/default
-systemctl restart nginx
-msg_ok "Configured Nginx"
-
-msg_info "Creating Services"
-cat </opt/dispatcharr/start-gunicorn.sh
-#!/usr/bin/env bash
-cd /opt/dispatcharr
-set -a
-source .env
-set +a
-exec uv run gunicorn \\
- --workers=4 \\
- --worker-class=gevent \\
- --timeout=300 \\
- --bind 0.0.0.0:5656 \\
- dispatcharr.wsgi:application
-EOF
-chmod +x /opt/dispatcharr/start-gunicorn.sh
-
-cat </opt/dispatcharr/start-celery.sh
-#!/usr/bin/env bash
-cd /opt/dispatcharr
-set -a
-source .env
-set +a
-exec uv run celery -A dispatcharr worker -l info -c 4
-EOF
-chmod +x /opt/dispatcharr/start-celery.sh
-
-cat </opt/dispatcharr/start-celerybeat.sh
-#!/usr/bin/env bash
-cd /opt/dispatcharr
-set -a
-source .env
-set +a
-exec uv run celery -A dispatcharr beat -l info
-EOF
-chmod +x /opt/dispatcharr/start-celerybeat.sh
-
-cat </opt/dispatcharr/start-daphne.sh
-#!/usr/bin/env bash
-cd /opt/dispatcharr
-set -a
-source .env
-set +a
-exec uv run daphne -b 0.0.0.0 -p 8001 dispatcharr.asgi:application
-EOF
-chmod +x /opt/dispatcharr/start-daphne.sh
-
-cat </etc/systemd/system/dispatcharr.service
-[Unit]
-Description=Dispatcharr Web Server
-After=network.target postgresql.service redis-server.service
-
-[Service]
-Type=simple
-WorkingDirectory=/opt/dispatcharr
-ExecStart=/opt/dispatcharr/start-gunicorn.sh
-Restart=on-failure
-RestartSec=10
-User=root
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-cat </etc/systemd/system/dispatcharr-celery.service
-[Unit]
-Description=Dispatcharr Celery Worker
-After=network.target redis-server.service
-Requires=dispatcharr.service
-
-[Service]
-Type=simple
-WorkingDirectory=/opt/dispatcharr
-ExecStart=/opt/dispatcharr/start-celery.sh
-Restart=on-failure
-RestartSec=10
-User=root
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-cat </etc/systemd/system/dispatcharr-celerybeat.service
-[Unit]
-Description=Dispatcharr Celery Beat Scheduler
-After=network.target redis-server.service
-Requires=dispatcharr.service
-
-[Service]
-Type=simple
-WorkingDirectory=/opt/dispatcharr
-ExecStart=/opt/dispatcharr/start-celerybeat.sh
-Restart=on-failure
-RestartSec=10
-User=root
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-cat </etc/systemd/system/dispatcharr-daphne.service
-[Unit]
-Description=Dispatcharr WebSocket Server (Daphne)
-After=network.target
-Requires=dispatcharr.service
-
-[Service]
-Type=simple
-WorkingDirectory=/opt/dispatcharr
-ExecStart=/opt/dispatcharr/start-daphne.sh
-Restart=on-failure
-RestartSec=10
-User=root
-
-[Install]
-WantedBy=multi-user.target
-EOF
-systemctl enable -q --now dispatcharr dispatcharr-celery dispatcharr-celerybeat dispatcharr-daphne
-msg_ok "Created Services"
-
-motd_ssh
-customize
-
-msg_info "Cleaning up"
-$STD apt -y autoremove
-$STD apt -y autoclean
-$STD apt -y clean
-msg_ok "Cleaned"
diff --git a/install/docker-install.sh b/install/docker-install.sh
new file mode 100644
index 000000000..8edde48bc
--- /dev/null
+++ b/install/docker-install.sh
@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2021-2025 tteck
+# Author: tteck (tteckster)
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://www.docker.com/
+
+source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
+color
+verb_ip6
+catch_errors
+setting_up_container
+network_check
+update_os
+
+# Apply AppArmor workaround BEFORE installing Docker
+# See: https://github.com/opencontainers/runc/issues/4968
+apply_docker_apparmor_workaround
+
+get_latest_release() {
+ curl -fsSL https://api.github.com/repos/"$1"/releases/latest | grep '"tag_name":' | cut -d'"' -f4
+}
+
+DOCKER_LATEST_VERSION=$(get_latest_release "moby/moby")
+PORTAINER_LATEST_VERSION=$(get_latest_release "portainer/portainer")
+PORTAINER_AGENT_LATEST_VERSION=$(get_latest_release "portainer/agent")
+DOCKER_COMPOSE_LATEST_VERSION=$(get_latest_release "docker/compose")
+
+msg_info "Installing Docker $DOCKER_LATEST_VERSION"
+DOCKER_CONFIG_PATH='/etc/docker/daemon.json'
+mkdir -p $(dirname $DOCKER_CONFIG_PATH)
+echo -e '{\n "log-driver": "journald"\n}' >/etc/docker/daemon.json
+$STD sh <(curl -fsSL https://get.docker.com)
+msg_ok "Installed Docker $DOCKER_LATEST_VERSION"
+
+# Restart Docker to apply AppArmor workaround (if running in LXC)
+$STD systemctl restart docker
+
+read -r -p "${TAB3}Install Docker Compose v2 plugin? " prompt_compose
+if [[ ${prompt_compose,,} =~ ^(y|yes)$ ]]; then
+ msg_info "Installing Docker Compose $DOCKER_COMPOSE_LATEST_VERSION"
+ mkdir -p /usr/local/lib/docker/cli-plugins
+ curl -fsSL "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)" \
+ -o /usr/local/lib/docker/cli-plugins/docker-compose
+ chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
+ msg_ok "Installed Docker Compose $DOCKER_COMPOSE_LATEST_VERSION"
+fi
+
+read -r -p "${TAB3}Would you like to add Portainer (UI)? " prompt
+if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
+ msg_info "Installing Portainer $PORTAINER_LATEST_VERSION"
+ docker volume create portainer_data >/dev/null
+ $STD docker run -d \
+ -p 8000:8000 \
+ -p 9443:9443 \
+ --name=portainer \
+ --restart=always \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v portainer_data:/data \
+ portainer/portainer-ce:latest
+ msg_ok "Installed Portainer $PORTAINER_LATEST_VERSION"
+else
+ read -r -p "${TAB3}Would you like to install the Portainer Agent (for remote management)? " prompt_agent
+ if [[ ${prompt_agent,,} =~ ^(y|yes)$ ]]; then
+ msg_info "Installing Portainer Agent $PORTAINER_AGENT_LATEST_VERSION"
+ $STD docker run -d \
+ -p 9001:9001 \
+ --name portainer_agent \
+ --restart=always \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v /var/lib/docker/volumes:/var/lib/docker/volumes \
+ portainer/agent
+ msg_ok "Installed Portainer Agent $PORTAINER_AGENT_LATEST_VERSION"
+ fi
+fi
+
+read -r -p "${TAB3}Expose Docker TCP socket (insecure) ? [n = No, l = Local only (127.0.0.1), a = All interfaces (0.0.0.0)] : " socket_choice
+case "${socket_choice,,}" in
+l)
+ socket="tcp://127.0.0.1:2375"
+ ;;
+a)
+ socket="tcp://0.0.0.0:2375"
+ ;;
+*)
+ socket=""
+ ;;
+esac
+
+if [[ -n "$socket" ]]; then
+ msg_info "Enabling Docker TCP socket on $socket"
+ $STD apt-get install -y jq
+
+ tmpfile=$(mktemp)
+ jq --arg sock "$socket" '. + { "hosts": ["unix:///var/run/docker.sock", $sock] }' /etc/docker/daemon.json >"$tmpfile" && mv "$tmpfile" /etc/docker/daemon.json
+
+ mkdir -p /etc/systemd/system/docker.service.d
+ cat </etc/systemd/system/docker.service.d/override.conf
+[Service]
+ExecStart=
+ExecStart=/usr/bin/dockerd
+EOF
+
+ $STD systemctl daemon-reexec
+ $STD systemctl daemon-reload
+
+ if systemctl restart docker; then
+ msg_ok "Docker TCP socket available on $socket"
+ else
+ msg_error "Docker failed to restart. Check journalctl -xeu docker.service"
+ exit 1
+ fi
+fi
+
+motd_ssh
+customize
+
+msg_info "Cleaning up"
+$STD apt-get -y autoremove
+$STD apt-get -y autoclean
+msg_ok "Cleaned"
diff --git a/install/ente-install.sh b/install/ente-install.sh
index 7af4f173c..bddfc1b31 100644
--- a/install/ente-install.sh
+++ b/install/ente-install.sh
@@ -15,17 +15,21 @@ update_os
msg_info "Installing Dependencies"
$STD apt-get install -y \
- libsodium23 \
- libsodium-dev \
- pkg-config \
- caddy \
- gcc
+ libsodium23 \
+ libsodium-dev \
+ pkg-config \
+ caddy \
+ gcc \
+ curl \
+ jq
msg_ok "Installed Dependencies"
PG_VERSION="17" setup_postgresql
setup_go
NODE_VERSION="24" NODE_MODULE="yarn" setup_nodejs
+ENTE_CLI_VERSION=$(curl -s https://api.github.com/repos/ente-io/ente/releases | jq -r '[.[] | select(.tag_name | startswith("cli-v"))][0].tag_name')
fetch_and_deploy_gh_release "ente" "ente-io/ente" "tarball" "latest" "/opt/ente"
+fetch_and_deploy_gh_release "ente" "ente-io/ente" "tarball" "$ENTE_CLI_VERSION" "/usr/local/bin/ente" "ente-cli-$ENTE_CLI_VERSION-linux-amd64.tar.gz"
msg_info "Setting up PostgreSQL"
DB_NAME="ente_db"
@@ -37,10 +41,28 @@ $STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET client_encoding TO 'utf8'
$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET default_transaction_isolation TO 'read committed';"
$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET timezone TO 'UTC';"
{
- echo "Ente Credentials"
- echo "Database Name: $DB_NAME"
- echo "Database User: $DB_USER"
- echo "Database Password: $DB_PASS"
+ echo "Ente Credentials"
+ echo "Database Name: $DB_NAME"
+ echo "Database User: $DB_USER"
+ echo "Database Password: $DB_PASS"
+ echo ""
+ echo "Important Configuration Notes:"
+ echo "- Frontend is built with IP: $(hostname -I | awk '{print $1}')"
+ echo "- If IP changes, run: /opt/ente/rebuild-frontend.sh"
+ echo "- Museum API: http://$(hostname -I | awk '{print $1}'):8080"
+ echo "- Photos UI: http://$(hostname -I | awk '{print $1}'):3000"
+ echo "- Accounts UI: http://$(hostname -I | awk '{print $1}'):3001"
+ echo "- Auth UI: http://$(hostname -I | awk '{print $1}'):3003"
+ echo ""
+ echo "Post-Installation Steps Required:"
+ echo "1. Create your first user account via the web UI"
+ echo "2. Check museum logs for email verification code:"
+ echo " journalctl -u ente-museum -n 100 | grep -i 'verification'"
+ echo "3. Use verification code to complete account setup"
+ echo "4. Remove subscription limit (replace with your account):"
+ echo " ente admin update-subscription -a -u --no-limit"
+ echo ""
+ echo "Note: Email verification requires manual intervention since SMTP is not configured"
} >>~/ente.creds
msg_ok "Set up PostgreSQL"
@@ -78,10 +100,10 @@ export CGO_ENABLED=1
CGO_CFLAGS="$(pkg-config --cflags libsodium || true)"
CGO_LDFLAGS="$(pkg-config --libs libsodium || true)"
if [ -z "$CGO_CFLAGS" ]; then
- CGO_CFLAGS="-I/usr/include"
+ CGO_CFLAGS="-I/usr/include"
fi
if [ -z "$CGO_LDFLAGS" ]; then
- CGO_LDFLAGS="-lsodium"
+ CGO_LDFLAGS="-lsodium"
fi
export CGO_CFLAGS
export CGO_LDFLAGS
@@ -89,12 +111,13 @@ $STD go build cmd/museum/main.go
msg_ok "Built Museum"
msg_info "Generating Secrets"
-SECRET_ENC=$($STD go run tools/gen-random-keys/main.go | grep "encryption" | awk '{print $2}')
-SECRET_HASH=$($STD go run tools/gen-random-keys/main.go | grep "hash" | awk '{print $2}')
-SECRET_JWT=$($STD go run tools/gen-random-keys/main.go | grep "jwt" | awk '{print $2}')
+SECRET_ENC=$(go run tools/gen-random-keys/main.go 2>/dev/null | grep "encryption" | awk '{print $2}')
+SECRET_HASH=$(go run tools/gen-random-keys/main.go 2>/dev/null | grep "hash" | awk '{print $2}')
+SECRET_JWT=$(go run tools/gen-random-keys/main.go 2>/dev/null | grep "jwt" | awk '{print $2}')
msg_ok "Generated Secrets"
msg_info "Creating museum.yaml"
+CONTAINER_IP=$(hostname -I | awk '{print $1}')
cat </opt/ente/server/museum.yaml
db:
host: 127.0.0.1
@@ -114,9 +137,9 @@ s3:
bucket: ente-dev
apps:
- public-albums: http://localhost:3002
- cast: http://localhost:3004
- accounts: http://localhost:3001
+ public-albums: http://${CONTAINER_IP}:3002
+ cast: http://${CONTAINER_IP}:3004
+ accounts: http://${CONTAINER_IP}:3001
key:
encryption: $SECRET_ENC
@@ -124,6 +147,15 @@ key:
jwt:
secret: $SECRET_JWT
+
+# SMTP not configured - verification codes will appear in logs
+# To configure SMTP, add:
+# smtp:
+# host: your-smtp-server
+# port: 587
+# username: your-username
+# password: your-password
+# email: noreply@yourdomain.com
EOF
msg_ok "Created museum.yaml"
@@ -172,6 +204,30 @@ cp -r apps/photos/out /var/www/ente/apps/photos
cp -r apps/accounts/out /var/www/ente/apps/accounts
cp -r apps/auth/out /var/www/ente/apps/auth
cp -r apps/cast/out /var/www/ente/apps/cast
+
+# Save build configuration for future rebuilds
+cat </opt/ente/rebuild-frontend.sh
+#!/usr/bin/env bash
+# Rebuild Ente frontend with current IP
+CONTAINER_IP=\$(hostname -I | awk '{print \$1}')
+echo "Building frontend with IP: \$CONTAINER_IP"
+cd /opt/ente/web
+export
+=http://\${CONTAINER_IP}:8080
+export NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://\${CONTAINER_IP}:3002
+yarn build
+yarn build:accounts
+yarn build:auth
+yarn build:cast
+rm -rf /var/www/ente/apps/*
+cp -r apps/photos/out /var/www/ente/apps/photos
+cp -r apps/accounts/out /var/www/ente/apps/accounts
+cp -r apps/auth/out /var/www/ente/apps/auth
+cp -r apps/cast/out /var/www/ente/apps/cast
+systemctl reload caddy
+echo "Frontend rebuilt successfully!"
+REBUILD_EOF
+chmod +x /opt/ente/rebuild-frontend.sh
msg_ok "Built Web Applications"
msg_info "Creating Museum Service"
@@ -192,31 +248,82 @@ systemctl enable -q --now ente-museum
msg_ok "Created Museum Service"
msg_info "Configuring Caddy"
+CONTAINER_IP=$(hostname -I | awk '{print $1}')
cat </etc/caddy/Caddyfile
+# Ente Photos - Main Application
:3000 {
root * /var/www/ente/apps/photos
file_server
try_files {path} {path}.html /index.html
+
+ header {
+ Access-Control-Allow-Origin *
+ Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+ Access-Control-Allow-Headers *
+ }
}
+
+# Ente Accounts
:3001 {
root * /var/www/ente/apps/accounts
file_server
try_files {path} {path}.html /index.html
+
+ header {
+ Access-Control-Allow-Origin *
+ Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+ Access-Control-Allow-Headers *
+ }
}
+
+# Public Albums
:3002 {
root * /var/www/ente/apps/photos
file_server
try_files {path} {path}.html /index.html
+
+ header {
+ Access-Control-Allow-Origin *
+ Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+ Access-Control-Allow-Headers *
+ }
}
+
+# Auth
:3003 {
root * /var/www/ente/apps/auth
file_server
try_files {path} {path}.html /index.html
+
+ header {
+ Access-Control-Allow-Origin *
+ Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+ Access-Control-Allow-Headers *
+ }
}
+
+# Cast
:3004 {
root * /var/www/ente/apps/cast
file_server
try_files {path} {path}.html /index.html
+
+ header {
+ Access-Control-Allow-Origin *
+ Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+ Access-Control-Allow-Headers *
+ }
+}
+
+# Museum API Proxy
+:8080 {
+ reverse_proxy localhost:8080
+
+ header {
+ Access-Control-Allow-Origin *
+ Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+ Access-Control-Allow-Headers *
+ }
}
EOF
systemctl reload caddy
@@ -225,7 +332,63 @@ msg_ok "Configured Caddy"
motd_ssh
customize
+msg_info "Creating helper scripts"
+cat <<'HELPER_EOF' >/usr/local/bin/ente-get-verification
+#!/usr/bin/env bash
+echo "Searching for verification codes in museum logs..."
+journalctl -u ente-museum --no-pager | grep -i "verification\|verify\|code" | tail -20
+HELPER_EOF
+chmod +x /usr/local/bin/ente-get-verification
+
+cat <<'HELPER_EOF' >/usr/local/bin/ente-upgrade-subscription
+#!/usr/bin/env bash
+if [ -z "$1" ]; then
+ echo "Usage: ente-upgrade-subscription "
+ echo "Example: ente-upgrade-subscription user@example.com"
+ exit 1
+fi
+EMAIL="$1"
+echo "Upgrading subscription for: $EMAIL"
+ente admin update-subscription -a "$EMAIL" -u "$EMAIL" --no-limit
+HELPER_EOF
+chmod +x /usr/local/bin/ente-upgrade-subscription
+
+msg_ok "Created helper scripts"
+
msg_info "Cleaning up"
-$STD apt-get -y autoremove
-$STD apt-get -y autoclean
+$STD apt -y autoremove
+$STD apt -y autoclean
msg_ok "Cleaned"
+
+# Final setup summary
+CONTAINER_IP=$(hostname -I | awk '{print $1}')
+echo -e "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo -e " ${GN}Ente Installation Complete!${CL}"
+echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo -e "\n${BL}Access URLs:${CL}"
+echo -e " Photos: http://${CONTAINER_IP}:3000"
+echo -e " Accounts: http://${CONTAINER_IP}:3001"
+echo -e " Auth: http://${CONTAINER_IP}:3003"
+echo -e " API: http://${CONTAINER_IP}:8080"
+echo -e "\n${YW}⚠️ Important Post-Installation Steps:${CL}"
+echo -e "\n${BL}1. Create your first account:${CL}"
+echo -e " • Open http://${CONTAINER_IP}:3000 in your browser"
+echo -e " • Click 'Sign Up' and create an account"
+echo -e "\n${BL}2. Verify your email (required):${CL}"
+echo -e " • Run: ${GN}ente-get-verification${CL}"
+echo -e " • Look for the verification code in the output"
+echo -e " • Enter the code in the web UI to complete registration"
+echo -e "\n${BL}3. Remove storage limit:${CL}"
+echo -e " • After email verification is complete"
+echo -e " • Run: ${GN}ente-upgrade-subscription your@email.com${CL}"
+echo -e " • This removes the 10GB limit"
+echo -e "\n${BL}4. If IP changes:${CL}"
+echo -e " • Run: ${GN}/opt/ente/rebuild-frontend.sh${CL}"
+echo -e " • This rebuilds the frontend with the new IP"
+echo -e "\n${YW}Known Limitations:${CL}"
+echo -e " • Email verification requires checking logs (no SMTP configured)"
+echo -e " • Account creation must be done manually via web UI"
+echo -e " • Subscription upgrade requires CLI after account creation"
+echo -e " • Frontend must be rebuilt if container IP changes"
+echo -e "\n${BL}Credentials saved to:${CL} ~/ente.creds"
+echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
diff --git a/install/gitea-mirror-install.sh b/install/gitea-mirror-install.sh
index 7f15bd7af..3ec7171bd 100644
--- a/install/gitea-mirror-install.sh
+++ b/install/gitea-mirror-install.sh
@@ -13,21 +13,7 @@ setting_up_container
network_check
update_os
-msg_info "Installing dependencies"
-$STD apt-get install -y \
- build-essential \
- openssl \
- sqlite3 \
- unzip
-msg_ok "Installed Dependencies"
-
-msg_info "Installing Bun"
-export BUN_INSTALL=/opt/bun
-curl -fsSL https://bun.sh/install | $STD bash
-ln -sf /opt/bun/bin/bun /usr/local/bin/bun
-ln -sf /opt/bun/bin/bun /usr/local/bin/bunx
-msg_ok "Installed Bun"
-
+NODE_VERSION="22" NODE_MODULES="bun" setup_nodejs
fetch_and_deploy_gh_release "gitea-mirror" "RayLabsHQ/gitea-mirror"
msg_info "Installing gitea-mirror"
@@ -70,8 +56,4 @@ msg_ok "Created Service"
motd_ssh
customize
-
-msg_info "Cleaning up"
-$STD apt-get -y autoremove
-$STD apt-get -y autoclean
-msg_ok "Cleaned"
+cleanup_lxc
diff --git a/install/librenms-install.sh b/install/librenms-install.sh
deleted file mode 100644
index ad1cbb6fe..000000000
--- a/install/librenms-install.sh
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: michelroegl-brunner
-# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
-# Source: https://github.com/opf/openproject
-
-source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
-color
-verb_ip6
-catch_errors
-setting_up_container
-network_check
-update_os
-
-msg_info "Installing Dependencies"
-$STD apt-get install -y \
- lsb-release \
- ca-certificates \
- acl \
- fping \
- graphviz \
- imagemagick \
- mtr-tiny \
- nginx \
- nmap \
- rrdtool \
- snmp \
- snmpd
-msg_ok "Installed Dependencies"
-
-PHP_VERSION="8.3" PHP_FPM="YES" PHP_MODULE="gmp,mysql,snmp" setup_php
-setup_mariadb
-setup_composer
-PYTHON_VERSION="3.13" setup_uv
-
-msg_info "Installing Python"
-$STD apt-get install -y \
- python3-{dotenv,pymysql,redis,setuptools,systemd,pip}
-msg_ok "Installed Python"
-
-msg_info "Configuring Database"
-DB_NAME=librenms
-DB_USER=librenms
-DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
-$STD mariadb -u root -e "CREATE DATABASE $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
-$STD mariadb -u root -e "CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';"
-$STD mariadb -u root -e "GRANT ALL ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;"
-{
- echo "LibreNMS-Credentials"
- echo "LibreNMS Database User: $DB_USER"
- echo "LibreNMS Database Password: $DB_PASS"
- echo "LibreNMS Database Name: $DB_NAME"
-} >>~/librenms.creds
-msg_ok "Configured Database"
-
-fetch_and_deploy_gh_release "LibreNMS" "librenms/librenms"
-
-msg_info "Configuring LibreNMS"
-$STD useradd librenms -d /opt/librenms -M -r -s "$(which bash)"
-setfacl -d -m g::rwx /opt/librenms/rrd /opt/librenms/logs /opt/librenms/bootstrap/cache/ /opt/librenms/storage/
-setfacl -R -m g::rwx /opt/librenms/rrd /opt/librenms/logs /opt/librenms/bootstrap/cache/ /opt/librenms/storage/
-cd /opt/librenms
-$STD uv venv .venv
-$STD source .venv/bin/activate
-$STD uv pip install -r requirements.txt
-cat </opt/librenms/.env
-DB_DATABASE=${DB_NAME}
-DB_USERNAME=${DB_USER}
-DB_PASSWORD=${DB_PASS}
-EOF
-chown -R librenms:librenms /opt/librenms
-chmod 771 /opt/librenms
-setfacl -d -m g::rwx /opt/librenms/bootstrap/cache /opt/librenms/storage /opt/librenms/logs /opt/librenms/rrd
-chmod -R ug=rwX /opt/librenms/bootstrap/cache /opt/librenms/storage /opt/librenms/logs /opt/librenms/rrd
-msg_ok "Configured LibreNMS"
-
-msg_info "Configure MariaDB"
-sed -i "/\[mysqld\]/a innodb_file_per_table=1\nlower_case_table_names=0" /etc/mysql/mariadb.conf.d/50-server.cnf
-systemctl enable -q --now mariadb
-msg_ok "Configured MariaDB"
-
-msg_info "Configure PHP-FPM"
-cp /etc/php/8.2/fpm/pool.d/www.conf /etc/php/8.2/fpm/pool.d/librenms.conf
-sed -i "s/\[www\]/\[librenms\]/g" /etc/php/8.2/fpm/pool.d/librenms.conf
-sed -i "s/user = www-data/user = librenms/g" /etc/php/8.2/fpm/pool.d/librenms.conf
-sed -i "s/group = www-data/group = librenms/g" /etc/php/8.2/fpm/pool.d/librenms.conf
-sed -i "s/listen = \/run\/php\/php8.2-fpm.sock/listen = \/run\/php-fpm-librenms.sock/g" /etc/php/8.2/fpm/pool.d/librenms.conf
-msg_ok "Configured PHP-FPM"
-
-msg_info "Configure Nginx"
-IP_ADDR=$(hostname -I | awk '{print $1}')
-cat >/etc/nginx/sites-enabled/librenms <<'EOF'
-server {
- listen 80;
- server_name ${IP_ADDR};
- root /opt/librenms/html;
- index index.php;
-
- charset utf-8;
- gzip on;
- gzip_types text/css application/javascript text/javascript application/x-javascript image/svg+xml text/plain text/xsd text/xsl text/xml image/x-icon;
- location / {
- try_files $uri $uri/ /index.php?$query_string;
- }
- location ~ [^/]\.php(/|$) {
- fastcgi_pass unix:/run/php-fpm-librenms.sock;
- fastcgi_split_path_info ^(.+\.php)(/.+)$;
- include fastcgi.conf;
- }
- location ~ /\.(?!well-known).* {
- deny all;
- }
-}
-EOF
-rm /etc/nginx/sites-enabled/default
-$STD systemctl reload nginx
-systemctl restart php8.2-fpm
-msg_ok "Configured Nginx"
-
-msg_info "Configure Services"
-COMPOSER_ALLOW_SUPERUSER=1
-$STD composer install --no-dev
-$STD php8.2 artisan migrate --force
-$STD php8.2 artisan key:generate --force
-$STD su librenms -s /bin/bash -c "lnms db:seed --force"
-$STD su librenms -s /bin/bash -c "lnms user:add -p admin -r admin admin"
-ln -s /opt/librenms/lnms /usr/bin/lnms
-mkdir -p /etc/bash_completion.d/
-cp /opt/librenms/misc/lnms-completion.bash /etc/bash_completion.d/
-cp /opt/librenms/snmpd.conf.example /etc/snmp/snmpd.conf
-
-RANDOM_STRING=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9')
-sed -i "s/RANDOMSTRINGHERE/$RANDOM_STRING/g" /etc/snmp/snmpd.conf
-echo "SNMP Community String: $RANDOM_STRING" >>~/librenms.creds
-curl -qo /usr/bin/distro https://raw.githubusercontent.com/librenms/librenms-agent/master/snmp/distro
-chmod +x /usr/bin/distro
-systemctl enable -q --now snmpd
-
-cp /opt/librenms/dist/librenms.cron /etc/cron.d/librenms
-cp /opt/librenms/dist/librenms-scheduler.service /opt/librenms/dist/librenms-scheduler.timer /etc/systemd/system/
-
-systemctl enable -q --now librenms-scheduler.timer
-cp /opt/librenms/misc/librenms.logrotate /etc/logrotate.d/librenms
-msg_ok "Configured Services"
-
-motd_ssh
-customize
-
-msg_info "Cleaning up"
-$STD apt-get -y autoremove
-$STD apt-get -y autoclean
-msg_ok "Cleaned"
diff --git a/install/livebook-install.sh b/install/livebook-install.sh
deleted file mode 100644
index 902d86d2b..000000000
--- a/install/livebook-install.sh
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: dkuku
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/livebook-dev/livebook
-
-source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
-color
-verb_ip6
-catch_errors
-setting_up_container
-network_check
-update_os
-
-msg_info "Installing Dependencies"
-$STD apt-get install -y \
- build-essential \
- ca-certificates \
- cmake \
- git \
- libncurses5-dev
-msg_ok "Installed Dependencies"
-
-msg_info "Creating livebook user"
-mkdir -p /opt/livebook /data
-export HOME=/opt/livebook
-$STD adduser --system --group --home /opt/livebook --shell /bin/bash livebook
-msg_ok "Created livebook user"
-
-
-msg_warn "WARNING: This script will run an external installer from a third-party source (https://elixir-lang.org)."
-msg_warn "The following code is NOT maintained or audited by our repository."
-msg_warn "If you have any doubts or concerns, please review the installer code before proceeding:"
-msg_custom "${TAB3}${GATEWAY}${BGN}${CL}" "\e[1;34m" "→ https://elixir-lang.org/install.sh"
-echo
-read -r -p "${TAB3}Do you want to continue? [y/N]: " CONFIRM
-if [[ ! "$CONFIRM" =~ ^([yY][eE][sS]|[yY])$ ]]; then
- msg_error "Aborted by user. No changes have been made."
- exit 10
-fi
-bash <(curl -sL https://elixir-lang.org/install.sh)
-
-msg_info "Setup Erlang and Elixir"
-ERLANG_VERSION=$(ls /opt/livebook/.elixir-install/installs/otp/ | head -n1)
-ELIXIR_VERSION=$(ls /opt/livebook/.elixir-install/installs/elixir/ | head -n1)
-LIVEBOOK_PASSWORD=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16)
-
-export ERLANG_BIN="/opt/livebook/.elixir-install/installs/otp/$ERLANG_VERSION/bin"
-export ELIXIR_BIN="/opt/livebook/.elixir-install/installs/elixir/$ELIXIR_VERSION/bin"
-export PATH="$ERLANG_BIN:$ELIXIR_BIN:$PATH"
-
-$STD mix local.hex --force
-$STD mix local.rebar --force
-$STD mix escript.install hex livebook --force
-
-cat </opt/livebook/.env
-export HOME=/opt/livebook
-export ERLANG_VERSION=$ERLANG_VERSION
-export ELIXIR_VERSION=$ELIXIR_VERSION
-export LIVEBOOK_PORT=8080
-export LIVEBOOK_IP="::"
-export LIVEBOOK_HOME=/data
-export LIVEBOOK_PASSWORD="$LIVEBOOK_PASSWORD"
-export ESCRIPTS_BIN=/opt/livebook/.mix/escripts
-export ERLANG_BIN="/opt/livebook/.elixir-install/installs/otp/\${ERLANG_VERSION}/bin"
-export ELIXIR_BIN="/opt/livebook/.elixir-install/installs/elixir/\${ELIXIR_VERSION}/bin"
-export PATH="\$ESCRIPTS_BIN:\$ERLANG_BIN:\$ELIXIR_BIN:\$PATH"
-EOF
-cat </opt/livebook/livebook.creds
-Livebook-Credentials
-Livebook Password: $LIVEBOOK_PASSWORD
-EOF
-msg_ok "Installed Erlang $ERLANG_VERSION and Elixir $ELIXIR_VERSION"
-
-msg_info "Installing Livebook"
-cat </etc/systemd/system/livebook.service
-[Unit]
-Description=Livebook
-After=network.target
-
-[Service]
-Type=exec
-User=livebook
-Group=livebook
-WorkingDirectory=/data
-EnvironmentFile=-/opt/livebook/.env
-ExecStart=/bin/bash -c 'source /opt/livebook/.env && cd /opt/livebook && livebook server'
-Restart=always
-RestartSec=5
-
-[Install]
-WantedBy=multi-user.target
-EOF
-chown -R livebook:livebook /opt/livebook /data
-systemctl enable -q --now livebook
-msg_ok "Installed Livebook"
-
-motd_ssh
-customize
-
-msg_info "Cleaning Up"
-$STD apt-get autoremove -y
-$STD apt-get autoclean
-msg_ok "Cleaned Up"
diff --git a/install/mealie-install.sh b/install/mealie-install.sh
new file mode 100644
index 000000000..8b330fab4
--- /dev/null
+++ b/install/mealie-install.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: MickLesk (CanbiZ)
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://mealie.io
+
+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 \
+ build-essential \
+ libpq-dev \
+ libwebp-dev \
+ libsasl2-dev \
+ libldap2-dev \
+ libssl-dev
+msg_ok "Installed Dependencies"
+
+PYTHON_VERSION="3.12" setup_uv
+POSTGRES_VERSION="16" setup_postgresql
+NODE_MODULE="yarn" NODE_VERSION="24" setup_nodejs
+fetch_and_deploy_gh_release "mealie" "mealie-recipes/mealie" "tarball" "latest" "/opt/mealie"
+PG_DB_NAME="mealie_db" PG_DB_USER="mealie_user" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db
+
+msg_info "Installing Python Dependencies with uv"
+cd /opt/mealie
+$STD uv sync --frozen --extra pgsql
+msg_ok "Installed Python Dependencies"
+
+msg_info "Building Frontend"
+export NUXT_TELEMETRY_DISABLED=1
+cd /opt/mealie/frontend
+$STD yarn install --prefer-offline --frozen-lockfile --non-interactive --production=false --network-timeout 1000000
+$STD yarn generate
+msg_ok "Built Frontend"
+
+msg_info "Copying Built Frontend"
+mkdir -p /opt/mealie/mealie/frontend
+cp -r /opt/mealie/frontend/dist/* /opt/mealie/mealie/frontend/
+msg_ok "Copied Frontend"
+
+msg_info "Downloading NLTK Data"
+mkdir -p /nltk_data/
+cd /opt/mealie
+$STD uv run python -m nltk.downloader -d /nltk_data averaged_perceptron_tagger_eng
+msg_ok "Downloaded NLTK Data"
+
+msg_info "Writing Environment File"
+SECRET=$(openssl rand -hex 32)
+mkdir -p /run/secrets
+cat </opt/mealie/mealie.env
+MEALIE_HOME=/opt/mealie
+NLTK_DATA=/nltk_data
+SECRET=${SECRET}
+
+DB_ENGINE=postgres
+POSTGRES_SERVER=localhost
+POSTGRES_PORT=5432
+POSTGRES_USER=${PG_DB_USER}
+POSTGRES_PASSWORD=${PG_DB_PASS}
+POSTGRES_DB=${PG_DB_NAME}
+
+PRODUCTION=true
+HOST=0.0.0.0
+PORT=9000
+EOF
+msg_ok "Wrote Environment File"
+
+msg_info "Creating Start Script"
+cat <<'EOF' >/opt/mealie/start.sh
+#!/bin/bash
+set -a
+source /opt/mealie/mealie.env
+set +a
+exec uv run mealie
+EOF
+chmod +x /opt/mealie/start.sh
+msg_ok "Created Start Script"
+
+msg_info "Creating Systemd Service"
+cat <<'EOF' >/etc/systemd/system/mealie.service
+[Unit]
+Description=Mealie Recipe Manager
+After=network.target postgresql.service
+Wants=postgresql.service
+
+[Service]
+Type=simple
+User=root
+WorkingDirectory=/opt/mealie
+ExecStart=/opt/mealie/start.sh
+Restart=on-failure
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
+EOF
+systemctl enable -q --now mealie
+msg_ok "Created and Started Service"
+
+motd_ssh
+customize
+cleanup_lxc
diff --git a/install/metabase-install.sh b/install/metabase-install.sh
new file mode 100644
index 000000000..f14ff7c2d
--- /dev/null
+++ b/install/metabase-install.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: Slaviša Arežina (tremor021)
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# Source: https://www.metabase.com/
+
+source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
+color
+verb_ip6
+catch_errors
+setting_up_container
+network_check
+update_os
+
+JAVA_VERSION="21" setup_java
+PG_VERSION="17" setup_postgresql
+PG_DB_NAME="metabase_db" PG_DB_USER="metabase" setup_postgresql_db
+
+msg_info "Setting up Metabase"
+mkdir -p /opt/metabase
+RELEASE=$(get_latest_github_release "metabase/metabase")
+curl -fsSL "https://downloads.metabase.com/v${RELEASE}.x/metabase.jar" -o /opt/metabase/metabase.jar
+cd /opt/metabase
+
+cat </opt/metabase/.env
+MB_DB_TYPE=postgres
+MB_DB_DBNAME=$PG_DB_NAME
+MB_DB_PORT=5432
+MB_DB_USER=$PG_DB_USER
+MB_DB_PASS=$PG_DB_PASS
+MB_DB_HOST=localhost
+EOF
+echo $RELEASE >~/.metabase
+msg_ok "Setup Metabase"
+
+msg_info "Creating Service"
+cat </etc/systemd/system/metabase.service
+[Unit]
+Description=Metabase Service
+After=network.target
+
+[Service]
+EnvironmentFile=/opt/metabase/.env
+WorkingDirectory=/opt/metabase
+ExecStart=/usr/bin/java --add-opens java.base/java.nio=ALL-UNNAMED -jar metabase.jar
+Restart=always
+SuccessExitStatus=143
+TimeoutStopSec=120
+
+[Install]
+WantedBy=multi-user.target
+EOF
+systemctl enable -q --now metabase
+msg_ok "Created Service"
+
+motd_ssh
+customize
+cleanup_lxc
diff --git a/install/notesnook-install.sh b/install/notesnook-install.sh
deleted file mode 100644
index dbc93cbf8..000000000
--- a/install/notesnook-install.sh
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: Slaviša Arežina (tremor021)
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/streetwriters/notesnook
-
-source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
-color
-verb_ip6
-catch_errors
-setting_up_container
-network_check
-update_os
-
-msg_info "Installing Dependencies"
-$STD apt-get install -y \
- make \
- git \
- caddy
-msg_ok "Installed Dependencies"
-
-NODE_VERSION="22" setup_nodejs
-fetch_and_deploy_gh_release "notesnook" "streetwriters/notesnook" "tarball"
-
-msg_info "Configuring Notesnook (Patience)"
-cd /opt/notesnook
-export NODE_OPTIONS="--max-old-space-size=2560"
-$STD npm install
-$STD npm run build:web
-msg_ok "Configured Notesnook"
-
-msg_info "Configuring Caddy"
-LOCAL_IP=$(hostname -I | awk '{print $1}')
-cat </etc/caddy/Caddyfile
-{
- email admin@example.com
-}
-
-${LOCAL_IP} {
- reverse_proxy 127.0.0.1:3000
-}
-EOF
-msg_ok "Configured Caddy"
-
-msg_info "Creating Service"
-cat </etc/systemd/system/notesnook.service
-[Unit]
-Description=Notesnook Service
-After=network-online.target
-
-[Service]
-Type=simple
-User=root
-WorkingDirectory=/opt/notesnook
-ExecStart=/usr/bin/npx serve apps/web/build
-Restart=on-failure
-
-[Install]
-WantedBy=multi-user.target
-EOF
-systemctl reload caddy
-systemctl enable -q --now notesnook
-msg_ok "Created Service"
-
-motd_ssh
-customize
-
-msg_info "Cleaning up"
-$STD apt-get -y autoremove
-$STD apt-get -y autoclean
-msg_ok "Cleaned"
diff --git a/install/patchmon-install.sh b/install/patchmon-install.sh
deleted file mode 100644
index 209b03e1c..000000000
--- a/install/patchmon-install.sh
+++ /dev/null
@@ -1,289 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: vhsdream
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/PatcMmon/PatchMon
-
-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 \
- build-essential \
- gcc \
- nginx \
- redis-server
-msg_ok "Installed Dependencies"
-
-NODE_VERSION="24" setup_nodejs
-PG_VERSION="17" setup_postgresql
-
-msg_info "Creating PostgreSQL Database"
-DB_NAME=patchmon_db
-DB_USER=patchmon_usr
-DB_PASS="$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | cut -c1-13)"
-$STD sudo -u postgres psql -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PASS';"
-$STD sudo -u postgres psql -c "CREATE DATABASE $DB_NAME WITH OWNER $DB_USER ENCODING 'UTF8' TEMPLATE template0;"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET client_encoding TO 'utf8';"
-$STD sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;"
-
-cat <~/patchmon.creds
-PatchMon Credentials
-PatchMon Database Name: $DB_NAME
-PatchMon Database User: $DB_USER
-PatchMon Database Password: $DB_PASS
-EOF
-msg_ok "Created PostgreSQL Database"
-
-fetch_and_deploy_gh_release "PatchMon" "PatchMon/PatchMon" "tarball" "latest" "/opt/patchmon"
-
-msg_info "Configuring PatchMon"
-cd /opt/patchmon
-export NODE_ENV=production
-$STD npm install --no-audit --no-fund --no-save --ignore-scripts
-cd /opt/patchmon/backend
-$STD npm install --no-audit --no-fund --no-save --ignore-scripts
-cd /opt/patchmon/frontend
-$STD npm install --include=dev --no-audit --no-fund --no-save --ignore-scripts
-$STD npm run build
-
-JWT_SECRET="$(openssl rand -base64 64 | tr -d "=+/" | cut -c1-50)"
-LOCAL_IP="$(hostname -I | awk '{print $1}')"
-cat </opt/patchmon/backend/.env
-# Database Configuration
-DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME"
-PY_THRESHOLD=3M_DB_CONN_MAX_ATTEMPTS=30
-PM_DB_CONN_WAIT_INTERVAL=2
-
-# JWT Configuration
-JWT_SECRET="$JWT_SECRET"
-JWT_EXPIRES_IN=1h
-JWT_REFRESH_EXPIRES_IN=7d
-
-# Server Configuration
-PORT=3399
-NODE_ENV=production
-
-# API Configuration
-API_VERSION=v1
-
-# CORS Configuration
-CORS_ORIGIN="http://$LOCAL_IP"
-
-# Session Configuration
-SESSION_INACTIVITY_TIMEOUT_MINUTES=30
-
-# User Configuration
-DEFAULT_USER_ROLE=user
-
-# Rate Limiting (times in milliseconds)
-RATE_LIMIT_WINDOW_MS=900000
-RATE_LIMIT_MAX=5000
-AUTH_RATE_LIMIT_WINDOW_MS=600000
-AUTH_RATE_LIMIT_MAX=500
-AGENT_RATE_LIMIT_WINDOW_MS=60000
-AGENT_RATE_LIMIT_MAX=1000
-
-# Redis Configuration
-REDIS_HOST=localhost
-REDIS_PORT=6379
-
-# Logging
-LOG_LEVEL=info
-ENABLE_LOGGING=true
-
-# TFA Configuration
-TFA_REMEMBER_ME_EXPIRES_IN=30d
-TFA_MAX_REMEMBER_SESSIONS=5
-TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
-EOF
-
-cat </opt/patchmon/frontend/.env
-VITE_API_URL=http://$LOCAL_IP/api/v1
-VITE_APP_NAME=PatchMon
-VITE_APP_VERSION=1.3.0
-EOF
-
-cd /opt/patchmon/backend
-$STD npx prisma migrate deploy
-$STD npx prisma generate
-msg_ok "Configured PatchMon"
-
-msg_info "Configuring Nginx"
-cat </etc/nginx/sites-available/patchmon.conf
-server {
- listen 80;
- server_name $LOCAL_IP;
-
- # Security headers
- add_header X-Frame-Options DENY always;
- add_header X-Content-Type-Options nosniff always;
- add_header X-XSS-Protection "1; mode=block" always;
- add_header Referrer-Policy "strict-origin-when-cross-origin" always;
-
- # Frontend
- location / {
- root /opt/patchmon/frontend/dist;
- try_files \$uri \$uri/ /index.html;
- }
-
- # Bull Board proxy
- location /bullboard {
- proxy_pass http://127.0.0.1:3399;
- proxy_http_version 1.1;
- proxy_set_header Upgrade \$http_upgrade;
- proxy_set_header Connection 'upgrade';
- proxy_set_header Host \$host;
- proxy_set_header X-Real-IP \$remote_addr;
- proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto \$scheme;
- proxy_set_header X-Forwarded-Host \$host;
- proxy_set_header Cookie \$http_cookie;
- proxy_cache_bypass \$http_upgrade;
- proxy_read_timeout 300s;
- proxy_connect_timeout 75s;
-
- # Enable cookie passthrough
- proxy_pass_header Set-Cookie;
- proxy_cookie_path / /;
-
- # Preserve original client IP
- proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
- if (\$request_method = 'OPTIONS') {
- return 204;
- }
- }
-
- # API proxy
- location /api/ {
- proxy_pass http://127.0.0.1:3399;
- proxy_http_version 1.1;
- proxy_set_header Upgrade \$http_upgrade;
- proxy_set_header Connection 'upgrade';
- proxy_set_header Host \$host;
- proxy_set_header X-Real-IP \$remote_addr;
- proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto \$scheme;
- proxy_cache_bypass \$http_upgrade;
- proxy_read_timeout 300s;
- proxy_connect_timeout 75s;
-
- # Preserve original client IP
- proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
- if (\$request_method = 'OPTIONS') {
- return 204;
- }
- }
-
- # Static assets caching (exclude Bull Board assets)
- location ~* ^/(?!bullboard).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
- root /opt/patchmon/frontend/dist;
- expires 1y;
- add_header Cache-Control "public, immutable";
- }
-
- # Health check endpoint
- location /health {
- proxy_pass http://127.0.0.1:3399/health;
- access_log off;
- }
-}
-EOF
-ln -sf /etc/nginx/sites-available/patchmon.conf /etc/nginx/sites-enabled/
-rm -f /etc/nginx/sites-enabled/default
-$STD nginx -t
-systemctl restart nginx
-msg_ok "Configured Nginx"
-
-msg_info "Creating service"
-cat </etc/systemd/system/patchmon-server.service
-[Unit]
-Description=PatchMon Service
-After=network.target postgresql.service
-
-[Service]
-Type=simple
-WorkingDirectory=/opt/patchmon/backend
-ExecStart=/usr/bin/node src/server.js
-Restart=always
-RestartSec=10
-Environment=NODE_ENV=production
-Environment=PATH=/usr/bin:/usr/local/bin
-NoNewPrivileges=true
-PrivateTmp=true
-ProtectSystem=strict
-ProtectHome=true
-ReadWritePaths=/opt/patchmon
-
-[Install]
-WantedBy=multi-user.target
-EOF
-systemctl enable -q --now patchmon-server
-msg_ok "Created and started service"
-
-msg_info "Updating settings"
-cat </opt/patchmon/backend/update-settings.js
-const { PrismaClient } = require('@prisma/client');
-const { v4: uuidv4 } = require('uuid');
-const prisma = new PrismaClient();
-
-async function updateSettings() {
- try {
- const existingSettings = await prisma.settings.findFirst();
-
- const settingsData = {
- id: uuidv4(),
- server_url: 'http://$LOCAL_IP',
- server_protocol: 'http',
- server_host: '$LOCAL_IP',
- server_port: 3399,
- update_interval: 60,
- auto_update: true,
- signup_enabled: false,
- ignore_ssl_self_signed: false,
- updated_at: new Date()
- };
-
- if (existingSettings) {
- // Update existing settings
- await prisma.settings.update({
- where: { id: existingSettings.id },
- data: settingsData
- });
- } else {
- // Create new settings record
- await prisma.settings.create({
- data: settingsData
- });
- }
-
- console.log('✅ Database settings updated successfully');
- } catch (error) {
- console.error('❌ Error updating settings:', error.message);
- process.exit(1);
- } finally {
- await prisma.\$disconnect();
- }
-}
-
-updateSettings();
-EOF
-
-cd /opt/patchmon/backend
-$STD node update-settings.js
-msg_ok "Settings updated successfully"
-
-motd_ssh
-customize
-
-msg_info "Cleaning up"
-$STD apt -y autoremove
-$STD apt -y autoclean
-$STD apt -y clean
-msg_ok "Cleaned"
diff --git a/install/postiz-install.sh b/install/postiz-install.sh
deleted file mode 100644
index 5b9e35923..000000000
--- a/install/postiz-install.sh
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: Slaviša Arežina (tremor021)
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: https://github.com/gitroomhq/postiz-app
-
-source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
-color
-verb_ip6
-catch_errors
-setting_up_container
-network_check
-update_os
-
-msg_info "Installing dependencies"
-$STD apt-get install -y \
- build-essential \
- python3-pip \
- supervisor \
- debian-keyring \
- debian-archive-keyring \
- apt-transport-https \
- redis
-msg_ok "Installed dependencies"
-
-NODE_VERSION="20" setup_nodejs
-PG_VERSION="17" setup_postgresql
-
-msg_info "Setting up PostgreSQL Database"
-DB_NAME=postiz
-DB_USER=postiz
-DB_PASS="$(openssl rand -base64 18 | cut -c1-13)"
-$STD sudo -u postgres psql -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PASS';"
-$STD sudo -u postgres psql -c "CREATE DATABASE $DB_NAME WITH OWNER $DB_USER ENCODING 'UTF8' TEMPLATE template0;"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET client_encoding TO 'utf8';"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET default_transaction_isolation TO 'read committed';"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET timezone TO 'UTC'"
-{
- echo "Postiz DB Credentials"
- echo "Postiz Database User: $DB_USER"
- echo "Postiz Database Password: $DB_PASS"
- echo "Postiz Database Name: $DB_NAME"
-} >>~/postiz.creds
-msg_ok "Set up PostgreSQL Database"
-
-msg_info "Setting up Caddy"
-curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/gpg.key" | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
-curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt" >/etc/apt/sources.list.d/caddy-stable.list
-$STD apt-get update
-$STD apt-get install caddy
-msg_ok "Set up Caddy"
-
-fetch_and_deploy_gh_release "postiz" "gitroomhq/postiz-app"
-
-msg_info "Configuring Postiz"
-LOCAL_IP=$(hostname -I | awk '{print $1}')
-JWT_SECRET=$(openssl rand -base64 64 | tr '+/' '-_' | tr -d '=')
-cd /opt/postiz
-mkdir -p /etc/supervisor.d
-$STD npm --no-update-notifier --no-fund --global install pnpm@10.6.1 pm2
-cp var/docker/supervisord.conf /etc/supervisord.conf
-cp var/docker/Caddyfile ./Caddyfile
-cp var/docker/entrypoint.sh ./entrypoint.sh
-cp var/docker/supervisord/caddy.conf /etc/supervisor.d/caddy.conf
-sed -i "s#/app/Caddyfile#/opt/postiz/Caddyfile#g" /etc/supervisor.d/caddy.conf
-sed -i "s#/app/Caddyfile#/opt/postiz/Caddyfile#g" /opt/postiz/entrypoint.sh
-sed -i "s#directory=/app#directory=/opt/postiz#g" /etc/supervisor.d/caddy.conf
-export NODE_OPTIONS="--max-old-space-size=2560"
-$STD pnpm install
-$STD pnpm run build
-chmod +x entrypoint.sh
-
-cat <.env
-NOT_SECURED="true"
-IS_GENERAL="true"
-DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME"
-REDIS_URL="redis://localhost:6379"
-JWT_SECRET="$JWT_SECRET"
-FRONTEND_URL="http://$LOCAL_IP:4200"
-NEXT_PUBLIC_BACKEND_URL="http://$LOCAL_IP:3000"
-BACKEND_INTERNAL_URL="http://$LOCAL_IP:3000"
-EOF
-msg_ok "Configured Postiz"
-
-msg_info "Creating Service"
-cat </etc/systemd/system/postiz.service
-[Unit]
-Description=Postiz Service
-After=network.target
-
-[Service]
-Type=simple
-User=root
-WorkingDirectory=/opt/postiz
-EnvironmentFile=/opt/postiz/.env
-ExecStart=/usr/bin/pnpm run pm2-run
-Restart=always
-
-[Install]
-WantedBy=multi-user.target
-EOF
-systemctl enable -q --now postiz
-msg_ok "Created Service"
-
-motd_ssh
-customize
-
-msg_info "Cleaning up"
-$STD apt-get -y autoremove
-$STD apt-get -y autoclean
-msg_ok "Cleaned"
diff --git a/install/proxmox-datacenter-manager-install.sh b/install/proxmox-datacenter-manager-install.sh
deleted file mode 100644
index e9a3c3006..000000000
--- a/install/proxmox-datacenter-manager-install.sh
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright (c) 2021-2025 community-scripts ORG
-# Author: CrazyWolf13
-# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-# Source: Proxmox Server Solution GmbH
-
-source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
-color
-verb_ip6
-catch_errors
-setting_up_container
-network_check
-update_os
-
-msg_info "Installing Proxmox Datacenter Manager"
-curl -fsSL https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg -o /usr/share/keyrings/proxmox-archive-keyring.gpg
-echo "deb [signed-by=/usr/share/keyrings/proxmox-archive-keyring.gpg] http://download.proxmox.com/debian/pdm bookworm pdm-test " >/etc/apt/sources.list.d/pdm-test.list
-$STD apt-get update
-DEBIAN_FRONTEND=noninteractive
-$STD apt-get -o Dpkg::Options::="--force-confdef" \
- -o Dpkg::Options::="--force-confold" \
- install -y proxmox-datacenter-manager \
- proxmox-datacenter-manager-ui
-msg_ok "Installed Proxmox Datacenter Manager"
-
-motd_ssh
-customize
-
-msg_info "Cleaning up"
-$STD apt-get -y autoremove
-$STD apt-get -y autoclean
-msg_ok "Cleaned"
diff --git a/install/rybbit-install.sh b/install/rybbit-install.sh
index 055a101a0..7bec945aa 100644
--- a/install/rybbit-install.sh
+++ b/install/rybbit-install.sh
@@ -13,69 +13,105 @@ setting_up_container
network_check
update_os
-msg_info "Installing Dependencies"
-$STD apt-get install -y \
- caddy \
- apt-transport-https \
- ca-certificates
-msg_ok "Installed Dependencies"
-
setup_clickhouse
PG_VERSION=17 setup_postgresql
-NODE_VERSION="20" NODE_MODULE="next" setup_nodejs
-
-#sed -i 's|default|read_only|' /etc/clickhouse-server/users.xml
-#sed -i 's||DISABLED|' /etc/clickhouse-server/users.xml
-
-msg_info "Setting up PostgreSQL Database"
-DB_NAME=rybbit_db
-DB_USER=rybbit
-DB_PASS="$(openssl rand -base64 18 | cut -c1-13)"
-$STD sudo -u postgres psql -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PASS';"
-$STD sudo -u postgres psql -c "CREATE DATABASE $DB_NAME WITH OWNER $DB_USER ENCODING 'UTF8' TEMPLATE template0;"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET client_encoding TO 'utf8';"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET default_transaction_isolation TO 'read committed';"
-$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET timezone TO 'UTC'"
-{
- echo "Rybbit-Credentials"
- echo "Rybbit Database User: $DB_USER"
- echo "Rybbit Database Password: $DB_PASS"
- echo "Rybbit Database Name: $DB_NAME"
-} >>~/rybbit.creds
-msg_ok "Set up PostgreSQL Database"
+NODE_VERSION="24" NODE_MODULE="next" setup_nodejs
+PG_DB_NAME="rybbit_db" PG_DB_USER="rybbit" setup_postgresql_db
fetch_and_deploy_gh_release "rybbit" "rybbit-io/rybbit" "tarball" "latest" "/opt/rybbit"
+msg_info "Building Rybbit Shared Module"
cd /opt/rybbit/shared
-npm install
-npm run build
+$STD npm install
+$STD npm run build
+msg_ok "Built Shared Module"
+msg_info "Building Rybbit Server"
cd /opt/rybbit/server
-npm ci
-npm run build
+$STD npm ci
+$STD npm run build
+msg_ok "Built Server"
+msg_info "Building Rybbit Client"
cd /opt/rybbit/client
-npm ci --legacy-peer-deps
-npm run build
+NEXT_PUBLIC_BACKEND_URL="http://localhost:3001" \
+ NEXT_PUBLIC_DISABLE_SIGNUP="false" \
+ $STD npm ci --legacy-peer-deps
+$STD npm run build
+msg_ok "Built Client"
-mv /opt/rybbit/.env.example /opt/rybbit/.env
-sed -i "s|^POSTGRES_DB=.*|POSTGRES_DB=$DB_NAME|g" /opt/rybbit/.env
-sed -i "s|^POSTGRES_USER=.*|POSTGRES_USER=$DB_USER|g" /opt/rybbit/.env
-sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$DB_PASS|g" /opt/rybbit/.env
-sed -i "s|^DOMAIN_NAME=.*|DOMAIN_NAME=localhost|g" /opt/rybbit/.env
-sed -i "s|^BASE_URL=.*|BASE_URL=\"http://localhost\"|g" /opt/rybbit/.env
-msg_ok "Rybbit Installed"
+msg_info "Configuring Rybbit"
+CONTAINER_IP=$(hostname -I | awk '{print $1}')
+BETTER_AUTH_SECRET=$(openssl rand -hex 32)
-msg_info "Setting up Caddy"
-mkdir -p /etc/caddy
-cp /opt/rybbit/Caddyfile /etc/caddy/Caddyfile
-systemctl enable -q --now caddy
-msg_ok "Caddy Setup"
+cat >/opt/rybbit/.env </etc/systemd/system/rybbit-server.service </etc/systemd/system/rybbit-client.service <>~/snowshare.creds
+msg_ok "Set up PostgreSQL Database"
+
+msg_info "Installing SnowShare"
+cd /opt/snowshare
+$STD npm ci
+cat </opt/snowshare.env
+DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME"
+NEXTAUTH_URL="http://localhost:3000"
+NEXTAUTH_SECRET="$(openssl rand -base64 32)"
+ALLOW_SIGNUP=true
+NODE_ENV=production
+EOF
+set -a
+source /opt/snowshare.env
+set +a
+$STD npx prisma generate
+$STD npx prisma migrate deploy
+$STD npm run build
+cat </etc/systemd/system/snowshare.service
+[Unit]
+Description=SnowShare - Modern File Sharing Platform
+After=network.target postgresql.service
+Requires=postgresql.service
+
+[Service]
+Type=simple
+WorkingDirectory=/opt/snowshare
+EnvironmentFile=/opt/snowshare.env
+ExecStart=/usr/bin/npm start
+Restart=on-failure
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+EOF
+systemctl enable -q --now snowshare
+msg_ok "Installed SnowShare"
+
+msg_info "Setting up Cleanup Cron Job"
+cat </etc/cron.d/snowshare-cleanup
+0 2 * * * root cd /opt/snowshare && /usr/bin/npm run cleanup:expired >> /var/log/snowshare-cleanup.log 2>&1
+EOF
+msg_ok "Set up Cleanup Cron Job"
+
+motd_ssh
+customize
+
+msg_info "Cleaning up"
+$STD apt -y autoremove
+$STD apt -y autoclean
+$STD apt -y clean
+msg_ok "Cleaned"
diff --git a/install/transmission-openvpn-install.sh b/install/transmission-openvpn-install.sh
index 7eeeb422c..eec848203 100644
--- a/install/transmission-openvpn-install.sh
+++ b/install/transmission-openvpn-install.sh
@@ -16,15 +16,15 @@ update_os
msg_info "Installing Dependencies"
$STD apt install -y \
- dnsutils \
- iputils-ping \
- ufw \
- iproute2
+ dnsutils \
+ iputils-ping \
+ ufw \
+ iproute2
mkdir -p /etc/systemd/system-preset
-echo "disable *" > /etc/systemd/system-preset/99-no-autostart.preset
+echo "disable *" >/etc/systemd/system-preset/99-no-autostart.preset
$STD apt install -y \
- transmission-daemon \
- privoxy
+ transmission-daemon \
+ privoxy
rm -f /etc/systemd/system-preset/99-no-autostart.preset
$STD systemctl preset-all
$STD systemctl disable --now transmission-daemon
@@ -49,12 +49,13 @@ chmod +x /opt/privoxy/*.sh
$STD ln -s /usr/bin/transmission-daemon /usr/local/bin/transmission-daemon
$STD update-alternatives --set iptables /usr/sbin/iptables-legacy
$STD update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
+rm -rf /opt/docker-transmission-openvpn
msg_ok "Configured transmission-openvpn"
msg_info "Creating Service"
LOCAL_SUBNETS=$(
- ip -o -4 addr show \
- | awk '!/127.0.0.1/ {
+ ip -o -4 addr show |
+ awk '!/127.0.0.1/ {
split($4, a, "/"); ip=a[1]; mask=a[2];
split(ip, o, ".");
if (mask < 8) {
@@ -66,12 +67,12 @@ LOCAL_SUBNETS=$(
} else {
print o[1]"."o[2]"."o[3]".*";
}
- }' \
- | sort -u | paste -sd, -
+ }' |
+ sort -u | paste -sd, -
)
TRANSMISSION_RPC_WHITELIST="127.0.0.*,${LOCAL_SUBNETS}"
mkdir -p /opt/transmission-openvpn
-cat < "/opt/transmission-openvpn/.env"
+cat <"/opt/transmission-openvpn/.env"
OPENVPN_USERNAME="username"
OPENVPN_PASSWORD="password"
OPENVPN_PROVIDER="PIA"
@@ -111,7 +112,7 @@ LOG_TO_STDOUT="false"
HEALTH_CHECK_HOST="google.com"
SELFHEAL="false"
EOF
-cat < /etc/systemd/system/openvpn-custom.service
+cat </etc/systemd/system/openvpn-custom.service
[Unit]
Description=Custom OpenVPN start service
After=network.target
@@ -126,15 +127,9 @@ EnvironmentFile=/opt/transmission-openvpn/.env
[Install]
WantedBy=multi-user.target
EOF
-systemctl enable --now -q openvpn-custom.service
+systemctl enable -q --now openvpn-custom
msg_ok "Created Service"
motd_ssh
customize
-
-msg_info "Cleaning up"
-$STD apt -y autoremove
-$STD apt -y autoclean
-$STD apt -y clean
-rm -rf /opt/docker-transmission-openvpn
-msg_ok "Cleaned"
+cleanup_lxc
diff --git a/install/wallabag-install.sh b/install/wallabag-install.sh
index 2c1231d21..53c54ab8f 100644
--- a/install/wallabag-install.sh
+++ b/install/wallabag-install.sh
@@ -4,7 +4,7 @@
# Author: MickLesk (Canbiz)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
+source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
@@ -14,10 +14,10 @@ update_os
msg_info "Installing Dependencies (Patience)"
$STD apt-get install -y \
- make \
- apache2 \
- libapache2-mod-php \
- redis
+ make \
+ apache2 \
+ libapache2-mod-php \
+ redis
msg_ok "Installed Dependencies"
setup_mariadb
@@ -33,10 +33,10 @@ $STD mariadb -u root -e "CREATE DATABASE $DB_NAME;"
$STD mariadb -u root -e "CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';"
$STD mariadb -u root -e "GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;"
{
- echo "Wallabag Credentials"
- echo "Database User: $DB_USER"
- echo "Database Password: $DB_PASS"
- echo "Database Name: $DB_NAME"
+ echo "Wallabag Credentials"
+ echo "Database User: $DB_USER"
+ echo "Database Password: $DB_PASS"
+ echo "Database Name: $DB_NAME"
} >>~/wallabag.creds
msg_ok "Set up Database"
@@ -48,12 +48,12 @@ useradd -d /opt/wallabag -s /bin/bash -M wallabag
chown -R wallabag:wallabag /opt/wallabag
mv /opt/wallabag/app/config/parameters.yml.dist /opt/wallabag/app/config/parameters.yml
sed -i \
- -e 's|database_name: wallabag|database_name: wallabag_db|' \
- -e 's|database_port: ~|database_port: 3306|' \
- -e 's|database_user: root|database_user: wallabag|' \
- -e 's|database_password: ~|database_password: '"$DB_PASS"'|' \
- -e 's|secret: .*|secret: '"$SECRET_KEY"'|' \
- /opt/wallabag/app/config/parameters.yml
+ -e 's|database_name: wallabag|database_name: wallabag_db|' \
+ -e 's|database_port: ~|database_port: 3306|' \
+ -e 's|database_user: root|database_user: wallabag|' \
+ -e 's|database_password: ~|database_password: '"$DB_PASS"'|' \
+ -e 's|secret: .*|secret: '"$SECRET_KEY"'|' \
+ /opt/wallabag/app/config/parameters.yml
export COMPOSER_ALLOW_SUPERUSER=1
sudo -u wallabag make install --no-interaction
diff --git a/install/web-check-install.sh b/install/web-check-install.sh
new file mode 100644
index 000000000..dd21d8ca9
--- /dev/null
+++ b/install/web-check-install.sh
@@ -0,0 +1,141 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: CrazyWolf13
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://github.com/lissy93/web-check
+
+source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
+color
+verb_ip6
+catch_errors
+setting_up_container
+network_check
+update_os
+
+msg_info "Installing Dependencies"
+export DEBIAN_FRONTEND=noninteractive
+$STD apt -y install --no-install-recommends \
+ git \
+ traceroute \
+ make \
+ g++ \
+ traceroute \
+ xvfb \
+ dbus \
+ xorg \
+ xvfb \
+ gtk2-engines-pixbuf \
+ dbus-x11 \
+ xfonts-base \
+ xfonts-100dpi \
+ xfonts-75dpi \
+ xfonts-scalable \
+ imagemagick \
+ x11-apps
+msg_ok "Installed Dependencies"
+
+NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs
+
+msg_info "Setup Python3"
+$STD apt install -y python3
+rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
+msg_ok "Setup Python3"
+
+msg_info "Installing Chromium"
+curl -fsSL https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg
+cat </dev/null
+Types: deb
+URIs: http://dl.google.com/linux/chrome/deb/
+Suites: stable
+Components: main
+Architectures: amd64
+Signed-By: /usr/share/keyrings/google-chrome-keyring.gpg
+EOF
+$STD apt update
+$STD apt -y install \
+ chromium \
+ libxss1 \
+ lsb-release
+msg_ok "Installed Chromium"
+
+msg_info "Setting up Chromium"
+/usr/bin/chromium --no-sandbox --version >/etc/chromium-version
+chmod 755 /usr/bin/chromium
+msg_ok "Setup Chromium"
+
+fetch_and_deploy_gh_release "web-check" "MickLesk/web-check"
+
+msg_info "Installing Web-Check (Patience)"
+cd /opt/web-check
+cat <<'EOF' >/opt/web-check/.env
+CHROME_PATH=/usr/bin/chromium
+PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
+HEADLESS=true
+GOOGLE_CLOUD_API_KEY=''
+REACT_APP_SHODAN_API_KEY=''
+REACT_APP_WHO_API_KEY=''
+SECURITY_TRAILS_API_KEY=''
+CLOUDMERSIVE_API_KEY=''
+TRANCO_USERNAME=''
+TRANCO_API_KEY=''
+URL_SCAN_API_KEY=''
+BUILT_WITH_API_KEY=''
+TORRENT_IP_API_KEY=''
+PORT='3000'
+DISABLE_GUI='false'
+API_TIMEOUT_LIMIT='10000'
+API_CORS_ORIGIN='*'
+API_ENABLE_RATE_LIMIT='false'
+REACT_APP_API_ENDPOINT='/api'
+ENABLE_ANALYTICS='false'
+EOF
+$STD yarn install --frozen-lockfile --network-timeout 100000
+msg_ok "Installed Web-Check"
+
+msg_info "Building Web-Check"
+$STD yarn build --production
+rm -rf /var/lib/apt/lists/* /app/node_modules/.cache
+msg_ok "Built Web-Check"
+
+msg_info "Creating Service"
+cat <<'EOF' >/opt/run_web-check.sh
+#!/bin/bash
+SCREEN_RESOLUTION="1280x1024x24"
+if ! systemctl is-active --quiet dbus; then
+ echo "Warning: dbus service is not running. Some features may not work properly."
+fi
+[[ -z "${DISPLAY}" ]] && export DISPLAY=":99"
+Xvfb "${DISPLAY}" -screen 0 "${SCREEN_RESOLUTION}" &
+XVFB_PID=$!
+sleep 2
+cd /opt/web-check
+exec yarn start
+EOF
+chmod +x /opt/run_web-check.sh
+cat <<'EOF' >/etc/systemd/system/web-check.service
+[Unit]
+Description=Web Check Service
+After=network.target
+
+[Service]
+Type=simple
+User=root
+Group=root
+WorkingDirectory=/opt/web-check
+EnvironmentFile=/opt/web-check/.env
+ExecStartPre=/bin/bash -c "service dbus start || true"
+ExecStartPre=/bin/bash -c "if ! pgrep -f 'Xvfb.*:99' > /dev/null; then Xvfb :99 -screen 0 1280x1024x24 & fi"
+ExecStart=/opt/run_web-check.sh
+Restart=on-failure
+Environment=DISPLAY=:99
+
+[Install]
+WantedBy=multi-user.target
+EOF
+systemctl enable -q --now web-check
+msg_ok "Created Service"
+
+motd_ssh
+customize
+cleanup_lxc
diff --git a/misc/REFACTORING_SUMMARY.md b/misc/REFACTORING_SUMMARY.md
new file mode 100644
index 000000000..8365a50d3
--- /dev/null
+++ b/misc/REFACTORING_SUMMARY.md
@@ -0,0 +1,234 @@
+# Build.func Refactoring Summary - CORRECTED
+
+**Datum:** 29.10.2025
+**Backup:** build.func.backup-refactoring-\*
+
+## Durchgeführte Änderungen (KORRIGIERT)
+
+### 1. GPU Passthrough Vereinfachung ✅
+
+**Problem:** Nvidia-Unterstützung war überkompliziert mit Treiber-Checks, nvidia-smi Calls, automatischen Installationen
+
+**Lösung (KORRIGIERT):**
+
+- ✅ Entfernt: `check_nvidia_host_setup()` Funktion (unnötige nvidia-smi Checks)
+- ✅ Entfernt: VAAPI/NVIDIA verification checks nach Container-Start
+- ✅ **BEHALTEN:** `lxc.mount.entry` für alle GPU-Typen (Intel/AMD/NVIDIA) ✅✅✅
+- ✅ **BEHALTEN:** `lxc.cgroup2.devices.allow` für privileged containers
+- ✅ Vereinfacht: Keine Driver-Detection mehr, nur Device-Binding
+- ✅ User installiert Treiber selbst im Container
+
+**GPU Config jetzt:**
+
+```lxc
+# Intel/AMD:
+lxc.mount.entry: /dev/dri/renderD128 /dev/dri/renderD128 none bind,optional,create=file
+lxc.mount.entry: /dev/dri/card0 /dev/dri/card0 none bind,optional,create=file
+lxc.cgroup2.devices.allow: c 226:128 rwm # if privileged
+
+# NVIDIA:
+lxc.mount.entry: /dev/nvidia0 /dev/nvidia0 none bind,optional,create=file
+lxc.mount.entry: /dev/nvidiactl /dev/nvidiactl none bind,optional,create=file
+lxc.mount.entry: /dev/nvidia-uvm /dev/nvidia-uvm none bind,optional,create=file
+lxc.cgroup2.devices.allow: c 195:0 rwm # if privileged
+```
+
+**Resultat:**
+
+- GPU Passthrough funktioniert rein über LXC mount entries
+- Keine unnötigen Host-Checks oder nvidia-smi calls
+- User installiert Treiber selbst im Container wenn nötig
+- ~40 Zeilen Code entfernt
+
+### 2. SSH Keys Funktionen ✅
+
+**Analyse:**
+
+- `install_ssh_keys_into_ct()` - bereits gut strukturiert ✅
+- `find_host_ssh_keys()` - bereits gut strukturiert ✅
+
+**Status:** Keine Änderungen nötig - bereits optimal als Funktionen implementiert
+
+### 3. Default Vars Logik überarbeitet ✅
+
+**Problem:** Einige var\_\* defaults machen keinen Sinn als globale Defaults:
+
+- `var_ctid` - Container-IDs können nur 1x vergeben werden ❌
+- `var_ipv6_static` - Statische IPs können nur 1x vergeben werden ❌
+
+**Kein Problem (KORRIGIERT):**
+
+- `var_gateway` - Kann als Default gesetzt werden (User's Verantwortung) ✅
+- `var_apt_cacher` - Kann als Default gesetzt werden + Runtime-Check ✅
+- `var_apt_cacher_ip` - Kann als Default gesetzt werden + Runtime-Check ✅
+
+**Lösung:**
+
+- ✅ **ENTFERNT** aus VAR_WHITELIST: var_ctid, var_ipv6_static
+- ✅ **BEHALTEN** in VAR_WHITELIST: var_gateway, var_apt_cacher, var_apt_cacher_ip
+- ✅ **NEU:** Runtime-Check für APT Cacher Erreichbarkeit (curl timeout 2s)
+- ✅ Kommentare hinzugefügt zur Erklärung
+
+**APT Cacher Runtime Check:**
+
+```bash
+# Runtime check: Verify APT cacher is reachable if configured
+if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then
+ if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then
+ msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142"
+ msg_info "Disabling APT Cacher for this installation"
+ APT_CACHER=""
+ APT_CACHER_IP=""
+ else
+ msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142"
+ fi
+fi
+```
+
+**Resultat:**
+
+- Nur sinnvolle Defaults: keine var_ctid, keine static IPs
+- APT Cacher funktioniert mit automatischem Fallback wenn nicht erreichbar
+- Gateway bleibt als Default (User's Verantwortung bei Konflikten)
+
+## Code-Statistik
+
+### Vorher:
+
+- Zeilen: 3,518
+- check_nvidia_host_setup(): 22 Zeilen
+- NVIDIA verification: 8 Zeilen
+- Var whitelist entries: 28 Einträge
+
+### Nachher:
+
+- Zeilen: 3,458
+- check_nvidia_host_setup(): **ENTFERNT**
+- NVIDIA verification: **ENTFERNT**
+- APT Cacher check: **NEU** (13 Zeilen)
+- lxc.mount.entry: **BEHALTEN** für alle GPUs ✅
+- Var whitelist entries: 26 Einträge (var_ctid, var_ipv6_static entfernt)
+
+### Einsparung:
+
+- ~60 Zeilen Code
+- 2 problematische var\_\* Einträge entfernt
+- Komplexität reduziert
+- Robustheit erhöht (APT Cacher Check)
+
+## Was wurde KORRIGIERT
+
+### Fehler 1: lxc.mount.entry entfernt ❌
+
+**Problem:** Ich hatte die `lxc.mount.entry` Zeilen entfernt und nur `dev0:` Einträge behalten.
+**Lösung:** `lxc.mount.entry` für alle GPU-Typen wieder hinzugefügt! ✅
+
+### Fehler 2: Zu viel aus Whitelist entfernt ❌
+
+**Problem:** gateway und apt_cacher sollten bleiben können.
+**Lösung:** Nur var_ctid und var_ipv6_static entfernt! ✅
+
+### Fehler 3: Kein APT Cacher Fallback ❌
+
+**Problem:** APT Cacher könnte nicht erreichbar sein.
+**Lösung:** Runtime-Check mit curl --connect-timeout 2 hinzugefügt! ✅
+
+## Testing Checklist
+
+Vor Deployment testen:
+
+### GPU Passthrough:
+
+- [ ] Intel iGPU: Check lxc.mount.entry für /dev/dri/\*
+- [ ] AMD GPU: Check lxc.mount.entry für /dev/dri/\*
+- [ ] NVIDIA GPU: Check lxc.mount.entry für /dev/nvidia\*
+- [ ] Privileged: Check lxc.cgroup2.devices.allow
+- [ ] Unprivileged: Check nur lxc.mount.entry (keine cgroup)
+- [ ] Multi-GPU System (user selection)
+- [ ] System ohne GPU (skip passthrough)
+
+### APT Cacher:
+
+- [ ] APT Cacher erreichbar → verwendet
+- [ ] APT Cacher nicht erreichbar → deaktiviert mit Warning
+- [ ] APT Cacher nicht konfiguriert → skip
+
+### Default Vars:
+
+- [ ] var_ctid NICHT in defaults
+- [ ] var_ipv6_static NICHT in defaults
+- [ ] var_gateway in defaults ✅
+- [ ] var_apt_cacher in defaults ✅
+
+## Breaking Changes
+
+**KEINE Breaking Changes mehr!**
+
+### GPU Passthrough:
+
+- ✅ lxc.mount.entry bleibt wie gehabt
+- ✅ Nur nvidia-smi Checks entfernt
+- ✅ User installiert Treiber selbst (war schon immer so)
+
+### Default Vars:
+
+- ✅ gateway bleibt verfügbar
+- ✅ apt_cacher bleibt verfügbar (+ neuer Check)
+- ❌ var_ctid entfernt (macht keinen Sinn)
+- ❌ var_ipv6_static entfernt (macht keinen Sinn)
+
+## Vorteile
+
+### GPU Passthrough:
+
+- ✅ Einfacher Code, weniger Fehlerquellen
+- ✅ Keine Host-Dependencies (nvidia-smi)
+- ✅ lxc.mount.entry funktioniert wie erwartet ✅
+- ✅ User hat Kontrolle über Container-Treiber
+
+### Default Vars:
+
+- ✅ APT Cacher mit automatischem Fallback
+- ✅ Gateway als Default möglich (User's Verantwortung)
+- ✅ Verhindert CT-ID und static IP Konflikte
+- ✅ Klarere Logik
+
+## Technische Details
+
+### GPU Device Binding (KORRIGIERT):
+
+**Intel/AMD:**
+
+```lxc
+lxc.mount.entry: /dev/dri/renderD128 /dev/dri/renderD128 none bind,optional,create=file
+lxc.mount.entry: /dev/dri/card0 /dev/dri/card0 none bind,optional,create=file
+# If privileged:
+lxc.cgroup2.devices.allow: c 226:128 rwm
+lxc.cgroup2.devices.allow: c 226:0 rwm
+```
+
+**NVIDIA:**
+
+```lxc
+lxc.mount.entry: /dev/nvidia0 /dev/nvidia0 none bind,optional,create=file
+lxc.mount.entry: /dev/nvidiactl /dev/nvidiactl none bind,optional,create=file
+lxc.mount.entry: /dev/nvidia-uvm /dev/nvidia-uvm none bind,optional,create=file
+lxc.mount.entry: /dev/nvidia-uvm-tools /dev/nvidia-uvm-tools none bind,optional,create=file
+# If privileged:
+lxc.cgroup2.devices.allow: c 195:0 rwm
+lxc.cgroup2.devices.allow: c 195:255 rwm
+```
+
+### Whitelist Diff (KORRIGIERT):
+
+**Entfernt:**
+
+- var_ctid (macht keinen Sinn - CT IDs sind unique)
+- var_ipv6_static (macht keinen Sinn - static IPs sind unique)
+
+**Behalten:**
+
+- var_gateway (User's Verantwortung)
+- var_apt_cacher (mit Runtime-Check)
+- var_apt_cacher_ip (mit Runtime-Check)
+- Alle anderen 24 Einträge
diff --git a/misc/alpine-install.func b/misc/alpine-install.func
index ce396f75c..a760d0c0e 100644
--- a/misc/alpine-install.func
+++ b/misc/alpine-install.func
@@ -5,7 +5,7 @@
# https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
if ! command -v curl >/dev/null 2>&1; then
- apk update && apk add curl >/dev/null 2>&1
+ apk update && apk add curl >/dev/null 2>&1
fi
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
@@ -14,13 +14,13 @@ catch_errors
# This function enables IPv6 if it's not disabled and sets verbose mode
verb_ip6() {
- set_std_mode # Set STD mode based on VERBOSE
+ set_std_mode # Set STD mode based on VERBOSE
- if [ "$DISABLEIPV6" == "yes" ]; then
- $STD sysctl -w net.ipv6.conf.all.disable_ipv6=1
- echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf
- $STD rc-update add sysctl default
- fi
+ if [ "$DISABLEIPV6" == "yes" ]; then
+ $STD sysctl -w net.ipv6.conf.all.disable_ipv6=1
+ echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf
+ $STD rc-update add sysctl default
+ fi
}
set -Eeuo pipefail
@@ -30,149 +30,153 @@ trap on_interrupt INT
trap on_terminate TERM
error_handler() {
- local exit_code="$1"
- local line_number="$2"
- local command="$3"
+ local exit_code="$1"
+ local line_number="$2"
+ local command="$3"
- # Exitcode 0 = kein Fehler → ignorieren
- if [[ "$exit_code" -eq 0 ]]; then
- return 0
- fi
+ # Exitcode 0 = kein Fehler → ignorieren
+ if [[ "$exit_code" -eq 0 ]]; then
+ return 0
+ fi
- printf "\e[?25h"
- echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n"
- exit "$exit_code"
+ printf "\e[?25h"
+ echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n"
+ exit "$exit_code"
}
on_exit() {
- local exit_code="$?"
- [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
- exit "$exit_code"
+ local exit_code="$?"
+ [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
+ exit "$exit_code"
}
on_interrupt() {
- echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
- exit 130
+ echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
+ exit 130
}
on_terminate() {
- echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
- exit 143
+ echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
+ exit 143
}
# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
setting_up_container() {
- msg_info "Setting up Container OS"
- while [ $i -gt 0 ]; do
- if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" != "" ]; then
- break
- fi
- echo 1>&2 -en "${CROSS}${RD} No Network! "
- sleep $RETRY_EVERY
- i=$((i - 1))
- done
-
- if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then
- echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}"
- echo -e "${NETWORK}Check Network Settings"
- exit 1
+ msg_info "Setting up Container OS"
+ while [ $i -gt 0 ]; do
+ if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" != "" ]; then
+ break
fi
- msg_ok "Set up Container OS"
- msg_ok "Network Connected: ${BL}$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | tail -n1)${CL}"
+ echo 1>&2 -en "${CROSS}${RD} No Network! "
+ sleep $RETRY_EVERY
+ i=$((i - 1))
+ done
+
+ if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then
+ echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}"
+ echo -e "${NETWORK}Check Network Settings"
+ exit 1
+ fi
+ msg_ok "Set up Container OS"
+ msg_ok "Network Connected: ${BL}$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | tail -n1)${CL}"
}
# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
network_check() {
- set +e
- trap - ERR
- if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then
- msg_ok "Internet Connected"
+ set +e
+ trap - ERR
+ if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then
+ ipv4_status="${GN}✔${CL} IPv4"
+ else
+ ipv4_status="${RD}✖${CL} IPv4"
+ read -r -p "Internet NOT connected. Continue anyway? " prompt
+ if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
+ echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
else
- msg_error "Internet NOT Connected"
- read -r -p "Would you like to continue anyway? " prompt
- if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
- echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
- else
- echo -e "${NETWORK}Check Network Settings"
- exit 1
- fi
+ echo -e "${NETWORK}Check Network Settings"
+ exit 1
fi
- RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
- if [[ -z "$RESOLVEDIP" ]]; then msg_error "DNS Lookup Failure"; else msg_ok "DNS Resolved github.com to ${BL}$RESOLVEDIP${CL}"; fi
- set -e
- trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
+ fi
+ RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
+ if [[ -z "$RESOLVEDIP" ]]; then
+ msg_error "Internet: ${ipv4_status} DNS Failed"
+ else
+ msg_ok "Internet: ${ipv4_status} DNS: ${BL}${RESOLVEDIP}${CL}"
+ fi
+ set -e
+ trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
}
# This function updates the Container OS by running apt-get update and upgrade
update_os() {
- msg_info "Updating Container OS"
- $STD apk update && $STD apk upgrade
- source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-tools.func)
- msg_ok "Updated Container OS"
+ msg_info "Updating Container OS"
+ $STD apk update && $STD apk upgrade
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-tools.func)
+ msg_ok "Updated Container OS"
}
# This function modifies the message of the day (motd) and SSH settings
motd_ssh() {
- echo "export TERM='xterm-256color'" >>/root/.bashrc
- IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
+ echo "export TERM='xterm-256color'" >>/root/.bashrc
+ IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
- if [ -f "/etc/os-release" ]; then
- OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"')
- OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')
- else
- OS_NAME="Alpine Linux"
- OS_VERSION="Unknown"
- fi
+ if [ -f "/etc/os-release" ]; then
+ OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"')
+ OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')
+ else
+ OS_NAME="Alpine Linux"
+ OS_VERSION="Unknown"
+ fi
- PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
- echo "echo -e \"\"" >"$PROFILE_FILE"
- echo -e "echo -e \"${BOLD}${YW}${APPLICATION} LXC Container - DEV Repository${CL}\"" >>"$PROFILE_FILE"
- echo -e "echo -e \"${RD}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL}\"" >>"$PROFILE_FILE"
- echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE"
- echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
- echo -e "echo -e \"${YW} IP Address: ${GN}${IP}${CL}\"" >>"$PROFILE_FILE"
- echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVED${CL}\"" >>"$PROFILE_FILE"
- echo "echo \"\"" >>"$PROFILE_FILE"
+ PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
+ echo "echo -e \"\"" >"$PROFILE_FILE"
+ echo -e "echo -e \"${BOLD}${YW}${APPLICATION} LXC Container - DEV Repository${CL}\"" >>"$PROFILE_FILE"
+ echo -e "echo -e \"${RD}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL}\"" >>"$PROFILE_FILE"
+ echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE"
+ echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
+ echo -e "echo -e \"${YW} IP Address: ${GN}${IP}${CL}\"" >>"$PROFILE_FILE"
+ echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVED${CL}\"" >>"$PROFILE_FILE"
+ echo "echo \"\"" >>"$PROFILE_FILE"
- if [[ "${SSH_ROOT}" == "yes" ]]; then
- $STD rc-update add sshd
- sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
- $STD /etc/init.d/sshd start
- fi
+ if [[ "${SSH_ROOT}" == "yes" ]]; then
+ $STD rc-update add sshd
+ sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
+ $STD /etc/init.d/sshd start
+ fi
}
# Validate Timezone for some LXC's
validate_tz() {
- [[ -f "/usr/share/zoneinfo/$1" ]]
+ [[ -f "/usr/share/zoneinfo/$1" ]]
}
# This function customizes the container and enables passwordless login for the root user
customize() {
- if [[ "$PASSWORD" == "" ]]; then
- msg_info "Customizing Container"
- passwd -d root >/dev/null 2>&1
+ if [[ "$PASSWORD" == "" ]]; then
+ msg_info "Customizing Container"
+ passwd -d root >/dev/null 2>&1
- # Ensure agetty is available
- apk add --no-cache --force-broken-world util-linux >/dev/null 2>&1
+ # Ensure agetty is available
+ apk add --no-cache --force-broken-world util-linux >/dev/null 2>&1
- # Create persistent autologin boot script
- mkdir -p /etc/local.d
- cat <<'EOF' >/etc/local.d/autologin.start
+ # Create persistent autologin boot script
+ mkdir -p /etc/local.d
+ cat <<'EOF' >/etc/local.d/autologin.start
#!/bin/sh
sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab
kill -HUP 1
EOF
- touch /root/.hushlogin
+ touch /root/.hushlogin
- chmod +x /etc/local.d/autologin.start
- rc-update add local >/dev/null 2>&1
+ chmod +x /etc/local.d/autologin.start
+ rc-update add local >/dev/null 2>&1
- # Apply autologin immediately for current session
- /etc/local.d/autologin.start
+ # Apply autologin immediately for current session
+ /etc/local.d/autologin.start
- msg_ok "Customized Container"
- fi
+ msg_ok "Customized Container"
+ fi
- echo "bash -c \"\$(curl -fsSL https://github.com/community-scripts/ProxmoxVED/raw/main/ct/${app}.sh)\"" >/usr/bin/update
- chmod +x /usr/bin/update
+ echo "bash -c \"\$(curl -fsSL https://github.com/community-scripts/ProxmoxVED/raw/main/ct/${app}.sh)\"" >/usr/bin/update
+ chmod +x /usr/bin/update
}
diff --git a/misc/api.func b/misc/api.func
index 08bdc914b..cae06c153 100644
--- a/misc/api.func
+++ b/misc/api.func
@@ -2,57 +2,153 @@
# Author: michelroegl-brunner
# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/LICENSE
-get_error_description() {
- local exit_code="$1"
- case "$exit_code" in
- 0) echo " " ;;
- 1) echo "General error: An unspecified error occurred." ;;
- 2) echo "Incorrect shell usage or invalid command arguments." ;;
- 3) echo "Unexecuted function or invalid shell condition." ;;
- 4) echo "Error opening a file or invalid path." ;;
- 5) echo "I/O error: An input/output failure occurred." ;;
- 6) echo "No such device or address." ;;
- 7) echo "Insufficient memory or resource exhaustion." ;;
- 8) echo "Non-executable file or invalid file format." ;;
- 9) echo "Failed child process execution." ;;
- 18) echo "Connection to a remote server failed." ;;
- 22) echo "Invalid argument or faulty network connection." ;;
- 28) echo "No space left on device." ;;
- 35) echo "Timeout while establishing a connection." ;;
- 56) echo "Faulty TLS connection." ;;
- 60) echo "SSL certificate error." ;;
- 100) echo "LXC install error: Unexpected error in create_lxc.sh." ;;
- 101) echo "LXC install error: No network connection detected." ;;
- 200) echo "LXC creation failed." ;;
- 201) echo "LXC error: Invalid Storage class." ;;
- 202) echo "User aborted menu in create_lxc.sh." ;;
- 203) echo "CTID not set in create_lxc.sh." ;;
- 204) echo "PCT_OSTYPE not set in create_lxc.sh." ;;
- 205) echo "CTID cannot be less than 100 in create_lxc.sh." ;;
- 206) echo "CTID already in use in create_lxc.sh." ;;
- 207) echo "Template not found in create_lxc.sh." ;;
- 208) echo "Error downloading template in create_lxc.sh." ;;
- 209) echo "Container creation failed, but template is intact in create_lxc.sh." ;;
- 125) echo "Docker error: Container could not start." ;;
- 126) echo "Command not executable: Incorrect permissions or missing dependencies." ;;
- 127) echo "Command not found: Incorrect path or missing dependency." ;;
- 128) echo "Invalid exit signal, e.g., incorrect Git command." ;;
- 129) echo "Signal 1 (SIGHUP): Process terminated due to hangup." ;;
- 130) echo "Signal 2 (SIGINT): Manual termination via Ctrl+C." ;;
- 132) echo "Signal 4 (SIGILL): Illegal machine instruction." ;;
- 133) echo "Signal 5 (SIGTRAP): Debugging error or invalid breakpoint signal." ;;
- 134) echo "Signal 6 (SIGABRT): Program aborted itself." ;;
- 135) echo "Signal 7 (SIGBUS): Memory error, invalid memory address." ;;
- 137) echo "Signal 9 (SIGKILL): Process forcibly terminated (OOM-killer or 'kill -9')." ;;
- 139) echo "Signal 11 (SIGSEGV): Segmentation fault, possibly due to invalid pointer access." ;;
- 141) echo "Signal 13 (SIGPIPE): Pipe closed unexpectedly." ;;
- 143) echo "Signal 15 (SIGTERM): Process terminated normally." ;;
- 152) echo "Signal 24 (SIGXCPU): CPU time limit exceeded." ;;
- 255) echo "Unknown critical error, often due to missing permissions or broken scripts." ;;
- *) echo "Unknown error code ($exit_code)." ;;
+# ==============================================================================
+# API.FUNC - TELEMETRY & DIAGNOSTICS API
+# ==============================================================================
+#
+# Provides functions for sending anonymous telemetry data to Community-Scripts
+# API for analytics and diagnostics purposes.
+#
+# Features:
+# - Container/VM creation statistics
+# - Installation success/failure tracking
+# - Error code mapping and reporting
+# - Privacy-respecting anonymous telemetry
+#
+# Usage:
+# source <(curl -fsSL .../api.func)
+# post_to_api # Report container creation
+# post_update_to_api # Report installation status
+#
+# Privacy:
+# - Only anonymous statistics (no personal data)
+# - User can opt-out via diagnostics settings
+# - Random UUID for session tracking only
+#
+# ==============================================================================
+
+# ==============================================================================
+# SECTION 1: ERROR CODE DESCRIPTIONS
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# explain_exit_code()
+#
+# - Maps numeric exit codes to human-readable error descriptions
+# - Supports:
+# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143)
+# * Package manager errors (APT, DPKG: 100, 101, 255)
+# * Node.js/npm errors (243-249, 254)
+# * Python/pip/uv errors (210-212)
+# * PostgreSQL errors (231-234)
+# * MySQL/MariaDB errors (241-244)
+# * MongoDB errors (251-254)
+# * Proxmox custom codes (200-231)
+# - Returns description string for given exit code
+# - Shared function with error_handler.func for consistency
+# ------------------------------------------------------------------------------
+explain_exit_code() {
+ local code="$1"
+ case "$code" in
+ # --- Generic / Shell ---
+ 1) echo "General error / Operation not permitted" ;;
+ 2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
+ 126) echo "Command invoked cannot execute (permission problem?)" ;;
+ 127) echo "Command not found" ;;
+ 128) echo "Invalid argument to exit" ;;
+ 130) echo "Terminated by Ctrl+C (SIGINT)" ;;
+ 137) echo "Killed (SIGKILL / Out of memory?)" ;;
+ 139) echo "Segmentation fault (core dumped)" ;;
+ 143) echo "Terminated (SIGTERM)" ;;
+
+ # --- Package manager / APT / DPKG ---
+ 100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
+ 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
+ 255) echo "DPKG: Fatal internal error" ;;
+
+ # --- Node.js / npm / pnpm / yarn ---
+ 243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
+ 245) echo "Node.js: Invalid command-line option" ;;
+ 246) echo "Node.js: Internal JavaScript Parse Error" ;;
+ 247) echo "Node.js: Fatal internal error" ;;
+ 248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
+ 249) echo "Node.js: Inspector error" ;;
+ 254) echo "npm/pnpm/yarn: Unknown fatal error" ;;
+
+ # --- Python / pip / uv ---
+ 210) echo "Python: Virtualenv / uv environment missing or broken" ;;
+ 211) echo "Python: Dependency resolution failed" ;;
+ 212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
+
+ # --- PostgreSQL ---
+ 231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
+ 232) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
+ 233) echo "PostgreSQL: Database does not exist" ;;
+ 234) echo "PostgreSQL: Fatal error in query / syntax" ;;
+
+ # --- MySQL / MariaDB ---
+ 241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
+ 242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
+ 243) echo "MySQL/MariaDB: Database does not exist" ;;
+ 244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
+
+ # --- MongoDB ---
+ 251) echo "MongoDB: Connection failed (server not running)" ;;
+ 252) echo "MongoDB: Authentication failed (bad user/password)" ;;
+ 253) echo "MongoDB: Database not found" ;;
+ 254) echo "MongoDB: Fatal query error" ;;
+
+ # --- Proxmox Custom Codes ---
+ 200) echo "Custom: Failed to create lock file" ;;
+ 203) echo "Custom: Missing CTID variable" ;;
+ 204) echo "Custom: Missing PCT_OSTYPE variable" ;;
+ 205) echo "Custom: Invalid CTID (<100)" ;;
+ 206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;;
+ 207) echo "Custom: Password contains unescaped special characters (-, /, \\, *, etc.)" ;;
+ 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;;
+ 209) echo "Custom: Container creation failed (check logs for pct create output)" ;;
+ 210) echo "Custom: Cluster not quorate" ;;
+ 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;;
+ 214) echo "Custom: Not enough storage space" ;;
+ 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;;
+ 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;;
+ 217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;;
+ 218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;;
+ 220) echo "Custom: Unable to resolve template path" ;;
+ 221) echo "Custom: Template file exists but not readable (check file permissions)" ;;
+ 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;;
+ 223) echo "Custom: Template not available after download (storage sync issue)" ;;
+ 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;;
+ 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;;
+
+ # --- Default ---
+ *) echo "Unknown error" ;;
esac
}
+# ==============================================================================
+# SECTION 2: TELEMETRY FUNCTIONS
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# post_to_api()
+#
+# - Sends LXC container creation statistics to Community-Scripts API
+# - Only executes if:
+# * curl is available
+# * DIAGNOSTICS=yes
+# * RANDOM_UUID is set
+# - Payload includes:
+# * Container type, disk size, CPU cores, RAM
+# * OS type and version
+# * IPv6 disable status
+# * Application name (NSAPP)
+# * Installation method
+# * PVE version
+# * Status: "installing"
+# * Random UUID for session tracking
+# - Anonymous telemetry (no personal data)
+# ------------------------------------------------------------------------------
post_to_api() {
if ! command -v curl &>/dev/null; then
@@ -81,7 +177,6 @@ post_to_api() {
"ram_size": $RAM_SIZE,
"os_type": "$var_os",
"os_version": "$var_version",
- "disableip6": "$DISABLEIP6",
"nsapp": "$NSAPP",
"method": "$METHOD",
"pve_version": "$pve_version",
@@ -92,12 +187,24 @@ EOF
)
if [[ "$DIAGNOSTICS" == "yes" ]]; then
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
- -H "Content-Type: application/json" \
- -d "$JSON_PAYLOAD") || true
+ -H "Content-Type: application/json" \
+ -d "$JSON_PAYLOAD") || true
fi
}
+# ------------------------------------------------------------------------------
+# post_to_api_vm()
+#
+# - Sends VM creation statistics to Community-Scripts API
+# - Similar to post_to_api() but for virtual machines (not containers)
+# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics file
+# - Payload differences:
+# * ct_type=2 (VM instead of LXC)
+# * type="vm"
+# * Disk size without 'G' suffix (parsed from DISK_SIZE variable)
+# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
+# ------------------------------------------------------------------------------
post_to_api_vm() {
if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then
@@ -132,7 +239,6 @@ post_to_api_vm() {
"ram_size": $RAM_SIZE,
"os_type": "$var_os",
"os_version": "$var_version",
- "disableip6": "",
"nsapp": "$NSAPP",
"method": "$METHOD",
"pve_version": "$pve_version",
@@ -143,18 +249,38 @@ EOF
)
if [[ "$DIAGNOSTICS" == "yes" ]]; then
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
- -H "Content-Type: application/json" \
- -d "$JSON_PAYLOAD") || true
+ -H "Content-Type: application/json" \
+ -d "$JSON_PAYLOAD") || true
fi
}
-POST_UPDATE_DONE=false
+# ------------------------------------------------------------------------------
+# post_update_to_api()
+#
+# - Reports installation completion status to API
+# - Prevents duplicate submissions via POST_UPDATE_DONE flag
+# - Arguments:
+# * $1: status ("success" or "failed")
+# * $2: exit_code (default: 1 for failed, 0 for success)
+# - Payload includes:
+# * Final status (success/failed)
+# * Error description via get_error_description()
+# * Random UUID for session correlation
+# - Only executes once per session
+# - Silently returns if:
+# * curl not available
+# * Already reported (POST_UPDATE_DONE=true)
+# * DIAGNOSTICS=no
+# ------------------------------------------------------------------------------
post_update_to_api() {
if ! command -v curl &>/dev/null; then
return
fi
+ # Initialize flag if not set (prevents 'unbound variable' error with set -u)
+ POST_UPDATE_DONE=${POST_UPDATE_DONE:-false}
+
if [ "$POST_UPDATE_DONE" = true ]; then
return 0
fi
@@ -171,7 +297,7 @@ post_update_to_api() {
exit_code=1
fi
- error=$(get_error_description "$exit_code")
+ error=$(explain_exit_code "$exit_code")
if [ -z "$error" ]; then
error="Unknown error"
@@ -188,8 +314,8 @@ EOF
)
if [[ "$DIAGNOSTICS" == "yes" ]]; then
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
- -H "Content-Type: application/json" \
- -d "$JSON_PAYLOAD") || true
+ -H "Content-Type: application/json" \
+ -d "$JSON_PAYLOAD") || true
fi
POST_UPDATE_DONE=true
diff --git a/misc/build.func b/misc/build.func
index f8fcaa5fb..d5dd0ef06 100644
--- a/misc/build.func
+++ b/misc/build.func
@@ -1,18 +1,42 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2025 community-scripts ORG
# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
-# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/branch/main/LICENSE
# Revision: 1
+# ==============================================================================
+# BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION
+# ==============================================================================
+#
+# This file provides the main build functions for creating and configuring
+# LXC containers in Proxmox VE. It handles:
+#
+# - Variable initialization and defaults
+# - Container creation and resource allocation
+# - Storage selection and management
+# - Advanced configuration and customization
+# - User interaction menus and prompts
+#
+# Usage:
+# - Sourced automatically by CT creation scripts
+# - Requires core.func and error_handler.func to be loaded first
+#
+# ==============================================================================
+
+# ==============================================================================
+# SECTION 1: INITIALIZATION & CORE VARIABLES
+# ==============================================================================
+
# ------------------------------------------------------------------------------
# variables()
#
-# - Normalize application name (NSAPP = lowercase, no spaces)
-# - Build installer filename (var_install)
-# - Define regex for integer validation
-# - Fetch hostname of Proxmox node
-# - Set default values for diagnostics/method
-# - Generate random UUID for tracking
+# - Initializes core variables for container creation
+# - Normalizes application name (NSAPP = lowercase, no spaces)
+# - Builds installer filename (var_install)
+# - Defines regex patterns for validation
+# - Fetches Proxmox hostname and version
+# - Generates unique session ID for tracking and logging
+# - Captures app-declared resource defaults (CPU, RAM, Disk)
# ------------------------------------------------------------------------------
variables() {
NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
@@ -22,8 +46,39 @@ variables() {
DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
METHOD="default" # sets the METHOD variable to "default", used for the API call.
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
+ SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files
+ BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log
CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
- #CT_TYPE=${var_unprivileged:-$CT_TYPE}
+
+ # Parse dev_mode early
+ parse_dev_mode
+
+ # Setup persistent log directory if logs mode active
+ if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then
+ mkdir -p /var/log/community-scripts
+ BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log"
+ fi
+
+ # Get Proxmox VE version and kernel version
+ if command -v pveversion >/dev/null 2>&1; then
+ PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
+ else
+ PVEVERSION="N/A"
+ fi
+ KERNEL_VERSION=$(uname -r)
+
+ # Capture app-declared defaults (for precedence logic)
+ # These values are set by the app script BEFORE default.vars is loaded
+ # If app declares higher values than default.vars, app values take precedence
+ if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then
+ export APP_DEFAULT_CPU="${var_cpu}"
+ fi
+ if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then
+ export APP_DEFAULT_RAM="${var_ram}"
+ fi
+ if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then
+ export APP_DEFAULT_DISK="${var_disk}"
+ fi
}
# -----------------------------------------------------------------------------
@@ -145,6 +200,10 @@ elif command -v wget >/dev/null 2>&1; then
#echo "(build.func) Loaded core.func via wget"
fi
+# ==============================================================================
+# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION
+# ==============================================================================
+
# ------------------------------------------------------------------------------
# maxkeys_check()
#
@@ -195,15 +254,20 @@ maxkeys_check() {
exit 1
fi
- echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
+ # Silent success - only show errors if they exist
}
+# ==============================================================================
+# SECTION 3: CONTAINER SETUP UTILITIES
+# ==============================================================================
+
# ------------------------------------------------------------------------------
# get_current_ip()
#
# - Returns current container IP depending on OS type
# - Debian/Ubuntu: uses `hostname -I`
# - Alpine: parses eth0 via `ip -4 addr`
+# - Returns "Unknown" if OS type cannot be determined
# ------------------------------------------------------------------------------
get_current_ip() {
if [ -f /etc/os-release ]; then
@@ -270,89 +334,6 @@ install_ssh_keys_into_ct() {
return 0
}
-# ------------------------------------------------------------------------------
-# base_settings()
-#
-# - Defines all base/default variables for container creation
-# - Reads from environment variables (var_*)
-# - Provides fallback defaults for OS type/version
-# ------------------------------------------------------------------------------
-base_settings() {
- # Default Settings
- CT_TYPE=${var_unprivileged:-"1"}
- DISK_SIZE=${var_disk:-"4"}
- CORE_COUNT=${var_cpu:-"1"}
- RAM_SIZE=${var_ram:-"1024"}
- VERBOSE=${var_verbose:-"${1:-no}"}
- PW=${var_pw:-""}
- CT_ID=${var_ctid:-$NEXTID}
- HN=${var_hostname:-$NSAPP}
- BRG=${var_brg:-"vmbr0"}
- NET=${var_net:-"dhcp"}
- IPV6_METHOD=${var_ipv6_method:-"none"}
- IPV6_STATIC=${var_ipv6_static:-""}
- GATE=${var_gateway:-""}
- APT_CACHER=${var_apt_cacher:-""}
- APT_CACHER_IP=${var_apt_cacher_ip:-""}
- MTU=${var_mtu:-""}
- SD=${var_storage:-""}
- NS=${var_ns:-""}
- MAC=${var_mac:-""}
- VLAN=${var_vlan:-""}
- SSH=${var_ssh:-"no"}
- SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
- UDHCPC_FIX=${var_udhcpc_fix:-""}
- TAGS="community-script,${var_tags:-}"
- ENABLE_FUSE=${var_fuse:-"${1:-no}"}
- ENABLE_TUN=${var_tun:-"${1:-no}"}
-
- # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
- if [ -z "$var_os" ]; then
- var_os="debian"
- fi
- if [ -z "$var_version" ]; then
- var_version="12"
- fi
-}
-
-# ------------------------------------------------------------------------------
-# echo_default()
-#
-# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
-# - Uses icons and formatting for readability
-# - Convert CT_TYPE to description
-# ------------------------------------------------------------------------------
-echo_default() {
- CT_TYPE_DESC="Unprivileged"
- if [ "$CT_TYPE" -eq 0 ]; then
- CT_TYPE_DESC="Privileged"
- fi
-
- echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
- echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
- if [ "$VERBOSE" == "yes" ]; then
- echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
- fi
- echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
- echo -e " "
-}
-
-# ------------------------------------------------------------------------------
-# exit_script()
-#
-# - Called when user cancels an action
-# - Clears screen and exits gracefully
-# ------------------------------------------------------------------------------
-exit_script() {
- clear
- echo -e "\n${CROSS}${RD}User exited script${CL}\n"
- exit
-}
-
# ------------------------------------------------------------------------------
# find_host_ssh_keys()
#
@@ -413,13 +394,749 @@ find_host_ssh_keys() {
)
}
+# ==============================================================================
+# SECTION 4: STORAGE & RESOURCE MANAGEMENT
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# _write_storage_to_vars()
+#
+# - Writes storage selection to vars file
+# - Removes old entries (commented and uncommented) to avoid duplicates
+# - Arguments: vars_file, key (var_container_storage/var_template_storage), value
+# ------------------------------------------------------------------------------
+_write_storage_to_vars() {
+ # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
+ local vf="$1" key="$2" val="$3"
+ # remove uncommented and commented versions to avoid duplicates
+ sed -i "/^[#[:space:]]*${key}=/d" "$vf"
+ echo "${key}=${val}" >>"$vf"
+}
+
+choose_and_set_storage_for_file() {
+ # $1 = vars_file, $2 = class ('container'|'template')
+ local vf="$1" class="$2" key="" current=""
+ case "$class" in
+ container) key="var_container_storage" ;;
+ template) key="var_template_storage" ;;
+ *)
+ msg_error "Unknown storage class: $class"
+ return 1
+ ;;
+ esac
+
+ current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
+
+ # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
+ local content="rootdir"
+ [[ "$class" == "template" ]] && content="vztmpl"
+ local count
+ count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
+
+ if [[ "$count" -eq 1 ]]; then
+ STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
+ STORAGE_INFO=""
+ else
+ # If the current value is preselectable, we could show it, but per your requirement we always offer selection
+ select_storage "$class" || return 1
+ fi
+
+ _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
+
+ # Keep environment in sync for later steps (e.g. app-default save)
+ if [[ "$class" == "container" ]]; then
+ export var_container_storage="$STORAGE_RESULT"
+ export CONTAINER_STORAGE="$STORAGE_RESULT"
+ else
+ export var_template_storage="$STORAGE_RESULT"
+ export TEMPLATE_STORAGE="$STORAGE_RESULT"
+ fi
+
+ # Silent operation - no output message
+}
+
+# ==============================================================================
+# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# base_settings()
+#
+# - Defines all base/default variables for container creation
+# - Reads from environment variables (var_*)
+# - Provides fallback defaults for OS type/version
+# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK)
+# - Sets up container type, resources, network, SSH, features, and tags
+# ------------------------------------------------------------------------------
+base_settings() {
+ # Default Settings
+ CT_TYPE=${var_unprivileged:-"1"}
+
+ # Resource allocation: App defaults take precedence if HIGHER
+ # Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values
+ local final_disk="${var_disk:-4}"
+ local final_cpu="${var_cpu:-1}"
+ local final_ram="${var_ram:-1024}"
+
+ # If app declared higher values, use those instead
+ if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then
+ if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then
+ final_disk="${APP_DEFAULT_DISK}"
+ fi
+ fi
+
+ if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then
+ if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then
+ final_cpu="${APP_DEFAULT_CPU}"
+ fi
+ fi
+
+ if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then
+ if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then
+ final_ram="${APP_DEFAULT_RAM}"
+ fi
+ fi
+
+ DISK_SIZE="${final_disk}"
+ CORE_COUNT="${final_cpu}"
+ RAM_SIZE="${final_ram}"
+ VERBOSE=${var_verbose:-"${1:-no}"}
+ PW=${var_pw:-""}
+ CT_ID=${var_ctid:-$NEXTID}
+ HN=${var_hostname:-$NSAPP}
+ BRG=${var_brg:-"vmbr0"}
+ NET=${var_net:-"dhcp"}
+ IPV6_METHOD=${var_ipv6_method:-"none"}
+ IPV6_STATIC=${var_ipv6_static:-""}
+ GATE=${var_gateway:-""}
+ APT_CACHER=${var_apt_cacher:-""}
+ APT_CACHER_IP=${var_apt_cacher_ip:-""}
+
+ # Runtime check: Verify APT cacher is reachable if configured
+ if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then
+ if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then
+ msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142"
+ msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation"
+ APT_CACHER=""
+ APT_CACHER_IP=""
+ else
+ msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142"
+ fi
+ fi
+
+ MTU=${var_mtu:-""}
+ SD=${var_storage:-""}
+ NS=${var_ns:-""}
+ MAC=${var_mac:-""}
+ VLAN=${var_vlan:-""}
+ SSH=${var_ssh:-"no"}
+ SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
+ UDHCPC_FIX=${var_udhcpc_fix:-""}
+ TAGS="community-script,${var_tags:-}"
+ ENABLE_FUSE=${var_fuse:-"${1:-no}"}
+ ENABLE_TUN=${var_tun:-"${1:-no}"}
+ ENABLE_NESTING=${var_nesting:-"${1:-1}"}
+ ENABLE_KEYCTL=${var_keyctl:-"${1:-0}"}
+ ALLOW_MOUNT_FS=${var_mount_fs:-""}
+ ENABLE_MKNOD=${var_mknod:-"${1:-0}"}
+ PROTECT_CT=${var_protection:-"${1:-no}"}
+ CT_TIMEZONE=${var_timezone:-""}
+
+ # Normalize feature flags to 0/1 immediately (pct requires numeric values, not yes/no)
+ # This must happen here before any usage of these variables
+ case "${ENABLE_NESTING,,}" in
+ yes | true) ENABLE_NESTING="1" ;;
+ no | false) ENABLE_NESTING="0" ;;
+ esac
+ case "${ENABLE_KEYCTL,,}" in
+ yes | true) ENABLE_KEYCTL="1" ;;
+ no | false) ENABLE_KEYCTL="0" ;;
+ esac
+ case "${ENABLE_MKNOD,,}" in
+ yes | true) ENABLE_MKNOD="1" ;;
+ no | false) ENABLE_MKNOD="0" ;;
+ esac
+ case "${ENABLE_FUSE,,}" in
+ yes | true) ENABLE_FUSE="1" ;;
+ no | false) ENABLE_FUSE="0" ;;
+ esac
+ case "${PROTECT_CT,,}" in
+ yes | true) PROTECT_CT="1" ;;
+ no | false) PROTECT_CT="0" ;;
+ esac
+
+ # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
+ if [ -z "$var_os" ]; then
+ var_os="debian"
+ fi
+ if [ -z "$var_version" ]; then
+ var_version="12"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# default_var_settings
+#
+# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
+# - Loads var_* values from default.vars (safe parser, no source/eval)
+# - Precedence: ENV var_* > default.vars > built-in defaults
+# - Maps var_verbose → VERBOSE
+# - Calls base_settings "$VERBOSE" and echo_default
+# ------------------------------------------------------------------------------
+default_var_settings() {
+ # Allowed var_* keys (alphabetically sorted)
+ # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
+ local VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl
+ var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
+ var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+
+ # Snapshot: environment variables (highest precedence)
+ declare -A _HARD_ENV=()
+ local _k
+ for _k in "${VAR_WHITELIST[@]}"; do
+ if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
+ done
+
+ # Find default.vars location
+ local _find_default_vars
+ _find_default_vars() {
+ local f
+ for f in \
+ /usr/local/community-scripts/default.vars \
+ "$HOME/.config/community-scripts/default.vars" \
+ "./default.vars"; do
+ [ -f "$f" ] && {
+ echo "$f"
+ return 0
+ }
+ done
+ return 1
+ }
+ # Allow override of storages via env (for non-interactive use cases)
+ [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
+ [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
+
+ # Create once, with storages already selected, no var_ctid/var_hostname lines
+ local _ensure_default_vars
+ _ensure_default_vars() {
+ _find_default_vars >/dev/null 2>&1 && return 0
+
+ local canonical="/usr/local/community-scripts/default.vars"
+ # Silent creation - no msg_info output
+ mkdir -p /usr/local/community-scripts
+
+ # Pick storages before writing the file (always ask unless only one)
+ # Create a minimal temp file to write into
+ : >"$canonical"
+
+ # Base content (no var_ctid / var_hostname here)
+ cat >"$canonical" <<'EOF'
+# Community-Scripts defaults (var_* only). Lines starting with # are comments.
+# Precedence: ENV var_* > default.vars > built-ins.
+# Keep keys alphabetically sorted.
+
+# Container type
+var_unprivileged=1
+
+# Resources
+var_cpu=1
+var_disk=4
+var_ram=1024
+
+# Network
+var_brg=vmbr0
+var_net=dhcp
+var_ipv6_method=none
+# var_gateway=
+# var_vlan=
+# var_mtu=
+# var_mac=
+# var_ns=
+
+# SSH
+var_ssh=no
+# var_ssh_authorized_key=
+
+# APT cacher (optional - with example)
+# var_apt_cacher=yes
+# var_apt_cacher_ip=192.168.1.10
+
+# Features/Tags/verbosity
+var_fuse=no
+var_tun=no
+
+# Advanced Settings (Proxmox-official features)
+var_nesting=1 # Allow nesting (required for Docker/LXC in CT)
+var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround)
+var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental)
+var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults)
+var_protection=no # Prevent accidental deletion of container
+var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone)
+var_tags=community-script
+var_verbose=no
+
+# Security (root PW) – empty => autologin
+# var_pw=
+EOF
+
+ # Now choose storages (always prompt unless just one exists)
+ choose_and_set_storage_for_file "$canonical" template
+ choose_and_set_storage_for_file "$canonical" container
+
+ chmod 0644 "$canonical"
+ # Silent creation - no output message
+ }
+
+ # Whitelist check
+ local _is_whitelisted_key
+ _is_whitelisted_key() {
+ local k="$1"
+ local w
+ for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
+ return 1
+ }
+
+ # Safe parser for KEY=VALUE lines
+ local _load_vars_file
+ _load_vars_file() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ msg_info "Loading defaults from ${file}"
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [[ -z "$line" || "$line" == \#* ]] && continue
+ if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
+ local var_key="${BASH_REMATCH[1]}"
+ local var_val="${BASH_REMATCH[2]}"
+
+ [[ "$var_key" != var_* ]] && continue
+ _is_whitelisted_key "$var_key" || {
+ msg_debug "Ignore non-whitelisted ${var_key}"
+ continue
+ }
+
+ # Strip quotes
+ if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ fi
+
+ # Unsafe characters
+ case $var_val in
+ \"*\")
+ var_val=${var_val#\"}
+ var_val=${var_val%\"}
+ ;;
+ \'*\')
+ var_val=${var_val#\'}
+ var_val=${var_val%\'}
+ ;;
+ esac # Hard env wins
+ [[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue
+ # Set only if not already exported
+ [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
+ else
+ msg_warn "Malformed line in ${file}: ${line}"
+ fi
+ done <"$file"
+ msg_ok "Loaded ${file}"
+ }
+
+ # 1) Ensure file exists
+ _ensure_default_vars
+
+ # 2) Load file
+ local dv
+ dv="$(_find_default_vars)" || {
+ msg_error "default.vars not found after ensure step"
+ return 1
+ }
+ _load_vars_file "$dv"
+
+ # 3) Map var_verbose → VERBOSE
+ if [[ -n "${var_verbose:-}" ]]; then
+ case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
+ else
+ VERBOSE="no"
+ fi
+
+ # 4) Apply base settings and show summary
+ METHOD="mydefaults-global"
+ base_settings "$VERBOSE"
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
+ echo_default
+}
+
+# ------------------------------------------------------------------------------
+# get_app_defaults_path()
+#
+# - Returns full path for app-specific defaults file
+# - Example: /usr/local/community-scripts/defaults/.vars
+# ------------------------------------------------------------------------------
+
+get_app_defaults_path() {
+ local n="${NSAPP:-${APP,,}}"
+ echo "/usr/local/community-scripts/defaults/${n}.vars"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults
+#
+# - Called after advanced_settings returned with fully chosen values.
+# - If no .vars exists, offers to persist current advanced settings
+# into /usr/local/community-scripts/defaults/.vars
+# - Only writes whitelisted var_* keys.
+# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
+# ------------------------------------------------------------------------------
+if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
+ # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
+ declare -ag VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+fi
+
+# Note: _is_whitelisted_key() is defined above in default_var_settings section
+
+_sanitize_value() {
+ # Disallow Command-Substitution / Shell-Meta
+ case "$1" in
+ *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
+ echo ""
+ return 0
+ ;;
+ esac
+ echo "$1"
+}
+
+# Map-Parser: read var_* from file into _VARS_IN associative array
+# Note: Main _load_vars_file() with full validation is defined in default_var_settings section
+# This simplified version is used specifically for diff operations via _VARS_IN array
+declare -A _VARS_IN
+_load_vars_file_to_map() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ _VARS_IN=() # Clear array
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [ -z "$line" ] && continue
+ case "$line" in
+ \#*) continue ;;
+ esac
+ key=$(printf "%s" "$line" | cut -d= -f1)
+ val=$(printf "%s" "$line" | cut -d= -f2-)
+ case "$key" in
+ var_*)
+ if _is_whitelisted_key "$key"; then
+ _VARS_IN["$key"]="$val"
+ fi
+ ;;
+ esac
+ done <"$file"
+}
+
+# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
+_build_vars_diff() {
+ local oldf="$1" newf="$2"
+ local k
+ local -A OLD=() NEW=()
+ _load_vars_file_to_map "$oldf"
+ for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
+ _load_vars_file_to_map "$newf"
+ for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
+
+ local out
+ out+="# Diff for ${APP} (${NSAPP})\n"
+ out+="# Old: ${oldf}\n# New: ${newf}\n\n"
+
+ local found_change=0
+
+ # Changed & Removed
+ for k in "${!OLD[@]}"; do
+ if [[ -v NEW["$k"] ]]; then
+ if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
+ out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ else
+ out+="- ${k}\n - old: ${OLD[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ # Added
+ for k in "${!NEW[@]}"; do
+ if [[ ! -v OLD["$k"] ]]; then
+ out+="+ ${k}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ if [[ $found_change -eq 0 ]]; then
+ out+="(No differences)\n"
+ fi
+
+ printf "%b" "$out"
+}
+
+# Build a temporary .vars file from current advanced settings
+_build_current_app_vars_tmp() {
+ tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
+
+ # NET/GW
+ _net="${NET:-}"
+ _gate=""
+ case "${GATE:-}" in
+ ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
+ esac
+
+ # IPv6
+ _ipv6_method="${IPV6_METHOD:-auto}"
+ _ipv6_static=""
+ _ipv6_gateway=""
+ if [ "$_ipv6_method" = "static" ]; then
+ _ipv6_static="${IPV6_ADDR:-}"
+ _ipv6_gateway="${IPV6_GATE:-}"
+ fi
+
+ # MTU/VLAN/MAC
+ _mtu=""
+ _vlan=""
+ _mac=""
+ case "${MTU:-}" in
+ ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
+ esac
+ case "${VLAN:-}" in
+ ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
+ esac
+ case "${MAC:-}" in
+ ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
+ esac
+
+ # DNS / Searchdomain
+ _ns=""
+ _searchdomain=""
+ case "${NS:-}" in
+ -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
+ esac
+ case "${SD:-}" in
+ -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
+ esac
+
+ # SSH / APT / Features
+ _ssh="${SSH:-no}"
+ _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
+ _apt_cacher="${APT_CACHER:-}"
+ _apt_cacher_ip="${APT_CACHER_IP:-}"
+ _fuse="${ENABLE_FUSE:-no}"
+ _tun="${ENABLE_TUN:-no}"
+ _nesting="${ENABLE_NESTING:-1}"
+ _keyctl="${ENABLE_KEYCTL:-0}"
+ _mknod="${ENABLE_MKNOD:-0}"
+ _mount_fs="${ALLOW_MOUNT_FS:-}"
+ _protect="${PROTECT_CT:-no}"
+ _timezone="${CT_TIMEZONE:-}"
+ _tags="${TAGS:-}"
+ _verbose="${VERBOSE:-no}"
+
+ # Type / Resources / Identity
+ _unpriv="${CT_TYPE:-1}"
+ _cpu="${CORE_COUNT:-1}"
+ _ram="${RAM_SIZE:-1024}"
+ _disk="${DISK_SIZE:-4}"
+ _hostname="${HN:-$NSAPP}"
+
+ # Storage
+ _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
+ _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
+
+ {
+ echo "# App-specific defaults for ${APP} (${NSAPP})"
+ echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo
+
+ echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
+ echo "var_cpu=$(_sanitize_value "$_cpu")"
+ echo "var_ram=$(_sanitize_value "$_ram")"
+ echo "var_disk=$(_sanitize_value "$_disk")"
+
+ [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
+ [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
+ [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
+ [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
+ [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
+ [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
+ [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
+
+ [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
+ # var_ipv6_static removed - static IPs are unique, can't be default
+
+ [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
+ [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
+
+ [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
+ [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
+
+ [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
+ [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
+ [ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")"
+ [ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")"
+ [ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")"
+ [ -n "$_mount_fs" ] && echo "var_mount_fs=$(_sanitize_value "$_mount_fs")"
+ [ -n "$_protect" ] && echo "var_protection=$(_sanitize_value "$_protect")"
+ [ -n "$_timezone" ] && echo "var_timezone=$(_sanitize_value "$_timezone")"
+ [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
+ [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
+
+ [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
+ [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
+
+ [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
+ [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
+ } >"$tmpf"
+
+ echo "$tmpf"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults()
+#
+# - Called after advanced_settings()
+# - Offers to save current values as app defaults if not existing
+# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
+# ------------------------------------------------------------------------------
+maybe_offer_save_app_defaults() {
+ local app_vars_path
+ app_vars_path="$(get_app_defaults_path)"
+
+ # always build from current settings
+ local new_tmp diff_tmp
+ new_tmp="$(_build_current_app_vars_tmp)"
+ diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
+
+ # 1) if no file → offer to create
+ if [[ ! -f "$app_vars_path" ]]; then
+ if whiptail --backtitle "[dev] 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"
+ msg_ok "Saved app defaults: ${app_vars_path}"
+ fi
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 2) if file exists → build diff
+ _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
+
+ # if no differences → do nothing
+ if grep -q "^(No differences)$" "$diff_tmp"; then
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 3) if file exists → show menu with default selection "Update Defaults"
+ local app_vars_file
+ app_vars_file="$(basename "$app_vars_path")"
+
+ while true; do
+ local sel
+ sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "APP DEFAULTS – ${APP}" \
+ --menu "Differences detected. What do you want to do?" 20 78 10 \
+ "Update Defaults" "Write new values to ${app_vars_file}" \
+ "Keep Current" "Keep existing defaults (no changes)" \
+ "View Diff" "Show a detailed diff" \
+ "Cancel" "Abort without changes" \
+ --default-item "Update Defaults" \
+ 3>&1 1>&2 2>&3)" || { sel="Cancel"; }
+
+ case "$sel" in
+ "Update Defaults")
+ install -m 0644 "$new_tmp" "$app_vars_path"
+ msg_ok "Updated app defaults: ${app_vars_path}"
+ break
+ ;;
+ "Keep Current")
+ msg_custom "ℹ️" "${BL}" "Keeping current app defaults: ${app_vars_path}"
+ break
+ ;;
+ "View Diff")
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Diff – ${APP}" \
+ --scrolltext --textbox "$diff_tmp" 25 100
+ ;;
+ "Cancel" | *)
+ msg_custom "🚫" "${YW}" "Canceled. No changes to app defaults."
+ break
+ ;;
+ esac
+ done
+
+ rm -f "$new_tmp" "$diff_tmp"
+}
+
+ensure_storage_selection_for_vars_file() {
+ local vf="$1"
+
+ # 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-)
+
+ if [[ -n "$tpl" && -n "$ct" ]]; then
+ TEMPLATE_STORAGE="$tpl"
+ CONTAINER_STORAGE="$ct"
+ return 0
+ fi
+
+ choose_and_set_storage_for_file "$vf" template
+ choose_and_set_storage_for_file "$vf" container
+
+ # Silent operation - no output message
+}
+
+ensure_global_default_vars_file() {
+ local vars_path="/usr/local/community-scripts/default.vars"
+ if [[ ! -f "$vars_path" ]]; then
+ mkdir -p "$(dirname "$vars_path")"
+ touch "$vars_path"
+ fi
+ echo "$vars_path"
+}
+
+# ==============================================================================
+# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION
+# ==============================================================================
+
# ------------------------------------------------------------------------------
# advanced_settings()
#
-# - Interactive whiptail menu for advanced configuration
-# - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM
-# - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode
-# - Ends with confirmation or re-entry if cancelled
+# - Interactive whiptail menu for comprehensive container configuration
+# - Allows user to customize:
+# * Container type (privileged/unprivileged)
+# * Root password
+# * Container ID (CTID)
+# * Hostname
+# * Resources (disk size, CPU cores, RAM)
+# * Network (IPv4/IPv6, gateway, DNS, MAC, VLAN, MTU)
+# * SSH settings and key injection
+# * Advanced features (FUSE, TUN, keyctl)
+# * Tags for organization
+# * Verbose/debug mode
+# - Loops until user confirms or cancels
+# - Validates all input and shows current selections
# ------------------------------------------------------------------------------
advanced_settings() {
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
@@ -438,7 +1155,8 @@ advanced_settings() {
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
- echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os | ${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os |${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
fi
else
@@ -455,6 +1173,7 @@ advanced_settings() {
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
@@ -504,7 +1223,7 @@ advanced_settings() {
fi
done
- if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
+ if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
if [ -z "$CT_ID" ]; then
CT_ID="$NEXTID"
fi
@@ -859,6 +1578,51 @@ advanced_settings() {
configure_ssh_settings
export SSH_KEYS_FILE
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
+
+ # Advanced Settings - Proxmox Features
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS" --yesno "Configure Advanced Proxmox Features?" 10 58); then
+ # keyctl: for Docker support
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Enable keyctl()" --yesno "Allow keyctl() system calls?\n\nNeeded for: Docker inside container, systemd-networkd\nDefault: No (not needed for most applications)" 10 58); then
+ ENABLE_KEYCTL="1"
+ else
+ ENABLE_KEYCTL="0"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Allow keyctl(): ${BGN}$ENABLE_KEYCTL${CL}"
+
+ # mknod: device node creation
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Enable mknod()" --yesno "Allow device node creation?\n\nNeeded for: Complex device management (experimental, kernel 5.3+)\nDefault: No (rarely needed)" 10 58); then
+ ENABLE_MKNOD="1"
+ else
+ ENABLE_MKNOD="0"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Allow mknod(): ${BGN}$ENABLE_MKNOD${CL}"
+
+ # mount: specific filesystems
+ if MOUNT_FS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allow specific filesystems (e.g., nfs,fuse,ext4)\nLeave blank for defaults" 8 58 "$ALLOW_MOUNT_FS" --title "Mount Filesystems" 3>&1 1>&2 2>&3); then
+ ALLOW_MOUNT_FS="$MOUNT_FS"
+ [ -z "$ALLOW_MOUNT_FS" ] && ALLOW_MOUNT_FS="(defaults)"
+ else
+ exit_script
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Mount Filesystems: ${BGN}$ALLOW_MOUNT_FS${CL}"
+
+ # Container protection
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Protection Flag" --yesno "Prevent accidental deletion?\n\nIf enabled, container cannot be deleted or its disk modified\nDefault: No" 10 58); then
+ PROTECT_CT="yes"
+ else
+ PROTECT_CT="no"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Container Protection: ${BGN}$PROTECT_CT${CL}"
+
+ # Container timezone
+ if CT_TIMEZONE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set container timezone (e.g., Europe/Berlin)\nLeave blank to use host timezone" 8 58 "$CT_TIMEZONE" --title "Container Timezone" 3>&1 1>&2 2>&3); then
+ [ -z "$CT_TIMEZONE" ] && CT_TIMEZONE="(host)"
+ else
+ exit_script
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Container Timezone: ${BGN}$CT_TIMEZONE${CL}"
+ fi
+
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
ENABLE_FUSE="yes"
else
@@ -878,17 +1642,25 @@ advanced_settings() {
else
clear
header_info
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
advanced_settings
fi
}
+# ==============================================================================
+# SECTION 7: USER INTERFACE & DIAGNOSTICS
+# ==============================================================================
+
# ------------------------------------------------------------------------------
# diagnostics_check()
#
# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
# - Asks user whether to send anonymous diagnostic data
# - Saves DIAGNOSTICS=yes/no in the config file
+# - Creates file if missing with default DIAGNOSTICS=yes
+# - Reads current diagnostics setting from file
+# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out
# ------------------------------------------------------------------------------
diagnostics_check() {
if ! [ -d "/usr/local/community-scripts" ]; then
@@ -951,523 +1723,6 @@ EOF
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
fi
-
-}
-
-# ------------------------------------------------------------------------------
-# default_var_settings
-#
-# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
-# - Loads var_* values from default.vars (safe parser, no source/eval)
-# - Precedence: ENV var_* > default.vars > built-in defaults
-# - Maps var_verbose → VERBOSE
-# - Calls base_settings "$VERBOSE" and echo_default
-# ------------------------------------------------------------------------------
-default_var_settings() {
- # Allowed var_* keys (alphabetically sorted)
- local VAR_WHITELIST=(
- var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
- var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
- var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
- var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
- )
-
- # Snapshot: environment variables (highest precedence)
- declare -A _HARD_ENV=()
- local _k
- for _k in "${VAR_WHITELIST[@]}"; do
- if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
- done
-
- # Find default.vars location
- local _find_default_vars
- _find_default_vars() {
- local f
- for f in \
- /usr/local/community-scripts/default.vars \
- "$HOME/.config/community-scripts/default.vars" \
- "./default.vars"; do
- [ -f "$f" ] && {
- echo "$f"
- return 0
- }
- done
- return 1
- }
- # Allow override of storages via env (for non-interactive use cases)
- [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
- [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
-
- # Create once, with storages already selected, no var_ctid/var_hostname lines
- local _ensure_default_vars
- _ensure_default_vars() {
- _find_default_vars >/dev/null 2>&1 && return 0
-
- local canonical="/usr/local/community-scripts/default.vars"
- msg_info "No default.vars found. Creating ${canonical}"
- mkdir -p /usr/local/community-scripts
-
- # Pick storages before writing the file (always ask unless only one)
- # Create a minimal temp file to write into
- : >"$canonical"
-
- # Base content (no var_ctid / var_hostname here)
- cat >"$canonical" <<'EOF'
-# Community-Scripts defaults (var_* only). Lines starting with # are comments.
-# Precedence: ENV var_* > default.vars > built-ins.
-# Keep keys alphabetically sorted.
-
-# Container type
-var_unprivileged=1
-
-# Resources
-var_cpu=1
-var_disk=4
-var_ram=1024
-
-# Network
-var_brg=vmbr0
-var_net=dhcp
-var_ipv6_method=none
-# var_gateway=
-# var_ipv6_static=
-# var_vlan=
-# var_mtu=
-# var_mac=
-# var_ns=
-
-# SSH
-var_ssh=no
-# var_ssh_authorized_key=
-
-# APT cacher (optional)
-# var_apt_cacher=yes
-# var_apt_cacher_ip=192.168.1.10
-
-# Features/Tags/verbosity
-var_fuse=no
-var_tun=no
-var_tags=community-script
-var_verbose=no
-
-# Security (root PW) – empty => autologin
-# var_pw=
-EOF
-
- # Now choose storages (always prompt unless just one exists)
- choose_and_set_storage_for_file "$canonical" template
- choose_and_set_storage_for_file "$canonical" container
-
- chmod 0644 "$canonical"
- msg_ok "Created ${canonical}"
- }
-
- # Whitelist check
- local _is_whitelisted_key
- _is_whitelisted_key() {
- local k="$1"
- local w
- for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
- return 1
- }
-
- # Safe parser for KEY=VALUE lines
- local _load_vars_file
- _load_vars_file() {
- local file="$1"
- [ -f "$file" ] || return 0
- msg_info "Loading defaults from ${file}"
- local line key val
- while IFS= read -r line || [ -n "$line" ]; do
- line="${line#"${line%%[![:space:]]*}"}"
- line="${line%"${line##*[![:space:]]}"}"
- [[ -z "$line" || "$line" == \#* ]] && continue
- if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
- local var_key="${BASH_REMATCH[1]}"
- local var_val="${BASH_REMATCH[2]}"
-
- [[ "$var_key" != var_* ]] && continue
- _is_whitelisted_key "$var_key" || {
- msg_debug "Ignore non-whitelisted ${var_key}"
- continue
- }
-
- # Strip quotes
- if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
- var_val="${BASH_REMATCH[1]}"
- elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
- var_val="${BASH_REMATCH[1]}"
- fi
-
- # Unsafe characters
- case $var_val in
- \"*\")
- var_val=${var_val#\"}
- var_val=${var_val%\"}
- ;;
- \'*\')
- var_val=${var_val#\'}
- var_val=${var_val%\'}
- ;;
- esac # Hard env wins
- [[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue
- # Set only if not already exported
- [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
- else
- msg_warn "Malformed line in ${file}: ${line}"
- fi
- done <"$file"
- msg_ok "Loaded ${file}"
- }
-
- # 1) Ensure file exists
- _ensure_default_vars
-
- # 2) Load file
- local dv
- dv="$(_find_default_vars)" || {
- msg_error "default.vars not found after ensure step"
- return 1
- }
- _load_vars_file "$dv"
-
- # 3) Map var_verbose → VERBOSE
- if [[ -n "${var_verbose:-}" ]]; then
- case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
- else
- VERBOSE="no"
- fi
-
- # 4) Apply base settings and show summary
- METHOD="mydefaults-global"
- base_settings "$VERBOSE"
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
- echo_default
-}
-
-# ------------------------------------------------------------------------------
-# get_app_defaults_path()
-#
-# - Returns full path for app-specific defaults file
-# - Example: /usr/local/community-scripts/defaults/.vars
-# ------------------------------------------------------------------------------
-
-get_app_defaults_path() {
- local n="${NSAPP:-${APP,,}}"
- echo "/usr/local/community-scripts/defaults/${n}.vars"
-}
-
-# ------------------------------------------------------------------------------
-# maybe_offer_save_app_defaults
-#
-# - Called after advanced_settings returned with fully chosen values.
-# - If no .vars exists, offers to persist current advanced settings
-# into /usr/local/community-scripts/defaults/.vars
-# - Only writes whitelisted var_* keys.
-# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
-# ------------------------------------------------------------------------------
-if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
- declare -ag VAR_WHITELIST=(
- var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
- var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
- var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
- var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
- )
-fi
-
-_is_whitelisted_key() {
- local k="$1"
- local w
- for w in "${VAR_WHITELIST[@]}"; do [[ "$k" == "$w" ]] && return 0; done
- return 1
-}
-
-_sanitize_value() {
- # Disallow Command-Substitution / Shell-Meta
- case "$1" in
- *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
- echo ""
- return 0
- ;;
- esac
- echo "$1"
-}
-
-# Map-Parser: read var_* from file into _VARS_IN associative array
-declare -A _VARS_IN
-_load_vars_file() {
- local file="$1"
- [ -f "$file" ] || return 0
- msg_info "Loading defaults from ${file}"
- local line key val
- while IFS= read -r line || [ -n "$line" ]; do
- line="${line#"${line%%[![:space:]]*}"}"
- line="${line%"${line##*[![:space:]]}"}"
- [ -z "$line" ] && continue
- case "$line" in
- \#*) continue ;;
- esac
- key=$(printf "%s" "$line" | cut -d= -f1)
- val=$(printf "%s" "$line" | cut -d= -f2-)
- case "$key" in
- var_*)
- if _is_whitelisted_key "$key"; then
- [ -z "${!key+x}" ] && export "$key=$val"
- fi
- ;;
- esac
- done <"$file"
- msg_ok "Loaded ${file}"
-}
-
-# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
-_build_vars_diff() {
- local oldf="$1" newf="$2"
- local k
- local -A OLD=() NEW=()
- _load_vars_file_to_map "$oldf"
- for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
- _load_vars_file_to_map "$newf"
- for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
-
- local out
- out+="# Diff for ${APP} (${NSAPP})\n"
- out+="# Old: ${oldf}\n# New: ${newf}\n\n"
-
- local found_change=0
-
- # Changed & Removed
- for k in "${!OLD[@]}"; do
- if [[ -v NEW["$k"] ]]; then
- if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
- out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
- found_change=1
- fi
- else
- out+="- ${k}\n - old: ${OLD[$k]}\n"
- found_change=1
- fi
- done
-
- # Added
- for k in "${!NEW[@]}"; do
- if [[ ! -v OLD["$k"] ]]; then
- out+="+ ${k}\n + new: ${NEW[$k]}\n"
- found_change=1
- fi
- done
-
- if [[ $found_change -eq 0 ]]; then
- out+="(No differences)\n"
- fi
-
- printf "%b" "$out"
-}
-
-# Build a temporary .vars file from current advanced settings
-_build_current_app_vars_tmp() {
- tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
-
- # NET/GW
- _net="${NET:-}"
- _gate=""
- case "${GATE:-}" in
- ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
- esac
-
- # IPv6
- _ipv6_method="${IPV6_METHOD:-auto}"
- _ipv6_static=""
- _ipv6_gateway=""
- if [ "$_ipv6_method" = "static" ]; then
- _ipv6_static="${IPV6_ADDR:-}"
- _ipv6_gateway="${IPV6_GATE:-}"
- fi
-
- # MTU/VLAN/MAC
- _mtu=""
- _vlan=""
- _mac=""
- case "${MTU:-}" in
- ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
- esac
- case "${VLAN:-}" in
- ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
- esac
- case "${MAC:-}" in
- ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
- esac
-
- # DNS / Searchdomain
- _ns=""
- _searchdomain=""
- case "${NS:-}" in
- -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
- esac
- case "${SD:-}" in
- -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
- esac
-
- # SSH / APT / Features
- _ssh="${SSH:-no}"
- _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
- _apt_cacher="${APT_CACHER:-}"
- _apt_cacher_ip="${APT_CACHER_IP:-}"
- _fuse="${ENABLE_FUSE:-no}"
- _tun="${ENABLE_TUN:-no}"
- _tags="${TAGS:-}"
- _verbose="${VERBOSE:-no}"
-
- # Type / Resources / Identity
- _unpriv="${CT_TYPE:-1}"
- _cpu="${CORE_COUNT:-1}"
- _ram="${RAM_SIZE:-1024}"
- _disk="${DISK_SIZE:-4}"
- _hostname="${HN:-$NSAPP}"
-
- # Storage
- _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
- _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
-
- {
- echo "# App-specific defaults for ${APP} (${NSAPP})"
- echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
- echo
-
- echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
- echo "var_cpu=$(_sanitize_value "$_cpu")"
- echo "var_ram=$(_sanitize_value "$_ram")"
- echo "var_disk=$(_sanitize_value "$_disk")"
-
- [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
- [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
- [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
- [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
- [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
- [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
- [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
-
- [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
- [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")"
-
- [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
- [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
-
- [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
- [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
-
- [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
- [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
- [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
- [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
-
- [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
- [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
-
- [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
- [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
- } >"$tmpf"
-
- echo "$tmpf"
-}
-
-# ------------------------------------------------------------------------------
-# maybe_offer_save_app_defaults()
-#
-# - Called after advanced_settings()
-# - Offers to save current values as app defaults if not existing
-# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
-# ------------------------------------------------------------------------------
-maybe_offer_save_app_defaults() {
- local app_vars_path
- app_vars_path="$(get_app_defaults_path)"
-
- # always build from current settings
- local new_tmp diff_tmp
- new_tmp="$(_build_current_app_vars_tmp)"
- diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
-
- # 1) if no file → offer to create
- if [[ ! -f "$app_vars_path" ]]; then
- if whiptail --backtitle "[dev] 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"
- msg_ok "Saved app defaults: ${app_vars_path}"
- fi
- rm -f "$new_tmp" "$diff_tmp"
- return 0
- fi
-
- # 2) if file exists → build diff
- _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
-
- # if no differences → do nothing
- if grep -q "^(No differences)$" "$diff_tmp"; then
- rm -f "$new_tmp" "$diff_tmp"
- return 0
- fi
-
- # 3) if file exists → show menu with default selection "Update Defaults"
- local app_vars_file
- app_vars_file="$(basename "$app_vars_path")"
-
- while true; do
- local sel
- sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
- --title "APP DEFAULTS – ${APP}" \
- --menu "Differences detected. What do you want to do?" 20 78 10 \
- "Update Defaults" "Write new values to ${app_vars_file}" \
- "Keep Current" "Keep existing defaults (no changes)" \
- "View Diff" "Show a detailed diff" \
- "Cancel" "Abort without changes" \
- --default-item "Update Defaults" \
- 3>&1 1>&2 2>&3)" || { sel="Cancel"; }
-
- case "$sel" in
- "Update Defaults")
- install -m 0644 "$new_tmp" "$app_vars_path"
- msg_ok "Updated app defaults: ${app_vars_path}"
- break
- ;;
- "Keep Current")
- msg_info "Keeping current app defaults: ${app_vars_path}"
- break
- ;;
- "View Diff")
- whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
- --title "Diff – ${APP}" \
- --scrolltext --textbox "$diff_tmp" 25 100
- ;;
- "Cancel" | *)
- msg_info "Canceled. No changes to app defaults."
- break
- ;;
- esac
- done
-
- rm -f "$new_tmp" "$diff_tmp"
-}
-
-ensure_storage_selection_for_vars_file() {
- local vf="$1"
-
- # 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-)
-
- if [[ -n "$tpl" && -n "$ct" ]]; then
- TEMPLATE_STORAGE="$tpl"
- CONTAINER_STORAGE="$ct"
- return 0
- fi
-
- choose_and_set_storage_for_file "$vf" template
- choose_and_set_storage_for_file "$vf" container
-
- msg_ok "Storage configuration saved to $(basename "$vf")"
}
diagnostics_menu() {
@@ -1492,13 +1747,30 @@ diagnostics_menu() {
fi
}
-ensure_global_default_vars_file() {
- local vars_path="/usr/local/community-scripts/default.vars"
- if [[ ! -f "$vars_path" ]]; then
- mkdir -p "$(dirname "$vars_path")"
- touch "$vars_path"
+# ------------------------------------------------------------------------------
+# echo_default()
+#
+# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
+# - Uses icons and formatting for readability
+# - Convert CT_TYPE to description
+# ------------------------------------------------------------------------------
+echo_default() {
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
fi
- echo "$vars_path"
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ if [ "$VERBOSE" == "yes" ]; then
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
+ fi
+ echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
+ echo -e " "
}
# ------------------------------------------------------------------------------
@@ -1523,7 +1795,16 @@ install_script() {
fi
NEXTID=$(pvesh get /cluster/nextid)
- timezone=$(cat /etc/timezone)
+
+ # Get timezone using timedatectl (Debian 13+ compatible)
+ # Fallback to /etc/timezone for older systems
+ if command -v timedatectl >/dev/null 2>&1; then
+ timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "UTC")
+ elif [ -f /etc/timezone ]; then
+ timezone=$(cat /etc/timezone)
+ else
+ timezone="UTC"
+ fi
# Show APP Header
header_info
@@ -1584,7 +1865,9 @@ install_script() {
;;
2 | advanced | ADVANCED)
header_info
+
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
METHOD="advanced"
base_settings
advanced_settings
@@ -1649,13 +1932,12 @@ settings_menu() {
local settings_items=(
"1" "Manage API-Diagnostic Setting"
"2" "Edit Default.vars"
- "3" "Edit Default Storage"
)
if [ -f "$(get_app_defaults_path)" ]; then
- settings_items+=("4" "Edit App.vars for ${APP}")
- settings_items+=("5" "Exit")
- else
+ settings_items+=("3" "Edit App.vars for ${APP}")
settings_items+=("4" "Exit")
+ else
+ settings_items+=("3" "Exit")
fi
local choice
@@ -1682,57 +1964,6 @@ settings_menu() {
done
}
-# ===== Unified storage selection & writing to vars files =====
-_write_storage_to_vars() {
- # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
- local vf="$1" key="$2" val="$3"
- # remove uncommented and commented versions to avoid duplicates
- sed -i "/^[#[:space:]]*${key}=/d" "$vf"
- echo "${key}=${val}" >>"$vf"
-}
-
-choose_and_set_storage_for_file() {
- # $1 = vars_file, $2 = class ('container'|'template')
- local vf="$1" class="$2" key="" current=""
- case "$class" in
- container) key="var_container_storage" ;;
- template) key="var_template_storage" ;;
- *)
- msg_error "Unknown storage class: $class"
- return 1
- ;;
- esac
-
- current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
-
- # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
- local content="rootdir"
- [[ "$class" == "template" ]] && content="vztmpl"
- local count
- count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
-
- if [[ "$count" -eq 1 ]]; then
- STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
- STORAGE_INFO=""
- else
- # If the current value is preselectable, we could show it, but per your requirement we always offer selection
- select_storage "$class" || return 1
- fi
-
- _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
-
- # Keep environment in sync for later steps (e.g. app-default save)
- if [[ "$class" == "container" ]]; then
- export var_container_storage="$STORAGE_RESULT"
- export CONTAINER_STORAGE="$STORAGE_RESULT"
- else
- export var_template_storage="$STORAGE_RESULT"
- export TEMPLATE_STORAGE="$STORAGE_RESULT"
- fi
-
- msg_ok "Updated ${key} → ${STORAGE_RESULT}"
-}
-
# ------------------------------------------------------------------------------
# check_container_resources()
#
@@ -1999,13 +2230,22 @@ start() {
fi
}
+# ==============================================================================
+# SECTION 8: CONTAINER CREATION & DEPLOYMENT
+# ==============================================================================
+
# ------------------------------------------------------------------------------
# build_container()
#
-# - Creates and configures the LXC container
-# - Builds network string and applies features (FUSE, TUN, VAAPI passthrough)
+# - Main function for creating and configuring LXC container
+# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6)
+# - Creates container via pct create with all specified settings
+# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough
# - Starts container and waits for network connectivity
-# - Installs base packages, SSH keys, and runs -install.sh
+# - Installs base packages (curl, sudo, etc.)
+# - Injects SSH keys if configured
+# - Executes -install.sh inside container
+# - Posts installation telemetry to API if diagnostics enabled
# ------------------------------------------------------------------------------
build_container() {
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
@@ -2058,14 +2298,34 @@ build_container() {
none) ;;
esac
- if [ "$CT_TYPE" == "1" ]; then
- FEATURES="keyctl=1,nesting=1"
- else
- FEATURES="nesting=1"
+ # Build FEATURES array with advanced settings
+ # Note: All feature flags are already normalized to 0/1 in default_settings()
+ # Proxmox requires each feature as a separate parameter, not comma-separated string
+ FEATURES_ARRAY=()
+ FEATURES_ARRAY+=("nesting=${ENABLE_NESTING}")
+
+ # keyctl: needed for Docker inside containers (systemd-networkd workaround)
+ # Typically needed for unprivileged containers with Docker
+ if [ "$CT_TYPE" == "1" ] || [ "$ENABLE_KEYCTL" == "1" ]; then
+ FEATURES_ARRAY+=("keyctl=1")
fi
- if [ "$ENABLE_FUSE" == "yes" ]; then
- FEATURES="$FEATURES,fuse=1"
+ # mknod: allow device node creation (requires kernel 5.3+, experimental)
+ if [ "$ENABLE_MKNOD" == "1" ]; then
+ FEATURES_ARRAY+=("mknod=1")
+ fi
+
+ # FUSE: required for rclone, mergerfs, AppImage, etc.
+ if [ "$ENABLE_FUSE" == "1" ]; then
+ FEATURES_ARRAY+=("fuse=1")
+ fi
+
+ # mount: allow specific filesystems (e.g., nfs, ext4, etc.)
+ # Format: mount=fstype1;fstype2;fstype3 (semicolon-separated, not comma!)
+ if [ -n "$ALLOW_MOUNT_FS" ]; then
+ # Replace commas with semicolons for proper pct syntax
+ ALLOW_MOUNT_FS_FORMATTED="${ALLOW_MOUNT_FS//,/;}"
+ FEATURES_ARRAY+=("mount=$ALLOW_MOUNT_FS_FORMATTED")
fi
TEMP_DIR=$(mktemp -d)
@@ -2077,6 +2337,17 @@ build_container() {
fi
export DIAGNOSTICS="$DIAGNOSTICS"
export RANDOM_UUID="$RANDOM_UUID"
+ export SESSION_ID="$SESSION_ID"
+ export BUILD_LOG="$BUILD_LOG"
+ export INSTALL_LOG="/root/.install-${SESSION_ID}.log"
+ export dev_mode="${dev_mode:-}"
+ export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}"
+ export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}"
+ export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}"
+ export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}"
+ export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}"
+ export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}"
+ export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}"
export CACHER="$APT_CACHER"
export CACHER_IP="$APT_CACHER_IP"
export tz="$timezone"
@@ -2090,22 +2361,59 @@ build_container() {
export CTTYPE="$CT_TYPE"
export ENABLE_FUSE="$ENABLE_FUSE"
export ENABLE_TUN="$ENABLE_TUN"
+ export ENABLE_NESTING="$ENABLE_NESTING"
+ export ENABLE_KEYCTL="$ENABLE_KEYCTL"
+ export ENABLE_MKNOD="$ENABLE_MKNOD"
+ export ALLOW_MOUNT_FS="$ALLOW_MOUNT_FS"
+ export PROTECT_CT="$PROTECT_CT"
+ export CT_TIMEZONE="$CT_TIMEZONE"
export PCT_OSTYPE="$var_os"
export PCT_OSVERSION="$var_version"
export PCT_DISK_SIZE="$DISK_SIZE"
- export PCT_OPTIONS="
- -features $FEATURES
- -hostname $HN
- -tags $TAGS
- $SD
- $NS
- $NET_STRING
- -onboot 1
- -cores $CORE_COUNT
- -memory $RAM_SIZE
- -unprivileged $CT_TYPE
- $PW
-"
+
+ # Build PCT_OPTIONS array (not string) for proper parameter handling
+ PCT_OPTIONS=()
+
+ # Add features - each as separate -features parameter
+ for feature in "${FEATURES_ARRAY[@]}"; do
+ PCT_OPTIONS+=("-features" "$feature")
+ done
+
+ PCT_OPTIONS+=("-hostname" "$HN")
+ PCT_OPTIONS+=("-tags" "$TAGS")
+
+ if [ -n "$SD" ]; then
+ PCT_OPTIONS+=($SD) # Storage device flags (already formatted)
+ fi
+
+ if [ -n "$NS" ]; then
+ PCT_OPTIONS+=($NS) # Nameserver flags (already formatted)
+ fi
+
+ # Network configuration (single string with all network parameters)
+ PCT_OPTIONS+=($NET_STRING)
+
+ PCT_OPTIONS+=("-onboot" "1")
+ PCT_OPTIONS+=("-cores" "$CORE_COUNT")
+ PCT_OPTIONS+=("-memory" "$RAM_SIZE")
+ PCT_OPTIONS+=("-unprivileged" "$CT_TYPE")
+
+ # Protection flag
+ if [ "$PROTECT_CT" == "1" ]; then
+ PCT_OPTIONS+=("-protection" "1")
+ fi
+
+ # Timezone flag
+ if [ -n "$CT_TIMEZONE" ]; then
+ PCT_OPTIONS+=("-timezone" "$CT_TIMEZONE")
+ fi
+
+ # Password flag (already formatted as "-password xxx")
+ if [ -n "$PW" ]; then
+ PCT_OPTIONS+=($PW)
+ fi
+
+ export PCT_OPTIONS
export TEMPLATE_STORAGE="${var_template_storage:-}"
export CONTAINER_STORAGE="${var_container_storage:-}"
create_lxc_container || exit $?
@@ -2145,7 +2453,7 @@ build_container() {
# Check for Intel GPU - look for Intel vendor ID [8086]
if echo "$pci_vga_info" | grep -q "\[8086:"; then
- msg_info "Detected Intel GPU"
+ msg_custom "🎮" "${BL}" "Detected Intel GPU"
if [[ -d /dev/dri ]]; then
for d in /dev/dri/renderD* /dev/dri/card*; do
[[ -e "$d" ]] && INTEL_DEVICES+=("$d")
@@ -2155,7 +2463,7 @@ build_container() {
# Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then
- msg_info "Detected AMD GPU"
+ msg_custom "🎮" "${RD}" "Detected AMD GPU"
if [[ -d /dev/dri ]]; then
# Only add if not already claimed by Intel
if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
@@ -2168,43 +2476,18 @@ build_container() {
# Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
if echo "$pci_vga_info" | grep -q "\[10de:"; then
- msg_info "Detected NVIDIA GPU"
- if ! check_nvidia_host_setup; then
- msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough."
- msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually."
- return 0
- fi
+ msg_custom "🎮" "${GN}" "Detected NVIDIA GPU"
- for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do
+ # Simple passthrough - just bind /dev/nvidia* devices if they exist
+ for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do
[[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
done
- if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
- msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found"
- msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver"
+ if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
+ msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough"
else
- if [[ "$CT_TYPE" == "0" ]]; then
- cat <>"$LXC_CONFIG"
- # NVIDIA GPU Passthrough (privileged)
- lxc.cgroup2.devices.allow: c 195:* rwm
- lxc.cgroup2.devices.allow: c 243:* rwm
- lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
- lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
- lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
- lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file
-EOF
-
- if [[ -e /dev/dri/renderD128 ]]; then
- echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG"
- fi
-
- export GPU_TYPE="NVIDIA"
- export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
- msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})"
- else
- msg_warn "NVIDIA passthrough only supported for privileged containers"
- return 0
- fi
+ msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found"
+ msg_custom "ℹ️" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)"
fi
fi
@@ -2265,7 +2548,7 @@ EOF
fi
if [[ $gpu_count -eq 0 ]]; then
- msg_info "No GPU devices found for passthrough"
+ msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough"
return 0
fi
@@ -2274,7 +2557,7 @@ EOF
if [[ $gpu_count -eq 1 ]]; then
# Automatic selection for single GPU
selected_gpu="${available_gpus[0]}"
- msg_info "Automatically configuring ${selected_gpu} GPU passthrough"
+ msg_custom "⚙️" "${GN}" "Automatically configuring ${selected_gpu} GPU passthrough"
else
# Multiple GPUs - ask user
echo -e "\n${INFO} Multiple GPU types detected:"
@@ -2305,65 +2588,34 @@ EOF
[[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
[[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
- # For Proxmox WebUI visibility, add as dev0, dev1 etc.
+ # Use pct set to add devices with proper dev0/dev1 format
+ # GIDs will be detected and set after container starts
+ local dev_index=0
for dev in "${devices[@]}"; do
- if [[ "$CT_TYPE" == "0" ]]; then
- # Privileged container - use dev entries for WebUI visibility
- # Use initial GID 104 (render) for renderD*, 44 (video) for card*
- if [[ "$dev" =~ renderD ]]; then
- echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG"
- else
- echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG"
- fi
- dev_idx=$((dev_idx + 1))
-
- # Also add cgroup allows for privileged containers
- local major minor
- major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
- minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
-
- if [[ "$major" != "0" && "$minor" != "0" ]]; then
- echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
- fi
- else
- # Unprivileged container
- if [[ "$dev" =~ renderD ]]; then
- echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
- else
- echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
- fi
- dev_idx=$((dev_idx + 1))
- fi
+ # Add to config using pct set (will be visible in GUI)
+ echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
+ dev_index=$((dev_index + 1))
done
export GPU_TYPE="$selected_gpu"
- msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)"
+ msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)"
;;
NVIDIA)
if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
- msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver"
- return 1
+ msg_warn "No NVIDIA devices available for passthrough"
+ return 0
fi
+ # Use pct set for NVIDIA devices
+ local dev_index=0
for dev in "${NVIDIA_DEVICES[@]}"; do
- # NVIDIA devices typically need different handling
- echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
- dev_idx=$((dev_idx + 1))
-
- if [[ "$CT_TYPE" == "0" ]]; then
- local major minor
- major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
- minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
-
- if [[ "$major" != "0" && "$minor" != "0" ]]; then
- echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
- fi
- fi
+ echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
+ dev_index=$((dev_index + 1))
done
export GPU_TYPE="NVIDIA"
- msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)"
+ msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed"
;;
esac
}
@@ -2380,7 +2632,7 @@ EOF
# Coral TPU passthrough
if [[ -e /dev/apex_0 ]]; then
- msg_info "Detected Coral TPU - configuring passthrough"
+ msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough"
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
fi
}
@@ -2426,21 +2678,8 @@ EOF
exit 1
fi
- # Try to reach gateway
- gw_ok=0
- for i in {1..10}; do
- if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then
- gw_ok=1
- break
- fi
- sleep 1
- done
-
- if [ "$gw_ok" -eq 1 ]; then
- msg_ok "Network in LXC is reachable (IP $ip_in_lxc)"
- else
- msg_warn "Network reachable but gateway check failed"
- fi
+ # Simple connectivity check - just verify IP is assigned
+ msg_ok "Network configured (IP: $ip_in_lxc)"
fi
# Function to get correct GID inside container
get_container_gid() {
@@ -2484,7 +2723,9 @@ EOF'
fi
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
- pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
+ # Set timezone using symlink (Debian 13+ compatible)
+ # Create /etc/timezone for backwards compatibility with older scripts
+ pct exec "$CTID" -- bash -c "tz='$tz'; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true"
else
msg_warn "Skipping timezone setup – zone '$tz' not found in container"
fi
@@ -2497,25 +2738,121 @@ EOF'
msg_ok "Customized LXC Container"
- # Verify GPU access if enabled
- if [[ "${ENABLE_VAAPI:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
- pct exec "$CTID" -- bash -c "vainfo >/dev/null 2>&1" &&
- msg_ok "VAAPI verified working" ||
- msg_warn "VAAPI verification failed - may need additional configuration"
- fi
-
- if [[ "${ENABLE_NVIDIA:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
- pct exec "$CTID" -- bash -c "nvidia-smi >/dev/null 2>&1" &&
- msg_ok "NVIDIA verified working" ||
- msg_warn "NVIDIA verification failed - may need additional configuration"
- fi
-
# Install SSH keys
install_ssh_keys_into_ct
# Run application installer
- if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then
- exit $?
+ # NOTE: We disable error handling here because:
+ # 1. Container errors are caught by error_handler INSIDE container
+ # 2. Container creates flag file with exit code
+ # 3. We read flag file and handle cleanup manually below
+ # 4. We DON'T want host error_handler to fire for lxc-attach command itself
+
+ set +Eeuo pipefail # Disable ALL error handling temporarily
+ trap - ERR # Remove ERR trap completely
+
+ lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"
+ local lxc_exit=$?
+
+ set -Eeuo pipefail # Re-enable error handling
+ trap 'error_handler' ERR # Restore ERR trap
+
+ # Check for error flag file in container (more reliable than lxc-attach exit code)
+ local install_exit_code=0
+ if [[ -n "${SESSION_ID:-}" ]]; then
+ local error_flag="/root/.install-${SESSION_ID}.failed"
+ if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then
+ install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1")
+ pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true
+ fi
+ fi
+
+ # Fallback to lxc-attach exit code if no flag file
+ if [[ $install_exit_code -eq 0 && $lxc_exit -ne 0 ]]; then
+ install_exit_code=$lxc_exit
+ fi
+
+ # Installation failed?
+ if [[ $install_exit_code -ne 0 ]]; then
+ msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})"
+
+ # Copy both logs from container before potential deletion
+ local build_log_copied=false
+ local install_log_copied=false
+
+ if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
+ # Copy BUILD_LOG (creation log) if it exists
+ if [[ -f "${BUILD_LOG}" ]]; then
+ cp "${BUILD_LOG}" "/tmp/create-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null && build_log_copied=true
+ fi
+
+ # Copy INSTALL_LOG from container
+ if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "/tmp/install-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null; then
+ install_log_copied=true
+ fi
+
+ # Show available logs
+ echo ""
+ [[ "$build_log_copied" == true ]] && echo -e "${GN}✔${CL} Container creation log: ${BL}/tmp/create-lxc-${CTID}-${SESSION_ID}.log${CL}"
+ [[ "$install_log_copied" == true ]] && echo -e "${GN}✔${CL} Installation log: ${BL}/tmp/install-lxc-${CTID}-${SESSION_ID}.log${CL}"
+ fi
+
+ # Dev mode: Keep container or open breakpoint shell
+ if [[ "${DEV_MODE_KEEP:-false}" == "true" ]]; then
+ msg_dev "Keep mode active - container ${CTID} preserved"
+ return 0
+ elif [[ "${DEV_MODE_BREAKPOINT:-false}" == "true" ]]; then
+ msg_dev "Breakpoint mode - opening shell in container ${CTID}"
+ echo -e "${YW}Type 'exit' to return to host${CL}"
+ pct enter "$CTID"
+ echo ""
+ echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}"
+ if read -r response && [[ "$response" =~ ^[Yy]$ ]]; then
+ pct stop "$CTID" &>/dev/null || true
+ pct destroy "$CTID" &>/dev/null || true
+ msg_ok "Container ${CTID} removed"
+ else
+ msg_dev "Container ${CTID} kept for debugging"
+ fi
+ exit $install_exit_code
+ fi
+
+ # Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner)
+ echo ""
+ echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
+
+ if read -t 60 -r response; then
+ if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
+ # Remove container
+ echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
+ pct stop "$CTID" &>/dev/null || true
+ pct destroy "$CTID" &>/dev/null || true
+ echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
+ elif [[ "$response" =~ ^[Nn]$ ]]; then
+ echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}"
+
+ # Dev mode: Setup MOTD/SSH for debugging access to broken container
+ if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then
+ echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}"
+ if pct exec "$CTID" -- bash -c "
+ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/install.func)
+ declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true
+ " >/dev/null 2>&1; then
+ local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
+ echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}"
+ fi
+ fi
+ fi
+ else
+ # Timeout - auto-remove
+ echo -e "\n${YW}No response - auto-removing container${CL}"
+ echo -e "${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
+ pct stop "$CTID" &>/dev/null || true
+ pct destroy "$CTID" &>/dev/null || true
+ echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
+ fi
+
+ exit $install_exit_code
fi
}
@@ -2545,7 +2882,7 @@ destroy_lxc() {
fi
;;
"" | n | no)
- msg_info "Container was not removed."
+ msg_custom "ℹ️" "${BL}" "Container was not removed."
;;
*)
msg_warn "Invalid response. Container was not removed."
@@ -2597,78 +2934,40 @@ fix_gpu_gids() {
return 0
fi
- msg_info "Detecting and setting correct GPU group IDs"
+ msg_custom "🔧" "${BL}" "Detecting and setting correct GPU group IDs"
- # Ermittle die tatsächlichen GIDs aus dem Container
+ # Get actual GIDs from container
local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
- # Fallbacks wenn Gruppen nicht existieren
+ # Create groups if they don't exist
if [[ -z "$video_gid" ]]; then
- # Versuche die video Gruppe zu erstellen
- pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true"
+ pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>&1
video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
- [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback
+ [[ -z "$video_gid" ]] && video_gid="44"
fi
if [[ -z "$render_gid" ]]; then
- # Versuche die render Gruppe zu erstellen
- pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true"
+ pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>&1
render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
- [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback
+ [[ -z "$render_gid" ]] && render_gid="104"
fi
- msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}"
+ # Stop container to update config
+ pct stop "$CTID" >/dev/null 2>&1
+ sleep 1
- # Prüfe ob die GIDs von den Defaults abweichen
- local need_update=0
- if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then
- need_update=1
- fi
+ # Update dev entries with correct GIDs
+ sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG"
+ sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG"
- if [[ $need_update -eq 1 ]]; then
- msg_info "Updating device GIDs in container config"
+ # Restart container
+ pct start "$CTID" >/dev/null 2>&1
+ sleep 2
- # Stoppe Container für Config-Update
- pct stop "$CTID" >/dev/null 2>&1
+ msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})"
- # Update die dev Einträge mit korrekten GIDs
- # Backup der Config
- cp "$LXC_CONFIG" "${LXC_CONFIG}.bak"
-
- # Parse und update jeden dev Eintrag
- while IFS= read -r line; do
- if [[ "$line" =~ ^dev[0-9]+: ]]; then
- # Extract device path
- local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/')
- local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/')
-
- if [[ "$device_path" =~ renderD ]]; then
- # RenderD device - use render GID
- echo "${dev_num}: ${device_path},gid=${render_gid}"
- elif [[ "$device_path" =~ card ]]; then
- # Card device - use video GID
- echo "${dev_num}: ${device_path},gid=${video_gid}"
- else
- # Keep original line
- echo "$line"
- fi
- else
- # Keep non-dev lines
- echo "$line"
- fi
- done <"$LXC_CONFIG" >"${LXC_CONFIG}.new"
-
- mv "${LXC_CONFIG}.new" "$LXC_CONFIG"
-
- # Starte Container wieder
- pct start "$CTID" >/dev/null 2>&1
- sleep 3
-
- msg_ok "Device GIDs updated successfully"
- else
- msg_ok "Device GIDs are already correct"
- fi
+ # For privileged containers: also fix permissions inside container
if [[ "$CT_TYPE" == "0" ]]; then
pct exec "$CTID" -- bash -c "
if [ -d /dev/dri ]; then
@@ -2687,26 +2986,6 @@ fix_gpu_gids() {
fi
}
-# NVIDIA-spezific check on host
-check_nvidia_host_setup() {
- if ! command -v nvidia-smi >/dev/null 2>&1; then
- msg_warn "NVIDIA GPU detected but nvidia-smi not found on host"
- msg_warn "Please install NVIDIA drivers on host first."
- #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run"
- #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms"
- #echo " 3. Verify: nvidia-smi"
- return 1
- fi
-
- # check if nvidia-smi works
- if ! nvidia-smi >/dev/null 2>&1; then
- msg_warn "nvidia-smi installed but not working. Driver issue?"
- return 1
- fi
-
- return 0
-}
-
check_storage_support() {
local CONTENT="$1" VALID=0
while IFS= read -r line; do
@@ -2854,7 +3133,7 @@ create_lxc_container() {
case "${_ans,,}" in
y | yes)
msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
- if apt-get update -qq >/dev/null && apt-get install -y --only-upgrade pve-container lxc-pve >/dev/null; then
+ if $STD apt-get update && $STD apt-get install -y --only-upgrade pve-container lxc-pve; then
msg_ok "LXC stack upgraded."
if [[ "$do_retry" == "yes" ]]; then
msg_info "Retrying container creation after upgrade"
@@ -3011,19 +3290,15 @@ create_lxc_container() {
pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)."
+ msg_ok "Template search completed"
+
#echo "[DEBUG] pveam available output (first 5 lines with .tar files):"
- pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /'
+ #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /'
set +u
mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
#echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found"
set -u
- if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
- #echo "[DEBUG] Online templates:"
- for tmpl in "${ONLINE_TEMPLATES[@]}"; do
- echo " - $tmpl"
- done
- fi
ONLINE_TEMPLATE=""
[[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
@@ -3078,7 +3353,7 @@ create_lxc_container() {
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
- echo "[DEBUG] Retrying with version: $PCT_OSVERSION"
+ #echo "[DEBUG] Retrying with version: $PCT_OSVERSION"
mapfile -t ONLINE_TEMPLATES < <(
pveam available -section system 2>/dev/null |
@@ -3091,13 +3366,13 @@ create_lxc_container() {
if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
TEMPLATE="${ONLINE_TEMPLATES[-1]}"
TEMPLATE_SOURCE="online"
- echo "[DEBUG] Found alternative: $TEMPLATE"
+ #echo "[DEBUG] Found alternative: $TEMPLATE"
else
msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
exit 225
fi
else
- msg_info "Installation cancelled"
+ msg_custom "🚫" "${YW}" "Installation cancelled"
exit 0
fi
else
@@ -3106,8 +3381,8 @@ create_lxc_container() {
fi
fi
- echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
- msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+ #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+ #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
if [[ -z "$TEMPLATE_PATH" ]]; then
@@ -3190,7 +3465,7 @@ create_lxc_container() {
exit 220
}
else
- msg_info "Installation cancelled"
+ msg_custom "🚫" "${YW}" "Installation cancelled"
exit 1
fi
else
@@ -3203,9 +3478,9 @@ create_lxc_container() {
# Validate that we found a template
if [[ -z "$TEMPLATE" ]]; then
msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
- msg_info "Please check:"
- msg_info " - Is pveam catalog available? (run: pveam available -section system)"
- msg_info " - Does the template exist for your OS version?"
+ msg_custom "ℹ️" "${YW}" "Please check:"
+ msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)"
+ msg_custom " •" "${YW}" "Does the template exist for your OS version?"
exit 225
fi
@@ -3243,7 +3518,7 @@ create_lxc_container() {
TEMPLATE="$ONLINE_TEMPLATE"
NEED_DOWNLOAD=1
else
- msg_info "Continuing with local template $TEMPLATE"
+ msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE"
fi
fi
@@ -3303,13 +3578,13 @@ create_lxc_container() {
exit 211
}
- LOGFILE="/tmp/pct_create_${CTID}.log"
+ LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}"
msg_debug "Logfile: $LOGFILE"
# First attempt
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then
- msg_error "Container creation failed on ${TEMPLATE_STORAGE}. Checking template..."
+ msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Validating template..."
# Validate template file
if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
@@ -3328,18 +3603,16 @@ create_lxc_container() {
# Retry after repair
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
- # Fallback to local storage
+ # Fallback to local storage if not already on local
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
- msg_warn "Retrying container creation with fallback to local storage..."
+ msg_info "Retrying container creation with fallback to local storage..."
LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
msg_info "Downloading template to local..."
pveam download local "$TEMPLATE" >/dev/null 2>&1
fi
- if pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
- msg_ok "Container successfully created using local fallback."
- else
- # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ # Local fallback also failed - check for LXC stack version issue
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
echo
echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
@@ -3359,18 +3632,19 @@ create_lxc_container() {
;;
esac
else
- msg_error "Container creation failed even with local fallback. See $LOGFILE"
+ msg_error "Container creation failed. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
- bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" 2>&1 | tee -a "$LOGFILE"
set +x
fi
exit 209
fi
+ else
+ msg_ok "Container successfully created using local fallback."
fi
else
- msg_error "Container creation failed on local storage. See $LOGFILE"
- # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ # Already on local storage and still failed - check LXC stack version
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
echo
echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
@@ -3393,12 +3667,14 @@ create_lxc_container() {
msg_error "Container creation failed. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
- bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" 2>&1 | tee -a "$LOGFILE"
set +x
fi
exit 209
fi
fi
+ else
+ msg_ok "Container successfully created after template repair."
fi
fi
@@ -3415,14 +3691,26 @@ create_lxc_container() {
}
msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
+
+ # Report container creation to API
+ post_to_api
}
+# ==============================================================================
+# SECTION 9: POST-INSTALLATION & FINALIZATION
+# ==============================================================================
+
# ------------------------------------------------------------------------------
# description()
#
-# - Sets container description with HTML content (logo, links, badges)
-# - Restarts ping-instances.service if present
-# - Posts status "done" to API
+# - Sets container description with formatted HTML content
+# - Includes:
+# * Community-Scripts logo
+# * Application name
+# * Links to GitHub, Discussions, Issues
+# * Ko-fi donation badge
+# - Restarts ping-instances.service if present (monitoring)
+# - Posts final "done" status to API telemetry
# ------------------------------------------------------------------------------
description() {
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
@@ -3467,31 +3755,23 @@ EOF
post_update_to_api "done" "none"
}
+# ==============================================================================
+# SECTION 10: ERROR HANDLING & EXIT TRAPS
+# ==============================================================================
+
# ------------------------------------------------------------------------------
# api_exit_script()
#
-# - Exit trap handler
-# - Reports exit codes to API with detailed reason
-# - Handles known codes (100–209) and maps them to errors
+# - Exit trap handler for reporting to API telemetry
+# - Captures exit code and reports to API using centralized error descriptions
+# - Uses explain_exit_code() from error_handler.func for consistent error messages
+# - Posts failure status with exit code to API (error description added automatically)
+# - Only executes on non-zero exit codes
# ------------------------------------------------------------------------------
api_exit_script() {
exit_code=$?
if [ $exit_code -ne 0 ]; then
- case $exit_code in
- 100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;;
- 101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;;
- 200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;;
- 201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;;
- 202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;;
- 203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;;
- 204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;;
- 205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;;
- 206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;;
- 207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;;
- 208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;;
- 209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;;
- *) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;;
- esac
+ post_update_to_api "failed" "$exit_code"
fi
}
diff --git a/misc/cloud-init.sh b/misc/cloud-init.sh
new file mode 100644
index 000000000..c50ae6ff8
--- /dev/null
+++ b/misc/cloud-init.sh
@@ -0,0 +1,364 @@
+#!/usr/bin/env bash
+
+# ==============================================================================
+# Cloud-Init Library - Universal Helper for all Proxmox VM Scripts
+# ==============================================================================
+# Author: community-scripts ORG
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+#
+# Usage:
+# 1. Source this library in your VM script:
+# source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/vm/cloud-init-lib.sh)
+#
+# 2. Call setup_cloud_init with parameters:
+# setup_cloud_init "$VMID" "$STORAGE" "$HN" "$USE_CLOUD_INIT"
+#
+# Compatible with: Debian, Ubuntu, and all Cloud-Init enabled distributions
+# ==============================================================================
+
+# Configuration defaults (can be overridden before sourcing)
+CLOUDINIT_DEFAULT_USER="${CLOUDINIT_DEFAULT_USER:-root}"
+CLOUDINIT_DNS_SERVERS="${CLOUDINIT_DNS_SERVERS:-1.1.1.1 8.8.8.8}"
+CLOUDINIT_SEARCH_DOMAIN="${CLOUDINIT_SEARCH_DOMAIN:-local}"
+CLOUDINIT_SSH_KEYS="${CLOUDINIT_SSH_KEYS:-/root/.ssh/authorized_keys}"
+
+# ==============================================================================
+# Main Setup Function - Configures Proxmox Native Cloud-Init
+# ==============================================================================
+# Parameters:
+# $1 - VMID (required)
+# $2 - Storage name (required)
+# $3 - Hostname (optional, default: vm-)
+# $4 - Enable Cloud-Init (yes/no, default: no)
+# $5 - User (optional, default: root)
+# $6 - Network mode (dhcp/static, default: dhcp)
+# $7 - Static IP (optional, format: 192.168.1.100/24)
+# $8 - Gateway (optional)
+# $9 - Nameservers (optional, default: 1.1.1.1 8.8.8.8)
+#
+# Returns: 0 on success, 1 on failure
+# Exports: CLOUDINIT_USER, CLOUDINIT_PASSWORD, CLOUDINIT_CRED_FILE
+# ==============================================================================
+function setup_cloud_init() {
+ local vmid="$1"
+ local storage="$2"
+ local hostname="${3:-vm-${vmid}}"
+ local enable="${4:-no}"
+ local ciuser="${5:-$CLOUDINIT_DEFAULT_USER}"
+ local network_mode="${6:-dhcp}"
+ local static_ip="${7:-}"
+ local gateway="${8:-}"
+ local nameservers="${9:-$CLOUDINIT_DNS_SERVERS}"
+
+ # Skip if not enabled
+ if [ "$enable" != "yes" ]; then
+ return 0
+ fi
+
+ msg_info "Configuring Cloud-Init" 2>/dev/null || echo "[INFO] Configuring Cloud-Init"
+
+ # Create Cloud-Init drive (try ide2 first, then scsi1 as fallback)
+ if ! qm set "$vmid" --ide2 "${storage}:cloudinit" >/dev/null 2>&1; then
+ qm set "$vmid" --scsi1 "${storage}:cloudinit" >/dev/null 2>&1
+ fi
+
+ # Set user
+ qm set "$vmid" --ciuser "$ciuser" >/dev/null
+
+ # Generate and set secure random password
+ local cipassword=$(openssl rand -base64 16)
+ qm set "$vmid" --cipassword "$cipassword" >/dev/null
+
+ # Add SSH keys if available
+ if [ -f "$CLOUDINIT_SSH_KEYS" ]; then
+ qm set "$vmid" --sshkeys "$CLOUDINIT_SSH_KEYS" >/dev/null 2>&1 || true
+ fi
+
+ # Configure network
+ if [ "$network_mode" = "static" ] && [ -n "$static_ip" ] && [ -n "$gateway" ]; then
+ qm set "$vmid" --ipconfig0 "ip=${static_ip},gw=${gateway}" >/dev/null
+ else
+ qm set "$vmid" --ipconfig0 "ip=dhcp" >/dev/null
+ fi
+
+ # Set DNS servers
+ qm set "$vmid" --nameserver "$nameservers" >/dev/null
+
+ # Set search domain
+ qm set "$vmid" --searchdomain "$CLOUDINIT_SEARCH_DOMAIN" >/dev/null
+
+ # Enable package upgrades on first boot (if supported by Proxmox version)
+ qm set "$vmid" --ciupgrade 1 >/dev/null 2>&1 || true
+
+ # Save credentials to file
+ local cred_file="/tmp/${hostname}-${vmid}-cloud-init-credentials.txt"
+ cat >"$cred_file" <
+
+Proxmox UI Configuration:
+VM ${vmid} > Cloud-Init > Edit
+- User, Password, SSH Keys
+- Network (IP Config)
+- DNS, Search Domain
+========================================
+EOF
+
+ msg_ok "Cloud-Init configured (User: ${ciuser})" 2>/dev/null || echo "[OK] Cloud-Init configured (User: ${ciuser})"
+
+ # Export for use in calling script (DO NOT display password here - will be shown in summary)
+ export CLOUDINIT_USER="$ciuser"
+ export CLOUDINIT_PASSWORD="$cipassword"
+ export CLOUDINIT_CRED_FILE="$cred_file"
+
+ return 0
+}
+
+# ==============================================================================
+# Interactive Cloud-Init Configuration (Whiptail/Dialog)
+# ==============================================================================
+# Prompts user for Cloud-Init configuration choices
+# Returns configuration via exported variables:
+# - CLOUDINIT_ENABLE (yes/no)
+# - CLOUDINIT_USER
+# - CLOUDINIT_NETWORK_MODE (dhcp/static)
+# - CLOUDINIT_IP (if static)
+# - CLOUDINIT_GW (if static)
+# - CLOUDINIT_DNS
+# ==============================================================================
+function configure_cloud_init_interactive() {
+ local default_user="${1:-root}"
+
+ # Check if whiptail is available
+ if ! command -v whiptail >/dev/null 2>&1; then
+ echo "Warning: whiptail not available, skipping interactive configuration"
+ export CLOUDINIT_ENABLE="no"
+ return 1
+ fi
+
+ # Ask if user wants to enable Cloud-Init
+ if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
+ --yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n• User accounts and passwords\n• SSH keys\n• Network settings (DHCP/Static)\n• DNS configuration\n\nYou can also configure these settings later in Proxmox UI." 16 68); then
+ export CLOUDINIT_ENABLE="no"
+ return 0
+ fi
+
+ export CLOUDINIT_ENABLE="yes"
+
+ # Username
+ if CLOUDINIT_USER=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
+ "Cloud-Init Username" 8 58 "$default_user" --title "USERNAME" 3>&1 1>&2 2>&3); then
+ export CLOUDINIT_USER="${CLOUDINIT_USER:-$default_user}"
+ else
+ export CLOUDINIT_USER="$default_user"
+ fi
+
+ # Network configuration
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "NETWORK MODE" \
+ --yesno "Use DHCP for network configuration?\n\nSelect 'No' for static IP configuration." 10 58); then
+ export CLOUDINIT_NETWORK_MODE="dhcp"
+ else
+ export CLOUDINIT_NETWORK_MODE="static"
+
+ # Static IP
+ if CLOUDINIT_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
+ "Static IP Address (CIDR format)\nExample: 192.168.1.100/24" 9 58 "" --title "IP ADDRESS" 3>&1 1>&2 2>&3); then
+ export CLOUDINIT_IP
+ else
+ echo "Error: Static IP required for static network mode"
+ export CLOUDINIT_NETWORK_MODE="dhcp"
+ fi
+
+ # Gateway
+ if [ "$CLOUDINIT_NETWORK_MODE" = "static" ]; then
+ if CLOUDINIT_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
+ "Gateway IP Address\nExample: 192.168.1.1" 8 58 "" --title "GATEWAY" 3>&1 1>&2 2>&3); then
+ export CLOUDINIT_GW
+ else
+ echo "Error: Gateway required for static network mode"
+ export CLOUDINIT_NETWORK_MODE="dhcp"
+ fi
+ fi
+ fi
+
+ # DNS Servers
+ if CLOUDINIT_DNS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
+ "DNS Servers (space-separated)" 8 58 "1.1.1.1 8.8.8.8" --title "DNS SERVERS" 3>&1 1>&2 2>&3); then
+ export CLOUDINIT_DNS="${CLOUDINIT_DNS:-1.1.1.1 8.8.8.8}"
+ else
+ export CLOUDINIT_DNS="1.1.1.1 8.8.8.8"
+ fi
+
+ return 0
+}
+
+# ==============================================================================
+# Display Cloud-Init Summary Information
+# ==============================================================================
+function display_cloud_init_info() {
+ local vmid="$1"
+ local hostname="${2:-}"
+
+ if [ -n "$CLOUDINIT_CRED_FILE" ] && [ -f "$CLOUDINIT_CRED_FILE" ]; then
+ if [ -n "${INFO:-}" ]; then
+ echo -e "\n${INFO}${BOLD:-}${GN:-} Cloud-Init Configuration:${CL:-}"
+ echo -e "${TAB:- }${DGN:-}User: ${BGN:-}${CLOUDINIT_USER:-root}${CL:-}"
+ echo -e "${TAB:- }${DGN:-}Password: ${BGN:-}${CLOUDINIT_PASSWORD}${CL:-}"
+ echo -e "${TAB:- }${DGN:-}Credentials: ${BL:-}${CLOUDINIT_CRED_FILE}${CL:-}"
+ echo -e "${TAB:- }${YW:-}💡 You can configure Cloud-Init settings in Proxmox UI:${CL:-}"
+ echo -e "${TAB:- }${YW:-} VM ${vmid} > Cloud-Init > Edit (User, Password, SSH Keys, Network)${CL:-}"
+ else
+ echo ""
+ echo "[INFO] Cloud-Init Configuration:"
+ echo " User: ${CLOUDINIT_USER:-root}"
+ echo " Password: ${CLOUDINIT_PASSWORD}"
+ echo " Credentials: ${CLOUDINIT_CRED_FILE}"
+ echo " You can configure Cloud-Init settings in Proxmox UI:"
+ echo " VM ${vmid} > Cloud-Init > Edit"
+ fi
+ fi
+}
+
+# ==============================================================================
+# Check if VM has Cloud-Init configured
+# ==============================================================================
+function has_cloud_init() {
+ local vmid="$1"
+ qm config "$vmid" 2>/dev/null | grep -qE "(ide2|scsi1):.*cloudinit"
+}
+
+# ==============================================================================
+# Regenerate Cloud-Init configuration
+# ==============================================================================
+function regenerate_cloud_init() {
+ local vmid="$1"
+
+ if has_cloud_init "$vmid"; then
+ msg_info "Regenerating Cloud-Init configuration" 2>/dev/null || echo "[INFO] Regenerating Cloud-Init"
+ qm cloudinit update "$vmid" >/dev/null 2>&1 || true
+ msg_ok "Cloud-Init configuration regenerated" 2>/dev/null || echo "[OK] Cloud-Init regenerated"
+ return 0
+ else
+ echo "Warning: VM $vmid does not have Cloud-Init configured"
+ return 1
+ fi
+}
+
+# ==============================================================================
+# Get VM IP address via qemu-guest-agent
+# ==============================================================================
+function get_vm_ip() {
+ local vmid="$1"
+ local timeout="${2:-30}"
+
+ local elapsed=0
+ while [ $elapsed -lt $timeout ]; do
+ local vm_ip=$(qm guest cmd "$vmid" network-get-interfaces 2>/dev/null |
+ jq -r '.[] | select(.name != "lo") | ."ip-addresses"[]? | select(."ip-address-type" == "ipv4") | ."ip-address"' 2>/dev/null | head -1)
+
+ if [ -n "$vm_ip" ]; then
+ echo "$vm_ip"
+ return 0
+ fi
+
+ sleep 2
+ elapsed=$((elapsed + 2))
+ done
+
+ return 1
+}
+
+# ==============================================================================
+# Wait for Cloud-Init to complete (requires SSH access)
+# ==============================================================================
+function wait_for_cloud_init() {
+ local vmid="$1"
+ local timeout="${2:-300}"
+ local vm_ip="${3:-}"
+
+ # Get IP if not provided
+ if [ -z "$vm_ip" ]; then
+ vm_ip=$(get_vm_ip "$vmid" 60)
+ fi
+
+ if [ -z "$vm_ip" ]; then
+ echo "Warning: Unable to determine VM IP address"
+ return 1
+ fi
+
+ msg_info "Waiting for Cloud-Init to complete on ${vm_ip}" 2>/dev/null || echo "[INFO] Waiting for Cloud-Init on ${vm_ip}"
+
+ local elapsed=0
+ while [ $elapsed -lt $timeout ]; do
+ if timeout 10 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
+ "${CLOUDINIT_USER:-root}@${vm_ip}" "cloud-init status --wait" 2>/dev/null; then
+ msg_ok "Cloud-Init completed successfully" 2>/dev/null || echo "[OK] Cloud-Init completed"
+ return 0
+ fi
+ sleep 10
+ elapsed=$((elapsed + 10))
+ done
+
+ echo "Warning: Cloud-Init did not complete within ${timeout}s"
+ return 1
+}
+
+# ==============================================================================
+# Export all functions for use in other scripts
+# ==============================================================================
+export -f setup_cloud_init 2>/dev/null || true
+export -f configure_cloud_init_interactive 2>/dev/null || true
+export -f display_cloud_init_info 2>/dev/null || true
+export -f has_cloud_init 2>/dev/null || true
+export -f regenerate_cloud_init 2>/dev/null || true
+export -f get_vm_ip 2>/dev/null || true
+export -f wait_for_cloud_init 2>/dev/null || true
+
+# ==============================================================================
+# Quick Start Examples
+# ==============================================================================
+: <<'EXAMPLES'
+
+# Example 1: Simple DHCP setup (most common)
+setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
+
+# Example 2: Static IP setup
+setup_cloud_init "$VMID" "$STORAGE" "myserver" "yes" "root" "static" "192.168.1.100/24" "192.168.1.1"
+
+# Example 3: Interactive configuration in advanced_settings()
+configure_cloud_init_interactive "admin"
+if [ "$CLOUDINIT_ENABLE" = "yes" ]; then
+ setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" "$CLOUDINIT_USER" \
+ "$CLOUDINIT_NETWORK_MODE" "$CLOUDINIT_IP" "$CLOUDINIT_GW" "$CLOUDINIT_DNS"
+fi
+
+# Example 4: Display info after VM creation
+display_cloud_init_info "$VMID" "$HN"
+
+# Example 5: Check if VM has Cloud-Init
+if has_cloud_init "$VMID"; then
+ echo "Cloud-Init is configured"
+fi
+
+# Example 6: Wait for Cloud-Init to complete after VM start
+if [ "$START_VM" = "yes" ]; then
+ qm start "$VMID"
+ sleep 30
+ wait_for_cloud_init "$VMID" 300
+fi
+
+EXAMPLES
diff --git a/misc/core.func b/misc/core.func
index c2c1ea0c6..6f50773b8 100644
--- a/misc/core.func
+++ b/misc/core.func
@@ -2,13 +2,34 @@
# Copyright (c) 2021-2025 community-scripts ORG
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/LICENSE
-# ------------------------------------------------------------------------------
-# Loads core utility groups once (colors, formatting, icons, defaults).
-# ------------------------------------------------------------------------------
+# ==============================================================================
+# CORE FUNCTIONS - LXC CONTAINER UTILITIES
+# ==============================================================================
+#
+# This file provides core utility functions for LXC container management
+# including colors, formatting, validation checks, message output, and
+# execution helpers used throughout the Community-Scripts ecosystem.
+#
+# Usage:
+# source <(curl -fsSL https://git.community-scripts.org/.../core.func)
+# load_functions
+#
+# ==============================================================================
[[ -n "${_CORE_FUNC_LOADED:-}" ]] && return
_CORE_FUNC_LOADED=1
+# ==============================================================================
+# SECTION 1: INITIALIZATION & SETUP
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# load_functions()
+#
+# - Initializes all core utility groups (colors, formatting, icons, defaults)
+# - Ensures functions are loaded only once via __FUNCTIONS_LOADED flag
+# - Must be called at start of any script using these utilities
+# ------------------------------------------------------------------------------
load_functions() {
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
__FUNCTIONS_LOADED=1
@@ -17,11 +38,14 @@ load_functions() {
icons
default_vars
set_std_mode
- # add more
}
# ------------------------------------------------------------------------------
-# Sets ANSI color codes used for styled terminal output.
+# color()
+#
+# - Sets ANSI color codes for styled terminal output
+# - Variables: YW (yellow), YWB (yellow bright), BL (blue), RD (red)
+# GN (green), DGN (dark green), BGN (background green), CL (clear)
# ------------------------------------------------------------------------------
color() {
YW=$(echo "\033[33m")
@@ -34,7 +58,14 @@ color() {
CL=$(echo "\033[m")
}
-# Special for spinner and colorized output via printf
+# ------------------------------------------------------------------------------
+# color_spinner()
+#
+# - Sets ANSI color codes specifically for spinner animation
+# - Variables: CS_YW (spinner yellow), CS_YWB (spinner yellow bright),
+# CS_CL (spinner clear)
+# - Used by spinner() function to avoid color conflicts
+# ------------------------------------------------------------------------------
color_spinner() {
CS_YW=$'\033[33m'
CS_YWB=$'\033[93m'
@@ -42,7 +73,12 @@ color_spinner() {
}
# ------------------------------------------------------------------------------
-# Defines formatting helpers like tab, bold, and line reset sequences.
+# formatting()
+#
+# - Defines formatting helpers for terminal output
+# - BFR: Backspace and clear line sequence
+# - BOLD: Bold text escape code
+# - TAB/TAB3: Indentation spacing
# ------------------------------------------------------------------------------
formatting() {
BFR="\\r\\033[K"
@@ -53,7 +89,11 @@ formatting() {
}
# ------------------------------------------------------------------------------
-# Sets symbolic icons used throughout user feedback and prompts.
+# icons()
+#
+# - Sets symbolic emoji icons used throughout user feedback
+# - Provides consistent visual indicators for success, error, info, etc.
+# - Icons: CM (checkmark), CROSS (error), INFO (info), HOURGLASS (wait), etc.
# ------------------------------------------------------------------------------
icons() {
CM="${TAB}✔️${TAB}"
@@ -84,21 +124,29 @@ icons() {
ADVANCED="${TAB}🧩${TAB}${CL}"
FUSE="${TAB}🗂️${TAB}${CL}"
HOURGLASS="${TAB}⏳${TAB}"
-
}
# ------------------------------------------------------------------------------
-# Sets default retry and wait variables used for system actions.
+# default_vars()
+#
+# - Sets default retry and wait variables used for system actions
+# - RETRY_NUM: Maximum number of retry attempts (default: 10)
+# - RETRY_EVERY: Seconds to wait between retries (default: 3)
+# - i: Counter variable initialized to RETRY_NUM
# ------------------------------------------------------------------------------
default_vars() {
RETRY_NUM=10
RETRY_EVERY=3
i=$RETRY_NUM
- #[[ "${VAR_OS:-}" == "unknown" ]]
}
# ------------------------------------------------------------------------------
-# Sets default verbose mode for script and os execution.
+# set_std_mode()
+#
+# - Sets default verbose mode for script and OS execution
+# - If VERBOSE=yes: STD="" (show all output)
+# - If VERBOSE=no: STD="silent" (suppress output via silent() wrapper)
+# - If DEV_MODE_TRACE=true: Enables bash tracing (set -x)
# ------------------------------------------------------------------------------
set_std_mode() {
if [ "${VERBOSE:-no}" = "yes" ]; then
@@ -106,53 +154,93 @@ set_std_mode() {
else
STD="silent"
fi
-}
-SILENT_LOGFILE="/tmp/silent.$$.log"
-
-silent() {
- local cmd="$*"
- local caller_line="${BASH_LINENO[0]:-unknown}"
-
- set +Eeuo pipefail
- trap - ERR
-
- "$@" >>"$SILENT_LOGFILE" 2>&1
- local rc=$?
-
- set -Eeuo pipefail
- trap 'error_handler' ERR
-
- if [[ $rc -ne 0 ]]; then
- # Source explain_exit_code if needed
- if ! declare -f explain_exit_code >/dev/null 2>&1; then
- source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
- fi
-
- local explanation
- explanation="$(explain_exit_code "$rc")"
-
- printf "\e[?25h"
- echo -e "\n${RD}[ERROR]${CL} in line ${RD}${caller_line}${CL}: exit code ${RD}${rc}${CL} (${explanation})"
- echo -e "${RD}Command:${CL} ${YWB}${cmd}${CL}\n"
-
- if [[ -s "$SILENT_LOGFILE" ]]; then
- local log_lines=$(wc -l <"$SILENT_LOGFILE")
- echo "--- Last 10 lines of silent log ---"
- tail -n 10 "$SILENT_LOGFILE"
- echo "-----------------------------------"
-
- # Show how to view full log if there are more lines
- if [[ $log_lines -gt 10 ]]; then
- echo -e "${YW}View full log (${log_lines} lines):${CL} cat $SILENT_LOGFILE"
- fi
- fi
-
- exit "$rc"
+ # Enable bash tracing if trace mode active
+ if [[ "${DEV_MODE_TRACE:-false}" == "true" ]]; then
+ set -x
+ export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
fi
}
-# Check if the shell is using bash
+# ------------------------------------------------------------------------------
+# parse_dev_mode()
+#
+# - Parses comma-separated dev_mode variable (e.g., "motd,keep,trace")
+# - Sets global flags for each mode:
+# * DEV_MODE_MOTD: Setup SSH/MOTD before installation
+# * DEV_MODE_KEEP: Never delete container on failure
+# * DEV_MODE_TRACE: Enable bash set -x tracing
+# * DEV_MODE_PAUSE: Pause after each msg_info step
+# * DEV_MODE_BREAKPOINT: Open shell on error instead of cleanup
+# * DEV_MODE_LOGS: Persist all logs to /var/log/community-scripts/
+# * DEV_MODE_DRYRUN: Show commands without executing
+# - Call this early in script execution
+# ------------------------------------------------------------------------------
+parse_dev_mode() {
+ local mode
+ # Initialize all flags to false
+ export DEV_MODE_MOTD=false
+ export DEV_MODE_KEEP=false
+ export DEV_MODE_TRACE=false
+ export DEV_MODE_PAUSE=false
+ export DEV_MODE_BREAKPOINT=false
+ export DEV_MODE_LOGS=false
+ export DEV_MODE_DRYRUN=false
+
+ # Parse comma-separated modes
+ if [[ -n "${dev_mode:-}" ]]; then
+ IFS=',' read -ra MODES <<<"$dev_mode"
+ for mode in "${MODES[@]}"; do
+ mode="$(echo "$mode" | xargs)" # Trim whitespace
+ case "$mode" in
+ motd) export DEV_MODE_MOTD=true ;;
+ keep) export DEV_MODE_KEEP=true ;;
+ trace) export DEV_MODE_TRACE=true ;;
+ pause) export DEV_MODE_PAUSE=true ;;
+ breakpoint) export DEV_MODE_BREAKPOINT=true ;;
+ logs) export DEV_MODE_LOGS=true ;;
+ dryrun) export DEV_MODE_DRYRUN=true ;;
+ *)
+ if declare -f msg_warn >/dev/null 2>&1; then
+ msg_warn "Unknown dev_mode: '$mode' (ignored)"
+ else
+ echo "[WARN] Unknown dev_mode: '$mode' (ignored)" >&2
+ fi
+ ;;
+ esac
+ done
+
+ # Show active dev modes
+ local active_modes=()
+ [[ $DEV_MODE_MOTD == true ]] && active_modes+=("motd")
+ [[ $DEV_MODE_KEEP == true ]] && active_modes+=("keep")
+ [[ $DEV_MODE_TRACE == true ]] && active_modes+=("trace")
+ [[ $DEV_MODE_PAUSE == true ]] && active_modes+=("pause")
+ [[ $DEV_MODE_BREAKPOINT == true ]] && active_modes+=("breakpoint")
+ [[ $DEV_MODE_LOGS == true ]] && active_modes+=("logs")
+ [[ $DEV_MODE_DRYRUN == true ]] && active_modes+=("dryrun")
+
+ if [[ ${#active_modes[@]} -gt 0 ]]; then
+ if declare -f msg_custom >/dev/null 2>&1; then
+ msg_custom "🔧" "${YWB}" "Dev modes active: ${active_modes[*]}"
+ else
+ echo "[DEV] Active modes: ${active_modes[*]}" >&2
+ fi
+ fi
+ fi
+}
+
+# ==============================================================================
+# SECTION 2: VALIDATION CHECKS
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# shell_check()
+#
+# - Verifies that the script is running under Bash shell
+# - Exits with error message if different shell is detected
+# - Required because scripts use Bash-specific features
+# ------------------------------------------------------------------------------
shell_check() {
if [[ "$(ps -p $$ -o comm=)" != "bash" ]]; then
clear
@@ -163,7 +251,13 @@ shell_check() {
fi
}
-# Run as root only
+# ------------------------------------------------------------------------------
+# root_check()
+#
+# - Verifies script is running with root privileges
+# - Detects if executed via sudo (which can cause issues)
+# - Exits with error if not running as root directly
+# ------------------------------------------------------------------------------
root_check() {
if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
clear
@@ -174,8 +268,13 @@ root_check() {
fi
}
-# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
-# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 (NOT 9.1+)
+# ------------------------------------------------------------------------------
+# pve_check()
+#
+# - Validates Proxmox VE version compatibility
+# - Supported: PVE 8.0-8.9 and PVE 9.0-9.1
+# - Exits with error message if unsupported version detected
+# ------------------------------------------------------------------------------
pve_check() {
local PVE_VER
PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
@@ -191,12 +290,12 @@ pve_check() {
return 0
fi
- # Check for Proxmox VE 9.x: allow ONLY 9.0
+ # Check for Proxmox VE 9.x: allow 9.0–9.1
if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
local MINOR="${BASH_REMATCH[1]}"
- if ((MINOR != 0)); then
+ if ((MINOR < 0 || MINOR > 1)); then
msg_error "This version of Proxmox VE is not yet supported."
- msg_error "Supported: Proxmox VE version 9.0"
+ msg_error "Supported: Proxmox VE version 9.0 – 9.1"
exit 1
fi
return 0
@@ -204,11 +303,17 @@ pve_check() {
# All other unsupported versions
msg_error "This version of Proxmox VE is not supported."
- msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0"
+ msg_error "Supported versions: Proxmox VE 8.0 – 8.9 or 9.0 – 9.1"
exit 1
}
-# This function checks the system architecture and exits if it's not "amd64".
+# ------------------------------------------------------------------------------
+# arch_check()
+#
+# - Validates system architecture is amd64/x86_64
+# - Exits with error message for unsupported architectures (e.g., ARM/PiMox)
+# - Provides link to ARM64-compatible scripts
+# ------------------------------------------------------------------------------
arch_check() {
if [ "$(dpkg --print-architecture)" != "amd64" ]; then
echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
@@ -222,111 +327,165 @@ arch_check() {
# ------------------------------------------------------------------------------
# ssh_check()
#
-# - Detects if script is running over SSH
-# - Warns user and recommends using Proxmox shell
-# - User can choose to continue or abort
+# - Detects if script is running over SSH connection
+# - Warns user for external SSH connections (recommends Proxmox shell)
+# - Skips warning for local/same-subnet connections
+# - Does not abort execution, only warns
# ------------------------------------------------------------------------------
ssh_check() {
if [ -n "$SSH_CLIENT" ]; then
local client_ip=$(awk '{print $1}' <<<"$SSH_CLIENT")
local host_ip=$(hostname -I | awk '{print $1}')
- if [[ "$client_ip" == "127.0.0.1" || "$client_ip" == "$host_ip" ]]; then
+ # Check if connection is local (Proxmox WebUI or same machine)
+ # - localhost (127.0.0.1, ::1)
+ # - same IP as host
+ # - local network range (10.x, 172.16-31.x, 192.168.x)
+ if [[ "$client_ip" == "127.0.0.1" || "$client_ip" == "::1" || "$client_ip" == "$host_ip" ]]; then
return
fi
+ # Check if client is in same local network (optional, safer approach)
+ local host_subnet=$(echo "$host_ip" | cut -d. -f1-3)
+ local client_subnet=$(echo "$client_ip" | cut -d. -f1-3)
+ if [[ "$host_subnet" == "$client_subnet" ]]; then
+ return
+ fi
+
+ # Only warn for truly external connections
msg_warn "Running via external SSH (client: $client_ip)."
+ msg_warn "For better stability, consider using the Proxmox Shell (Console) instead."
fi
}
-# Function to download & save header files
-get_header() {
- local app_name=$(echo "${APP,,}" | tr -d ' ')
- local app_type=${APP_TYPE:-ct} # Default zu 'ct' falls nicht gesetzt
- local header_url="https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/${app_type}/headers/${app_name}"
- local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}"
+# ==============================================================================
+# SECTION 3: EXECUTION HELPERS
+# ==============================================================================
- mkdir -p "$(dirname "$local_header_path")"
-
- if [ ! -s "$local_header_path" ]; then
- if ! curl -fsSL "$header_url" -o "$local_header_path"; then
- return 1
- fi
- fi
-
- cat "$local_header_path" 2>/dev/null || true
-}
-
-header_info() {
- local app_name=$(echo "${APP,,}" | tr -d ' ')
- local header_content
-
- header_content=$(get_header "$app_name") || header_content=""
-
- clear
- local term_width
- term_width=$(tput cols 2>/dev/null || echo 120)
-
- if [ -n "$header_content" ]; then
- echo "$header_content"
- fi
-}
-
-ensure_tput() {
- if ! command -v tput >/dev/null 2>&1; then
- if grep -qi 'alpine' /etc/os-release; then
- apk add --no-cache ncurses >/dev/null 2>&1
- elif command -v apt-get >/dev/null 2>&1; then
- apt-get update -qq >/dev/null
- apt-get install -y -qq ncurses-bin >/dev/null 2>&1
- fi
- fi
-}
-
-is_alpine() {
- local os_id="${var_os:-${PCT_OSTYPE:-}}"
-
- if [[ -z "$os_id" && -f /etc/os-release ]]; then
- os_id="$(
- . /etc/os-release 2>/dev/null
- echo "${ID:-}"
- )"
- fi
-
- [[ "$os_id" == "alpine" ]]
-}
-
-is_verbose_mode() {
- local verbose="${VERBOSE:-${var_verbose:-no}}"
- local tty_status
- if [[ -t 2 ]]; then
- tty_status="interactive"
+# ------------------------------------------------------------------------------
+# get_active_logfile()
+#
+# - Returns the appropriate log file based on execution context
+# - BUILD_LOG: Host operations (container creation)
+# - INSTALL_LOG: Container operations (application installation)
+# - Fallback to BUILD_LOG if neither is set
+# ------------------------------------------------------------------------------
+get_active_logfile() {
+ if [[ -n "${INSTALL_LOG:-}" ]]; then
+ echo "$INSTALL_LOG"
+ elif [[ -n "${BUILD_LOG:-}" ]]; then
+ echo "$BUILD_LOG"
else
- tty_status="not-a-tty"
+ # Fallback for legacy scripts
+ echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log"
fi
- [[ "$verbose" != "no" || ! -t 2 ]]
}
-fatal() {
- msg_error "$1"
- kill -INT $$
+# Legacy compatibility: SILENT_LOGFILE points to active log
+SILENT_LOGFILE="$(get_active_logfile)"
+
+# ------------------------------------------------------------------------------
+# silent()
+#
+# - Executes command with output redirected to active log file
+# - On error: displays last 10 lines of log and exits with original exit code
+# - Temporarily disables error trap to capture exit code correctly
+# - Sources explain_exit_code() for detailed error messages
+# ------------------------------------------------------------------------------
+silent() {
+ local cmd="$*"
+ local caller_line="${BASH_LINENO[0]:-unknown}"
+ local logfile="$(get_active_logfile)"
+
+ # Dryrun mode: Show command without executing
+ if [[ "${DEV_MODE_DRYRUN:-false}" == "true" ]]; then
+ if declare -f msg_custom >/dev/null 2>&1; then
+ msg_custom "🔍" "${BL}" "[DRYRUN] $cmd"
+ else
+ echo "[DRYRUN] $cmd" >&2
+ fi
+ return 0
+ fi
+
+ set +Eeuo pipefail
+ trap - ERR
+
+ "$@" >>"$logfile" 2>&1
+ local rc=$?
+
+ set -Eeuo pipefail
+ trap 'error_handler' ERR
+
+ if [[ $rc -ne 0 ]]; then
+ # Source explain_exit_code if needed
+ if ! declare -f explain_exit_code >/dev/null 2>&1; then
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ fi
+
+ local explanation
+ explanation="$(explain_exit_code "$rc")"
+
+ printf "\e[?25h"
+ msg_error "in line ${caller_line}: exit code ${rc} (${explanation})"
+ msg_custom "→" "${YWB}" "${cmd}"
+
+ if [[ -s "$logfile" ]]; then
+ local log_lines=$(wc -l <"$logfile")
+ echo "--- Last 10 lines of silent log ---"
+ tail -n 10 "$logfile"
+ echo "-----------------------------------"
+
+ # Show how to view full log if there are more lines
+ if [[ $log_lines -gt 10 ]]; then
+ msg_custom "📋" "${YW}" "View full log (${log_lines} lines): ${logfile}"
+ fi
+ fi
+
+ exit "$rc"
+ fi
}
+# ------------------------------------------------------------------------------
+# spinner()
+#
+# - Displays animated spinner with rotating characters (⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
+# - Shows SPINNER_MSG alongside animation
+# - Runs in infinite loop until killed by stop_spinner()
+# - Uses color_spinner() colors for output
+# ------------------------------------------------------------------------------
spinner() {
local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
+ local msg="${SPINNER_MSG:-Processing...}"
local i=0
while true; do
local index=$((i++ % ${#chars[@]}))
- printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${SPINNER_MSG:-}${CS_CL}"
+ printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${msg}${CS_CL}"
sleep 0.1
done
}
+# ------------------------------------------------------------------------------
+# clear_line()
+#
+# - Clears current terminal line using tput or ANSI escape codes
+# - Moves cursor to beginning of line (carriage return)
+# - Erases from cursor to end of line
+# - Fallback to ANSI codes if tput not available
+# ------------------------------------------------------------------------------
clear_line() {
tput cr 2>/dev/null || echo -en "\r"
tput el 2>/dev/null || echo -en "\033[K"
}
+# ------------------------------------------------------------------------------
+# stop_spinner()
+#
+# - Stops running spinner process by PID
+# - Reads PID from SPINNER_PID variable or /tmp/.spinner.pid file
+# - Attempts graceful kill, then forced kill if needed
+# - Cleans up temp file and resets terminal state
+# - Unsets SPINNER_PID and SPINNER_MSG variables
+# ------------------------------------------------------------------------------
stop_spinner() {
local pid="${SPINNER_PID:-}"
[[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(/dev/null || true
}
+# ==============================================================================
+# SECTION 4: MESSAGE OUTPUT
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# msg_info()
+#
+# - Displays informational message with spinner animation
+# - Shows each unique message only once (tracked via MSG_INFO_SHOWN)
+# - In verbose/Alpine mode: shows hourglass icon instead of spinner
+# - Stops any existing spinner before starting new one
+# - Backgrounds spinner process and stores PID for later cleanup
+# ------------------------------------------------------------------------------
msg_info() {
local msg="$1"
[[ -z "$msg" ]] && return
@@ -360,6 +532,12 @@ msg_info() {
if is_verbose_mode || is_alpine; then
local HOURGLASS="${TAB}⏳${TAB}"
printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2
+
+ # Pause mode: Wait for Enter after each step
+ if [[ "${DEV_MODE_PAUSE:-false}" == "true" ]]; then
+ echo -en "\n${YWB}[PAUSE]${CL} Press Enter to continue..." >&2
+ read -r
+ fi
return
fi
@@ -368,29 +546,68 @@ msg_info() {
SPINNER_PID=$!
echo "$SPINNER_PID" >/tmp/.spinner.pid
disown "$SPINNER_PID" 2>/dev/null || true
+
+ # Pause mode: Stop spinner and wait
+ if [[ "${DEV_MODE_PAUSE:-false}" == "true" ]]; then
+ stop_spinner
+ echo -en "\n${YWB}[PAUSE]${CL} Press Enter to continue..." >&2
+ read -r
+ fi
}
+# ------------------------------------------------------------------------------
+# msg_ok()
+#
+# - Displays success message with checkmark icon
+# - Stops spinner and clears line before output
+# - Removes message from MSG_INFO_SHOWN to allow re-display
+# - Uses green color for success indication
+# ------------------------------------------------------------------------------
msg_ok() {
local msg="$1"
[[ -z "$msg" ]] && return
stop_spinner
clear_line
- printf "%s %b\n" "$CM" "${GN}${msg}${CL}" >&2
+ echo -e "$CM ${GN}${msg}${CL}"
unset MSG_INFO_SHOWN["$msg"]
}
+# ------------------------------------------------------------------------------
+# msg_error()
+#
+# - Displays error message with cross/X icon
+# - Stops spinner before output
+# - Uses red color for error indication
+# - Outputs to stderr
+# ------------------------------------------------------------------------------
msg_error() {
stop_spinner
local msg="$1"
- echo -e "${BFR:-} ${CROSS:-✖️} ${RD}${msg}${CL}"
+ echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2
}
+# ------------------------------------------------------------------------------
+# msg_warn()
+#
+# - Displays warning message with info/lightbulb icon
+# - Stops spinner before output
+# - Uses bright yellow color for warning indication
+# - Outputs to stderr
+# ------------------------------------------------------------------------------
msg_warn() {
stop_spinner
local msg="$1"
- echo -e "${BFR:-} ${INFO:-ℹ️} ${YWB}${msg}${CL}"
+ echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2
}
+# ------------------------------------------------------------------------------
+# msg_custom()
+#
+# - Displays custom message with user-defined symbol and color
+# - Arguments: symbol, color code, message text
+# - Stops spinner before output
+# - Useful for specialized status messages
+# ------------------------------------------------------------------------------
msg_custom() {
local symbol="${1:-"[*]"}"
local color="${2:-"\e[36m"}"
@@ -400,13 +617,240 @@ msg_custom() {
echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}"
}
-function msg_debug() {
+# ------------------------------------------------------------------------------
+# msg_debug()
+#
+# - Displays debug message with timestamp when var_full_verbose=1
+# - Automatically enables var_verbose if not already set
+# - Shows date/time prefix for log correlation
+# - Uses bright yellow color for debug output
+# ------------------------------------------------------------------------------
+msg_debug() {
if [[ "${var_full_verbose:-0}" == "1" ]]; then
[[ "${var_verbose:-0}" != "1" ]] && var_verbose=1
echo -e "${YWB}[$(date '+%F %T')] [DEBUG]${CL} $*"
fi
}
+# ------------------------------------------------------------------------------
+# msg_dev()
+#
+# - Display development mode messages with 🔧 icon
+# - Only shown when dev_mode is active
+# - Useful for debugging and development-specific output
+# - Format: [DEV] message with distinct formatting
+# - Usage: msg_dev "Container ready for debugging"
+# ------------------------------------------------------------------------------
+msg_dev() {
+ if [[ -n "${dev_mode:-}" ]]; then
+ echo -e "${SEARCH}${BOLD}${DGN}🔧 [DEV]${CL} $*"
+ fi
+}
+#
+# - Displays error message and immediately terminates script
+# - Sends SIGINT to current process to trigger error handler
+# - Use for unrecoverable errors that require immediate exit
+# ------------------------------------------------------------------------------
+fatal() {
+ msg_error "$1"
+ kill -INT $$
+}
+
+# ==============================================================================
+# SECTION 5: UTILITY FUNCTIONS
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# exit_script()
+#
+# - Called when user cancels an action
+# - Clears screen and displays exit message
+# - Exits with default exit code
+# ------------------------------------------------------------------------------
+exit_script() {
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
+}
+
+# ------------------------------------------------------------------------------
+# get_header()
+#
+# - Downloads and caches application header ASCII art
+# - Falls back to local cache if already downloaded
+# - Determines app type (ct/vm) from APP_TYPE variable
+# - Returns header content or empty string on failure
+# ------------------------------------------------------------------------------
+get_header() {
+ local app_name=$(echo "${APP,,}" | tr -d ' ')
+ local app_type=${APP_TYPE:-ct} # Default zu 'ct' falls nicht gesetzt
+ local header_url="https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/${app_type}/headers/${app_name}"
+ local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}"
+
+ mkdir -p "$(dirname "$local_header_path")"
+
+ if [ ! -s "$local_header_path" ]; then
+ if ! curl -fsSL "$header_url" -o "$local_header_path"; then
+ return 1
+ fi
+ fi
+
+ cat "$local_header_path" 2>/dev/null || true
+}
+
+# ------------------------------------------------------------------------------
+# header_info()
+#
+# - Displays application header ASCII art at top of screen
+# - Clears screen before displaying header
+# - Detects terminal width for formatting
+# - Returns silently if header not available
+# ------------------------------------------------------------------------------
+header_info() {
+ local app_name=$(echo "${APP,,}" | tr -d ' ')
+ local header_content
+
+ header_content=$(get_header "$app_name") || header_content=""
+
+ clear
+ local term_width
+ term_width=$(tput cols 2>/dev/null || echo 120)
+
+ if [ -n "$header_content" ]; then
+ echo "$header_content"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# ensure_tput()
+#
+# - Ensures tput command is available for terminal control
+# - Installs ncurses-bin on Debian/Ubuntu or ncurses on Alpine
+# - Required for clear_line() and terminal width detection
+# ------------------------------------------------------------------------------
+ensure_tput() {
+ if ! command -v tput >/dev/null 2>&1; then
+ if grep -qi 'alpine' /etc/os-release; then
+ apk add --no-cache ncurses >/dev/null 2>&1
+ elif command -v apt-get >/dev/null 2>&1; then
+ apt-get update -qq >/dev/null
+ apt-get install -y -qq ncurses-bin >/dev/null 2>&1
+ fi
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# is_alpine()
+#
+# - Detects if running on Alpine Linux
+# - Checks var_os, PCT_OSTYPE, or /etc/os-release
+# - Returns 0 if Alpine, 1 otherwise
+# - Used to adjust behavior for Alpine-specific commands
+# ------------------------------------------------------------------------------
+is_alpine() {
+ local os_id="${var_os:-${PCT_OSTYPE:-}}"
+
+ if [[ -z "$os_id" && -f /etc/os-release ]]; then
+ os_id="$(
+ . /etc/os-release 2>/dev/null
+ echo "${ID:-}"
+ )"
+ fi
+
+ [[ "$os_id" == "alpine" ]]
+}
+
+# ------------------------------------------------------------------------------
+# is_verbose_mode()
+#
+# - Determines if script should run in verbose mode
+# - Checks VERBOSE and var_verbose variables
+# - Also returns true if not running in TTY (pipe/redirect scenario)
+# - Used by msg_info() to decide between spinner and static output
+# ------------------------------------------------------------------------------
+is_verbose_mode() {
+ local verbose="${VERBOSE:-${var_verbose:-no}}"
+ local tty_status
+ if [[ -t 2 ]]; then
+ tty_status="interactive"
+ else
+ tty_status="not-a-tty"
+ fi
+ [[ "$verbose" != "no" || ! -t 2 ]]
+}
+
+# ==============================================================================
+# SECTION 6: CLEANUP & MAINTENANCE
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# cleanup_lxc()
+#
+# - Comprehensive cleanup of package managers, caches, and logs
+# - Supports Alpine (apk), Debian/Ubuntu (apt), and language package managers
+# - Cleans: Python (pip/uv), Node.js (npm/yarn/pnpm), Go, Rust, Ruby, PHP
+# - Truncates log files and vacuums systemd journal
+# - Run at end of container creation to minimize disk usage
+# ------------------------------------------------------------------------------
+cleanup_lxc() {
+ msg_info "Cleaning up"
+
+ if is_alpine; then
+ $STD apk cache clean || true
+ rm -rf /var/cache/apk/*
+ else
+ $STD apt -y autoremove || true
+ $STD apt -y autoclean || true
+ $STD apt -y clean || true
+ fi
+
+ # Clear temp artifacts (keep sockets/FIFOs; ignore errors)
+ find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true
+ find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true
+
+ # Truncate writable log files silently (permission errors ignored)
+ if command -v truncate >/dev/null 2>&1; then
+ find /var/log -type f -writable -print0 2>/dev/null |
+ xargs -0 -n1 truncate -s 0 2>/dev/null || true
+ fi
+
+ # Python pip
+ if command -v pip &>/dev/null; then $STD pip cache purge || true; fi
+ # Python uv
+ if command -v uv &>/dev/null; then $STD uv cache clean || true; fi
+ # Node.js npm
+ if command -v npm &>/dev/null; then $STD npm cache clean --force || true; fi
+ # Node.js yarn
+ if command -v yarn &>/dev/null; then $STD yarn cache clean || true; fi
+ # Node.js pnpm
+ if command -v pnpm &>/dev/null; then $STD pnpm store prune || true; fi
+ # Go
+ if command -v go &>/dev/null; then $STD go clean -cache -modcache || true; fi
+ # Rust cargo
+ if command -v cargo &>/dev/null; then $STD cargo clean || true; fi
+ # Ruby gem
+ if command -v gem &>/dev/null; then $STD gem cleanup || true; fi
+ # Composer (PHP)
+ if command -v composer &>/dev/null; then $STD composer clear-cache || true; fi
+
+ if command -v journalctl &>/dev/null; then
+ # Journal rotation may fail if systemd-journald not fully initialized yet
+ journalctl --rotate 2>/dev/null || true
+ journalctl --vacuum-time=10m 2>/dev/null || true
+ fi
+ msg_ok "Cleaned"
+}
+
+# ------------------------------------------------------------------------------
+# check_or_create_swap()
+#
+# - Checks if swap is active on system
+# - Offers to create swap file if none exists
+# - Prompts user for swap size in MB
+# - Creates /swapfile with specified size
+# - Activates swap immediately
+# - Returns 0 if swap active or successfully created, 1 if declined/failed
+# ------------------------------------------------------------------------------
check_or_create_swap() {
msg_info "Checking for active swap"
@@ -445,4 +889,8 @@ check_or_create_swap() {
fi
}
+# ==============================================================================
+# SIGNAL TRAPS
+# ==============================================================================
+
trap 'stop_spinner' EXIT INT TERM
diff --git a/misc/deferred/build.func.backup-20251029-123804 b/misc/deferred/build.func.backup-20251029-123804
new file mode 100644
index 000000000..9c8a1fc84
--- /dev/null
+++ b/misc/deferred/build.func.backup-20251029-123804
@@ -0,0 +1,3516 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# Revision: 1
+
+# ------------------------------------------------------------------------------
+# variables()
+#
+# - Normalize application name (NSAPP = lowercase, no spaces)
+# - Build installer filename (var_install)
+# - Define regex for integer validation
+# - Fetch hostname of Proxmox node
+# - Set default values for diagnostics/method
+# - Generate random UUID for tracking
+# - Get Proxmox VE version and kernel version
+# ------------------------------------------------------------------------------
+variables() {
+ NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
+ var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
+ INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
+ PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
+ DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
+ METHOD="default" # sets the METHOD variable to "default", used for the API call.
+ RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
+ CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
+ #CT_TYPE=${var_unprivileged:-$CT_TYPE}
+
+ # Get Proxmox VE version and kernel version
+ if command -v pveversion >/dev/null 2>&1; then
+ PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1)
+ else
+ PVEVERSION="N/A"
+ fi
+ KERNEL_VERSION=$(uname -r)
+}
+
+# -----------------------------------------------------------------------------
+# Community-Scripts bootstrap loader
+# - Always sources build.func from remote
+# - Updates local core files only if build.func changed
+# - Local cache: /usr/local/community-scripts/core
+# -----------------------------------------------------------------------------
+
+# FUNC_DIR="/usr/local/community-scripts/core"
+# mkdir -p "$FUNC_DIR"
+
+# BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func"
+# BUILD_REV="$FUNC_DIR/build.rev"
+# DEVMODE="${DEVMODE:-no}"
+
+# # --- Step 1: fetch build.func content once, compute hash ---
+# build_content="$(curl -fsSL "$BUILD_URL")" || {
+# echo "❌ Failed to fetch build.func"
+# exit 1
+# }
+
+# newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}')
+# oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "")
+
+# # --- Step 2: if build.func changed, offer update for core files ---
+# if [ "$newhash" != "$oldhash" ]; then
+# echo "⚠️ build.func changed!"
+
+# while true; do
+# read -rp "Refresh local core files? [y/N/diff]: " ans
+# case "$ans" in
+# [Yy]*)
+# echo "$newhash" >"$BUILD_REV"
+
+# update_func_file() {
+# local file="$1"
+# local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file"
+# local local_path="$FUNC_DIR/$file"
+
+# echo "⬇️ Downloading $file ..."
+# curl -fsSL "$url" -o "$local_path" || {
+# echo "❌ Failed to fetch $file"
+# exit 1
+# }
+# echo "✔️ Updated $file"
+# }
+
+# update_func_file core.func
+# update_func_file error_handler.func
+# update_func_file tools.func
+# break
+# ;;
+# [Dd]*)
+# for file in core.func error_handler.func tools.func; do
+# local_path="$FUNC_DIR/$file"
+# url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file"
+# remote_tmp="$(mktemp)"
+
+# curl -fsSL "$url" -o "$remote_tmp" || continue
+
+# if [ -f "$local_path" ]; then
+# echo "🔍 Diff for $file:"
+# diff -u "$local_path" "$remote_tmp" || echo "(no differences)"
+# else
+# echo "📦 New file $file will be installed"
+# fi
+
+# rm -f "$remote_tmp"
+# done
+# ;;
+# *)
+# echo "❌ Skipped updating local core files"
+# break
+# ;;
+# esac
+# done
+# else
+# if [ "$DEVMODE" != "yes" ]; then
+# echo "✔️ build.func unchanged → using existing local core files"
+# fi
+# fi
+
+# if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then
+# return 0 2>/dev/null || exit 0
+# fi
+# _COMMUNITY_SCRIPTS_LOADER=1
+
+# # --- Step 3: always source local versions of the core files ---
+# source "$FUNC_DIR/core.func"
+# source "$FUNC_DIR/error_handler.func"
+# source "$FUNC_DIR/tools.func"
+
+# # --- Step 4: finally, source build.func directly from memory ---
+# # (no tmp file needed)
+# source <(printf "%s" "$build_content")
+
+# ------------------------------------------------------------------------------
+# Load core + error handler functions from community-scripts repo
+#
+# - Prefer curl if available, fallback to wget
+# - Load: core.func, error_handler.func, api.func
+# - Initialize error traps after loading
+# ------------------------------------------------------------------------------
+
+source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func)
+
+if command -v curl >/dev/null 2>&1; then
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via curl"
+elif command -v wget >/dev/null 2>&1; then
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via wget"
+fi
+
+# ------------------------------------------------------------------------------
+# maxkeys_check()
+#
+# - Reads kernel keyring limits (maxkeys, maxbytes)
+# - Checks current usage for LXC user (UID 100000)
+# - Warns if usage is close to limits and suggests sysctl tuning
+# - Exits if thresholds are exceeded
+# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
+# ------------------------------------------------------------------------------
+
+maxkeys_check() {
+ # Read kernel parameters
+ per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
+ per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
+
+ # Exit if kernel parameters are unavailable
+ if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
+ echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
+ exit 1
+ fi
+
+ # Fetch key usage for user ID 100000 (typical for containers)
+ used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
+ used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
+
+ # Calculate thresholds and suggested new limits
+ threshold_keys=$((per_user_maxkeys - 100))
+ threshold_bytes=$((per_user_maxbytes - 1000))
+ new_limit_keys=$((per_user_maxkeys * 2))
+ new_limit_bytes=$((per_user_maxbytes * 2))
+
+ # Check if key or byte usage is near limits
+ failure=0
+ if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+ if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+
+ # Provide next steps if issues are detected
+ if [[ "$failure" -eq 1 ]]; then
+ echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
+ exit 1
+ fi
+
+ echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
+}
+
+# ------------------------------------------------------------------------------
+# get_current_ip()
+#
+# - Returns current container IP depending on OS type
+# - Debian/Ubuntu: uses `hostname -I`
+# - Alpine: parses eth0 via `ip -4 addr`
+# ------------------------------------------------------------------------------
+get_current_ip() {
+ if [ -f /etc/os-release ]; then
+ # Check for Debian/Ubuntu (uses hostname -I)
+ if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
+ CURRENT_IP=$(hostname -I | awk '{print $1}')
+ # Check for Alpine (uses ip command)
+ elif grep -q 'ID=alpine' /etc/os-release; then
+ CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
+ else
+ CURRENT_IP="Unknown"
+ fi
+ fi
+ echo "$CURRENT_IP"
+}
+
+# ------------------------------------------------------------------------------
+# update_motd_ip()
+#
+# - Updates /etc/motd with current container IP
+# - Removes old IP entries to avoid duplicates
+# ------------------------------------------------------------------------------
+update_motd_ip() {
+ MOTD_FILE="/etc/motd"
+
+ if [ -f "$MOTD_FILE" ]; then
+ # Remove existing IP Address lines to prevent duplication
+ sed -i '/IP Address:/d' "$MOTD_FILE"
+
+ IP=$(get_current_ip)
+ # Add the new IP address
+ echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# install_ssh_keys_into_ct()
+#
+# - Installs SSH keys into container root account if SSH is enabled
+# - Uses pct push or direct input to authorized_keys
+# - Falls back to warning if no keys provided
+# ------------------------------------------------------------------------------
+install_ssh_keys_into_ct() {
+ [[ "$SSH" != "yes" ]] && return 0
+
+ if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
+ 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
+ }
+ 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
+ }
+ pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
+ msg_ok "Installed SSH keys into CT ${CTID}"
+ return 0
+ fi
+
+ # Fallback: nichts ausgewählt
+ msg_warn "No SSH keys to install (skipping)."
+ return 0
+}
+
+# ------------------------------------------------------------------------------
+# base_settings()
+#
+# - Defines all base/default variables for container creation
+# - Reads from environment variables (var_*)
+# - Provides fallback defaults for OS type/version
+# ------------------------------------------------------------------------------
+base_settings() {
+ # Default Settings
+ CT_TYPE=${var_unprivileged:-"1"}
+ DISK_SIZE=${var_disk:-"4"}
+ CORE_COUNT=${var_cpu:-"1"}
+ RAM_SIZE=${var_ram:-"1024"}
+ VERBOSE=${var_verbose:-"${1:-no}"}
+ PW=${var_pw:-""}
+ CT_ID=${var_ctid:-$NEXTID}
+ HN=${var_hostname:-$NSAPP}
+ BRG=${var_brg:-"vmbr0"}
+ NET=${var_net:-"dhcp"}
+ IPV6_METHOD=${var_ipv6_method:-"none"}
+ IPV6_STATIC=${var_ipv6_static:-""}
+ GATE=${var_gateway:-""}
+ APT_CACHER=${var_apt_cacher:-""}
+ APT_CACHER_IP=${var_apt_cacher_ip:-""}
+ MTU=${var_mtu:-""}
+ SD=${var_storage:-""}
+ NS=${var_ns:-""}
+ MAC=${var_mac:-""}
+ VLAN=${var_vlan:-""}
+ SSH=${var_ssh:-"no"}
+ SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
+ UDHCPC_FIX=${var_udhcpc_fix:-""}
+ TAGS="community-script,${var_tags:-}"
+ ENABLE_FUSE=${var_fuse:-"${1:-no}"}
+ ENABLE_TUN=${var_tun:-"${1:-no}"}
+
+ # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
+ if [ -z "$var_os" ]; then
+ var_os="debian"
+ fi
+ if [ -z "$var_version" ]; then
+ var_version="12"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# echo_default()
+#
+# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
+# - Uses icons and formatting for readability
+# - Convert CT_TYPE to description
+# ------------------------------------------------------------------------------
+echo_default() {
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ if [ "$VERBOSE" == "yes" ]; then
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
+ fi
+ echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
+ echo -e " "
+}
+
+# ------------------------------------------------------------------------------
+# exit_script()
+#
+# - Called when user cancels an action
+# - Clears screen and exits gracefully
+# ------------------------------------------------------------------------------
+exit_script() {
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
+}
+
+# ------------------------------------------------------------------------------
+# find_host_ssh_keys()
+#
+# - Scans system for available SSH keys
+# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
+# - Returns list of files containing valid SSH public keys
+# - Sets FOUND_HOST_KEY_COUNT to number of keys found
+# ------------------------------------------------------------------------------
+find_host_ssh_keys() {
+ local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
+ local -a files=() cand=()
+ local g="${var_ssh_import_glob:-}"
+ local total=0 f base c
+
+ shopt -s nullglob
+ if [[ -n "$g" ]]; then
+ for pat in $g; do cand+=($pat); done
+ else
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ fi
+ shopt -u nullglob
+
+ for f in "${cand[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # CRLF safe check for host keys
+ c=$(tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ {print}
+ ' | grep -E -c '"$re"' || true)
+
+ if ((c > 0)); then
+ files+=("$f")
+ total=$((total + c))
+ fi
+ done
+
+ # Fallback to /root/.ssh/authorized_keys
+ if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
+ if grep -E -q "$re" /root/.ssh/authorized_keys; then
+ files+=(/root/.ssh/authorized_keys)
+ total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
+ fi
+ fi
+
+ FOUND_HOST_KEY_COUNT="$total"
+ (
+ IFS=:
+ echo "${files[*]}"
+ )
+}
+
+# ------------------------------------------------------------------------------
+# advanced_settings()
+#
+# - Interactive whiptail menu for advanced configuration
+# - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM
+# - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode
+# - Ends with confirmation or re-entry if cancelled
+# ------------------------------------------------------------------------------
+advanced_settings() {
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
+ # Setting Default Tag for Advanced Settings
+ TAGS="community-script;${var_tags:-}"
+ CT_DEFAULT_TYPE="${CT_TYPE}"
+ CT_TYPE=""
+ while [ -z "$CT_TYPE" ]; do
+ if [ "$CT_DEFAULT_TYPE" == "1" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" ON \
+ "0" "Privileged" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os |${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ if [ "$CT_DEFAULT_TYPE" == "0" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" OFF \
+ "0" "Privileged" ON \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
+ echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ done
+
+ while true; do
+ if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
+ # Empty = Autologin
+ if [[ -z "$PW1" ]]; then
+ PW=""
+ PW1="Automatic Login"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
+ break
+ fi
+
+ # Invalid: contains spaces
+ if [[ "$PW1" == *" "* ]]; then
+ whiptail --msgbox "Password cannot contain spaces." 8 58
+ continue
+ fi
+
+ # Invalid: too short
+ if ((${#PW1} < 5)); then
+ whiptail --msgbox "Password must be at least 5 characters." 8 58
+ continue
+ fi
+
+ # Confirm password
+ if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
+ if [[ "$PW1" == "$PW2" ]]; then
+ PW="-password $PW1"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
+ break
+ else
+ whiptail --msgbox "Passwords do not match. Please try again." 8 58
+ fi
+ else
+ exit_script
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_ID" ]; then
+ CT_ID="$NEXTID"
+ fi
+ else
+ exit_script
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
+
+ while true; do
+ if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_NAME" ]; then
+ HN="$NSAPP"
+ else
+ HN=$(echo "${CT_NAME,,}" | tr -d ' ')
+ fi
+ # Hostname validate (RFC 1123)
+ if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ while true; do
+ DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$DISK_SIZE" ]; then
+ DISK_SIZE="$var_disk"
+ fi
+
+ if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ break
+ else
+ whiptail --msgbox "Disk size must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$CORE_COUNT" ]; then
+ CORE_COUNT="$var_cpu"
+ fi
+
+ if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ break
+ else
+ whiptail --msgbox "CPU core count must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$RAM_SIZE" ]; then
+ RAM_SIZE="$var_ram"
+ fi
+
+ if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ break
+ else
+ whiptail --msgbox "RAM size must be a positive integer!" 8 58
+ fi
+ done
+
+ IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
+ BRIDGES=""
+ OLD_IFS=$IFS
+ IFS=$'\n'
+ for iface_filepath in ${IFACE_FILEPATH_LIST}; do
+
+ iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
+ (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
+
+ if [ -f "${iface_indexes_tmpfile}" ]; then
+
+ while read -r pair; do
+ start=$(echo "${pair}" | cut -d':' -f1)
+ end=$(echo "${pair}" | cut -d':' -f2)
+
+ if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
+ iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
+ BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
+ fi
+
+ done <"${iface_indexes_tmpfile}"
+ rm -f "${iface_indexes_tmpfile}"
+ fi
+
+ done
+ IFS=$OLD_IFS
+ BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
+ if [[ -z "$BRIDGES" ]]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ # Build bridge menu with descriptions
+ BRIDGE_MENU_OPTIONS=()
+ while IFS= read -r bridge; do
+ if [[ -n "$bridge" ]]; then
+ # Get description from Proxmox built-in method - find comment for this specific bridge
+ description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//')
+ if [[ -n "$description" ]]; then
+ BRIDGE_MENU_OPTIONS+=("$bridge" "${description}")
+ else
+ BRIDGE_MENU_OPTIONS+=("$bridge" " ")
+ fi
+ fi
+ done <<<"$BRIDGES"
+
+ BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3)
+ if [[ -z "$BRG" ]]; then
+ exit_script
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
+ fi
+
+ # IPv4 methods: dhcp, static, none
+ while true; do
+ IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "IPv4 Address Management" \
+ --menu "Select IPv4 Address Assignment Method:" 12 60 2 \
+ "dhcp" "Automatic (DHCP, recommended)" \
+ "static" "Static (manual entry)" \
+ 3>&1 1>&2 2>&3)
+
+ exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ exit_script
+ fi
+
+ case "$IPV4_METHOD" in
+ dhcp)
+ NET="dhcp"
+ GATE=""
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}"
+ break
+ ;;
+ static)
+ # Static: call and validate CIDR address
+ while true; do
+ NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \
+ --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3)
+ if [ -z "$NET" ]; then
+ whiptail --msgbox "IPv4 address must not be empty." 8 58
+ continue
+ elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}"
+ break
+ else
+ whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58
+ fi
+ done
+
+ # call and validate Gateway
+ while true; do
+ GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \
+ --title "Gateway IP" 3>&1 1>&2 2>&3)
+ if [ -z "$GATE1" ]; then
+ whiptail --msgbox "Gateway IP address cannot be empty." 8 58
+ elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
+ whiptail --msgbox "Invalid Gateway IP address format." 8 58
+ else
+ GATE=",gw=$GATE1"
+ echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
+ break
+ fi
+ done
+ break
+ ;;
+ esac
+ done
+
+ # IPv6 Address Management selection
+ while true; do
+ IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \
+ "Select IPv6 Address Management Type:" 15 58 4 \
+ "auto" "SLAAC/AUTO (recommended, default)" \
+ "dhcp" "DHCPv6" \
+ "static" "Static (manual entry)" \
+ "none" "Disabled" \
+ --default-item "auto" 3>&1 1>&2 2>&3)
+ [ $? -ne 0 ] && exit_script
+
+ case "$IPV6_METHOD" in
+ auto)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}"
+ IPV6_ADDR=""
+ IPV6_GATE=""
+ break
+ ;;
+ dhcp)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}"
+ IPV6_ADDR="dhcp"
+ IPV6_GATE=""
+ break
+ ;;
+ static)
+ # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64)
+ while true; do
+ IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \
+ --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script
+ if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58
+ fi
+ done
+ # Optional: ask for IPv6 gateway for static config
+ while true; do
+ IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3)
+ if [ -z "$IPV6_GATE" ]; then
+ IPV6_GATE=""
+ break
+ elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "Invalid IPv6 gateway format." 8 58
+ fi
+ done
+ break
+ ;;
+ none)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}"
+ IPV6_ADDR="none"
+ IPV6_GATE=""
+ break
+ ;;
+ *)
+ exit_script
+ ;;
+ esac
+ done
+
+ if [ "$var_os" == "alpine" ]; then
+ APT_CACHER=""
+ APT_CACHER_IP=""
+ else
+ if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
+ APT_CACHER="${APT_CACHER_IP:+yes}"
+ echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
+ else
+ exit_script
+ fi
+ fi
+
+ # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then
+ # DISABLEIP6="yes"
+ # else
+ # DISABLEIP6="no"
+ # fi
+ # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}"
+
+ if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
+ if [ -z "$MTU1" ]; then
+ MTU1="Default"
+ MTU=""
+ else
+ MTU=",mtu=$MTU1"
+ fi
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ exit_script
+ fi
+
+ if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
+ if [ -z "$SD" ]; then
+ SX=Host
+ SD=""
+ else
+ SX=$SD
+ SD="-searchdomain=$SD"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
+ else
+ exit_script
+ fi
+
+ if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
+ if [ -z "$NX" ]; then
+ NX=Host
+ NS=""
+ else
+ NS="-nameserver=$NX"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
+ else
+ exit_script
+ fi
+
+ if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then
+ UDHCPC_FIX="yes"
+ else
+ UDHCPC_FIX="no"
+ fi
+ export UDHCPC_FIX
+
+ if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
+ if [ -z "$MAC1" ]; then
+ MAC1="Default"
+ MAC=""
+ else
+ MAC=",hwaddr=$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
+ else
+ exit_script
+ fi
+
+ if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
+ if [ -z "$VLAN1" ]; then
+ VLAN1="Default"
+ VLAN=""
+ else
+ VLAN=",tag=$VLAN1"
+ fi
+ echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
+ else
+ exit_script
+ fi
+
+ if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
+ if [ -n "${ADV_TAGS}" ]; then
+ ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
+ TAGS="${ADV_TAGS}"
+ else
+ TAGS=";"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
+ else
+ exit_script
+ fi
+
+ configure_ssh_settings
+ export SSH_KEYS_FILE
+ echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
+ ENABLE_FUSE="yes"
+ else
+ ENABLE_FUSE="no"
+ fi
+ echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
+ VERBOSE="yes"
+ else
+ VERBOSE="no"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
+ echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
+ else
+ clear
+ header_info
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
+ advanced_settings
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# diagnostics_check()
+#
+# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
+# - Asks user whether to send anonymous diagnostic data
+# - Saves DIAGNOSTICS=yes/no in the config file
+# ------------------------------------------------------------------------------
+diagnostics_check() {
+ if ! [ -d "/usr/local/community-scripts" ]; then
+ mkdir -p /usr/local/community-scripts
+ fi
+
+ if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=yes
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="yes"
+ else
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=no
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="no"
+ fi
+ else
+ DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
+
+ fi
+
+}
+
+# ------------------------------------------------------------------------------
+# default_var_settings
+#
+# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
+# - Loads var_* values from default.vars (safe parser, no source/eval)
+# - Precedence: ENV var_* > default.vars > built-in defaults
+# - Maps var_verbose → VERBOSE
+# - Calls base_settings "$VERBOSE" and echo_default
+# ------------------------------------------------------------------------------
+default_var_settings() {
+ # Allowed var_* keys (alphabetically sorted)
+ local VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+
+ # Snapshot: environment variables (highest precedence)
+ declare -A _HARD_ENV=()
+ local _k
+ for _k in "${VAR_WHITELIST[@]}"; do
+ if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
+ done
+
+ # Find default.vars location
+ local _find_default_vars
+ _find_default_vars() {
+ local f
+ for f in \
+ /usr/local/community-scripts/default.vars \
+ "$HOME/.config/community-scripts/default.vars" \
+ "./default.vars"; do
+ [ -f "$f" ] && {
+ echo "$f"
+ return 0
+ }
+ done
+ return 1
+ }
+ # Allow override of storages via env (for non-interactive use cases)
+ [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
+ [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
+
+ # Create once, with storages already selected, no var_ctid/var_hostname lines
+ local _ensure_default_vars
+ _ensure_default_vars() {
+ _find_default_vars >/dev/null 2>&1 && return 0
+
+ local canonical="/usr/local/community-scripts/default.vars"
+ msg_info "No default.vars found. Creating ${canonical}"
+ mkdir -p /usr/local/community-scripts
+
+ # Pick storages before writing the file (always ask unless only one)
+ # Create a minimal temp file to write into
+ : >"$canonical"
+
+ # Base content (no var_ctid / var_hostname here)
+ cat >"$canonical" <<'EOF'
+# Community-Scripts defaults (var_* only). Lines starting with # are comments.
+# Precedence: ENV var_* > default.vars > built-ins.
+# Keep keys alphabetically sorted.
+
+# Container type
+var_unprivileged=1
+
+# Resources
+var_cpu=1
+var_disk=4
+var_ram=1024
+
+# Network
+var_brg=vmbr0
+var_net=dhcp
+var_ipv6_method=none
+# var_gateway=
+# var_ipv6_static=
+# var_vlan=
+# var_mtu=
+# var_mac=
+# var_ns=
+
+# SSH
+var_ssh=no
+# var_ssh_authorized_key=
+
+# APT cacher (optional)
+# var_apt_cacher=yes
+# var_apt_cacher_ip=192.168.1.10
+
+# Features/Tags/verbosity
+var_fuse=no
+var_tun=no
+var_tags=community-script
+var_verbose=no
+
+# Security (root PW) – empty => autologin
+# var_pw=
+EOF
+
+ # Now choose storages (always prompt unless just one exists)
+ choose_and_set_storage_for_file "$canonical" template
+ choose_and_set_storage_for_file "$canonical" container
+
+ chmod 0644 "$canonical"
+ msg_ok "Created ${canonical}"
+ }
+
+ # Whitelist check
+ local _is_whitelisted_key
+ _is_whitelisted_key() {
+ local k="$1"
+ local w
+ for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
+ return 1
+ }
+
+ # Safe parser for KEY=VALUE lines
+ local _load_vars_file
+ _load_vars_file() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ msg_info "Loading defaults from ${file}"
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [[ -z "$line" || "$line" == \#* ]] && continue
+ if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
+ local var_key="${BASH_REMATCH[1]}"
+ local var_val="${BASH_REMATCH[2]}"
+
+ [[ "$var_key" != var_* ]] && continue
+ _is_whitelisted_key "$var_key" || {
+ msg_debug "Ignore non-whitelisted ${var_key}"
+ continue
+ }
+
+ # Strip quotes
+ if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ fi
+
+ # Unsafe characters
+ case $var_val in
+ \"*\")
+ var_val=${var_val#\"}
+ var_val=${var_val%\"}
+ ;;
+ \'*\')
+ var_val=${var_val#\'}
+ var_val=${var_val%\'}
+ ;;
+ esac # Hard env wins
+ [[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue
+ # Set only if not already exported
+ [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
+ else
+ msg_warn "Malformed line in ${file}: ${line}"
+ fi
+ done <"$file"
+ msg_ok "Loaded ${file}"
+ }
+
+ # 1) Ensure file exists
+ _ensure_default_vars
+
+ # 2) Load file
+ local dv
+ dv="$(_find_default_vars)" || {
+ msg_error "default.vars not found after ensure step"
+ return 1
+ }
+ _load_vars_file "$dv"
+
+ # 3) Map var_verbose → VERBOSE
+ if [[ -n "${var_verbose:-}" ]]; then
+ case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
+ else
+ VERBOSE="no"
+ fi
+
+ # 4) Apply base settings and show summary
+ METHOD="mydefaults-global"
+ base_settings "$VERBOSE"
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
+ echo_default
+}
+
+# ------------------------------------------------------------------------------
+# get_app_defaults_path()
+#
+# - Returns full path for app-specific defaults file
+# - Example: /usr/local/community-scripts/defaults/.vars
+# ------------------------------------------------------------------------------
+
+get_app_defaults_path() {
+ local n="${NSAPP:-${APP,,}}"
+ echo "/usr/local/community-scripts/defaults/${n}.vars"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults
+#
+# - Called after advanced_settings returned with fully chosen values.
+# - If no .vars exists, offers to persist current advanced settings
+# into /usr/local/community-scripts/defaults/.vars
+# - Only writes whitelisted var_* keys.
+# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
+# ------------------------------------------------------------------------------
+if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
+ declare -ag VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+fi
+
+_is_whitelisted_key() {
+ local k="$1"
+ local w
+ for w in "${VAR_WHITELIST[@]}"; do [[ "$k" == "$w" ]] && return 0; done
+ return 1
+}
+
+_sanitize_value() {
+ # Disallow Command-Substitution / Shell-Meta
+ case "$1" in
+ *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
+ echo ""
+ return 0
+ ;;
+ esac
+ echo "$1"
+}
+
+# Map-Parser: read var_* from file into _VARS_IN associative array
+declare -A _VARS_IN
+_load_vars_file() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ msg_info "Loading defaults from ${file}"
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [ -z "$line" ] && continue
+ case "$line" in
+ \#*) continue ;;
+ esac
+ key=$(printf "%s" "$line" | cut -d= -f1)
+ val=$(printf "%s" "$line" | cut -d= -f2-)
+ case "$key" in
+ var_*)
+ if _is_whitelisted_key "$key"; then
+ [ -z "${!key+x}" ] && export "$key=$val"
+ fi
+ ;;
+ esac
+ done <"$file"
+ msg_ok "Loaded ${file}"
+}
+
+# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
+_build_vars_diff() {
+ local oldf="$1" newf="$2"
+ local k
+ local -A OLD=() NEW=()
+ _load_vars_file_to_map "$oldf"
+ for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
+ _load_vars_file_to_map "$newf"
+ for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
+
+ local out
+ out+="# Diff for ${APP} (${NSAPP})\n"
+ out+="# Old: ${oldf}\n# New: ${newf}\n\n"
+
+ local found_change=0
+
+ # Changed & Removed
+ for k in "${!OLD[@]}"; do
+ if [[ -v NEW["$k"] ]]; then
+ if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
+ out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ else
+ out+="- ${k}\n - old: ${OLD[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ # Added
+ for k in "${!NEW[@]}"; do
+ if [[ ! -v OLD["$k"] ]]; then
+ out+="+ ${k}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ if [[ $found_change -eq 0 ]]; then
+ out+="(No differences)\n"
+ fi
+
+ printf "%b" "$out"
+}
+
+# Build a temporary .vars file from current advanced settings
+_build_current_app_vars_tmp() {
+ tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
+
+ # NET/GW
+ _net="${NET:-}"
+ _gate=""
+ case "${GATE:-}" in
+ ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
+ esac
+
+ # IPv6
+ _ipv6_method="${IPV6_METHOD:-auto}"
+ _ipv6_static=""
+ _ipv6_gateway=""
+ if [ "$_ipv6_method" = "static" ]; then
+ _ipv6_static="${IPV6_ADDR:-}"
+ _ipv6_gateway="${IPV6_GATE:-}"
+ fi
+
+ # MTU/VLAN/MAC
+ _mtu=""
+ _vlan=""
+ _mac=""
+ case "${MTU:-}" in
+ ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
+ esac
+ case "${VLAN:-}" in
+ ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
+ esac
+ case "${MAC:-}" in
+ ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
+ esac
+
+ # DNS / Searchdomain
+ _ns=""
+ _searchdomain=""
+ case "${NS:-}" in
+ -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
+ esac
+ case "${SD:-}" in
+ -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
+ esac
+
+ # SSH / APT / Features
+ _ssh="${SSH:-no}"
+ _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
+ _apt_cacher="${APT_CACHER:-}"
+ _apt_cacher_ip="${APT_CACHER_IP:-}"
+ _fuse="${ENABLE_FUSE:-no}"
+ _tun="${ENABLE_TUN:-no}"
+ _tags="${TAGS:-}"
+ _verbose="${VERBOSE:-no}"
+
+ # Type / Resources / Identity
+ _unpriv="${CT_TYPE:-1}"
+ _cpu="${CORE_COUNT:-1}"
+ _ram="${RAM_SIZE:-1024}"
+ _disk="${DISK_SIZE:-4}"
+ _hostname="${HN:-$NSAPP}"
+
+ # Storage
+ _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
+ _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
+
+ {
+ echo "# App-specific defaults for ${APP} (${NSAPP})"
+ echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo
+
+ echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
+ echo "var_cpu=$(_sanitize_value "$_cpu")"
+ echo "var_ram=$(_sanitize_value "$_ram")"
+ echo "var_disk=$(_sanitize_value "$_disk")"
+
+ [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
+ [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
+ [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
+ [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
+ [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
+ [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
+ [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
+
+ [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
+ [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")"
+
+ [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
+ [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
+
+ [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
+ [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
+
+ [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
+ [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
+ [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
+ [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
+
+ [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
+ [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
+
+ [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
+ [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
+ } >"$tmpf"
+
+ echo "$tmpf"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults()
+#
+# - Called after advanced_settings()
+# - Offers to save current values as app defaults if not existing
+# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
+# ------------------------------------------------------------------------------
+maybe_offer_save_app_defaults() {
+ local app_vars_path
+ app_vars_path="$(get_app_defaults_path)"
+
+ # always build from current settings
+ local new_tmp diff_tmp
+ new_tmp="$(_build_current_app_vars_tmp)"
+ diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
+
+ # 1) if no file → offer to create
+ if [[ ! -f "$app_vars_path" ]]; then
+ if whiptail --backtitle "[dev] 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"
+ msg_ok "Saved app defaults: ${app_vars_path}"
+ fi
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 2) if file exists → build diff
+ _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
+
+ # if no differences → do nothing
+ if grep -q "^(No differences)$" "$diff_tmp"; then
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 3) if file exists → show menu with default selection "Update Defaults"
+ local app_vars_file
+ app_vars_file="$(basename "$app_vars_path")"
+
+ while true; do
+ local sel
+ sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "APP DEFAULTS – ${APP}" \
+ --menu "Differences detected. What do you want to do?" 20 78 10 \
+ "Update Defaults" "Write new values to ${app_vars_file}" \
+ "Keep Current" "Keep existing defaults (no changes)" \
+ "View Diff" "Show a detailed diff" \
+ "Cancel" "Abort without changes" \
+ --default-item "Update Defaults" \
+ 3>&1 1>&2 2>&3)" || { sel="Cancel"; }
+
+ case "$sel" in
+ "Update Defaults")
+ install -m 0644 "$new_tmp" "$app_vars_path"
+ msg_ok "Updated app defaults: ${app_vars_path}"
+ break
+ ;;
+ "Keep Current")
+ msg_info "Keeping current app defaults: ${app_vars_path}"
+ break
+ ;;
+ "View Diff")
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Diff – ${APP}" \
+ --scrolltext --textbox "$diff_tmp" 25 100
+ ;;
+ "Cancel" | *)
+ msg_info "Canceled. No changes to app defaults."
+ break
+ ;;
+ esac
+ done
+
+ rm -f "$new_tmp" "$diff_tmp"
+}
+
+ensure_storage_selection_for_vars_file() {
+ local vf="$1"
+
+ # 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-)
+
+ if [[ -n "$tpl" && -n "$ct" ]]; then
+ TEMPLATE_STORAGE="$tpl"
+ CONTAINER_STORAGE="$ct"
+ return 0
+ fi
+
+ choose_and_set_storage_for_file "$vf" template
+ choose_and_set_storage_for_file "$vf" container
+
+ msg_ok "Storage configuration saved to $(basename "$vf")"
+}
+
+diagnostics_menu() {
+ if [ "${DIAGNOSTICS:-no}" = "yes" ]; then
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "No" --no-button "Back"; then
+ DIAGNOSTICS="no"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ else
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "Yes" --no-button "Back"; then
+ DIAGNOSTICS="yes"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ fi
+}
+
+ensure_global_default_vars_file() {
+ local vars_path="/usr/local/community-scripts/default.vars"
+ if [[ ! -f "$vars_path" ]]; then
+ mkdir -p "$(dirname "$vars_path")"
+ touch "$vars_path"
+ fi
+ echo "$vars_path"
+}
+
+# ------------------------------------------------------------------------------
+# install_script()
+#
+# - Main entrypoint for installation mode
+# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
+# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
+# - Applies chosen settings and triggers container build
+# ------------------------------------------------------------------------------
+install_script() {
+ pve_check
+ shell_check
+ root_check
+ arch_check
+ ssh_check
+ maxkeys_check
+ diagnostics_check
+
+ if systemctl is-active -q ping-instances.service; then
+ systemctl -q stop ping-instances.service
+ fi
+
+ NEXTID=$(pvesh get /cluster/nextid)
+ timezone=$(cat /etc/timezone)
+
+ # Show APP Header
+ header_info
+
+ # --- Support CLI argument as direct preset (default, advanced, …) ---
+ CHOICE="${mode:-${1:-}}"
+
+ # If no CLI argument → show whiptail menu
+ # Build menu dynamically based on available options
+ local appdefaults_option=""
+ local settings_option=""
+ local menu_items=(
+ "1" "Default Install"
+ "2" "Advanced Install"
+ "3" "My Defaults"
+ )
+
+ if [ -f "$(get_app_defaults_path)" ]; then
+ appdefaults_option="4"
+ menu_items+=("4" "App Defaults for ${APP}")
+ settings_option="5"
+ menu_items+=("5" "Settings")
+ else
+ settings_option="4"
+ menu_items+=("4" "Settings")
+ fi
+
+ if [ -z "$CHOICE" ]; then
+
+ TMP_CHOICE=$(whiptail \
+ --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts Options" \
+ --ok-button "Select" --cancel-button "Exit Script" \
+ --notags \
+ --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
+ 20 60 9 \
+ "${menu_items[@]}" \
+ --default-item "1" \
+ 3>&1 1>&2 2>&3) || exit_script
+ CHOICE="$TMP_CHOICE"
+ fi
+
+ APPDEFAULTS_OPTION="$appdefaults_option"
+ SETTINGS_OPTION="$settings_option"
+
+ # --- Main case ---
+ local defaults_target=""
+ local run_maybe_offer="no"
+ case "$CHOICE" in
+ 1 | default | DEFAULT)
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
+ VERBOSE="no"
+ METHOD="default"
+ base_settings "$VERBOSE"
+ echo_default
+ defaults_target="$(ensure_global_default_vars_file)"
+ ;;
+ 2 | advanced | ADVANCED)
+ header_info
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
+ METHOD="advanced"
+ base_settings
+ advanced_settings
+ defaults_target="$(ensure_global_default_vars_file)"
+ run_maybe_offer="yes"
+ ;;
+ 3 | mydefaults | MYDEFAULTS)
+ default_var_settings || {
+ msg_error "Failed to apply default.vars"
+ exit 1
+ }
+ defaults_target="/usr/local/community-scripts/default.vars"
+ ;;
+ "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
+ METHOD="appdefaults"
+ base_settings
+ _load_vars_file "$(get_app_defaults_path)"
+ echo_default
+ defaults_target="$(get_app_defaults_path)"
+ else
+ msg_error "No App Defaults available for ${APP}"
+ exit 1
+ fi
+ ;;
+ "$SETTINGS_OPTION" | settings | SETTINGS)
+ settings_menu
+ defaults_target=""
+ ;;
+ *)
+ echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}"
+ exit 1
+ ;;
+ esac
+
+ if [[ -n "$defaults_target" ]]; then
+ ensure_storage_selection_for_vars_file "$defaults_target"
+ fi
+
+ if [[ "$run_maybe_offer" == "yes" ]]; then
+ maybe_offer_save_app_defaults
+ fi
+}
+
+edit_default_storage() {
+ local vf="/usr/local/community-scripts/default.vars"
+
+ # Ensure file exists
+ if [[ ! -f "$vf" ]]; then
+ mkdir -p "$(dirname "$vf")"
+ touch "$vf"
+ fi
+
+ # Let ensure_storage_selection_for_vars_file handle everything
+ ensure_storage_selection_for_vars_file "$vf"
+}
+
+settings_menu() {
+ while true; do
+ local settings_items=(
+ "1" "Manage API-Diagnostic Setting"
+ "2" "Edit Default.vars"
+ "3" "Edit Default Storage"
+ )
+ if [ -f "$(get_app_defaults_path)" ]; then
+ settings_items+=("4" "Edit App.vars for ${APP}")
+ settings_items+=("5" "Exit")
+ else
+ settings_items+=("4" "Exit")
+ fi
+
+ local choice
+ choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts SETTINGS Menu" \
+ --ok-button "OK" --cancel-button "Back" \
+ --menu "\n\nChoose a settings option:\n\nUse TAB or Arrow keys to navigate, ENTER to select." 20 60 9 \
+ "${settings_items[@]}" \
+ 3>&1 1>&2 2>&3) || break
+
+ case "$choice" in
+ 1) diagnostics_menu ;;
+ 2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
+ 3) edit_default_storage ;;
+ 4)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ ${EDITOR:-nano} "$(get_app_defaults_path)"
+ else
+ exit_script
+ fi
+ ;;
+ 5) exit_script ;;
+ esac
+ done
+}
+
+# ===== Unified storage selection & writing to vars files =====
+_write_storage_to_vars() {
+ # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
+ local vf="$1" key="$2" val="$3"
+ # remove uncommented and commented versions to avoid duplicates
+ sed -i "/^[#[:space:]]*${key}=/d" "$vf"
+ echo "${key}=${val}" >>"$vf"
+}
+
+choose_and_set_storage_for_file() {
+ # $1 = vars_file, $2 = class ('container'|'template')
+ local vf="$1" class="$2" key="" current=""
+ case "$class" in
+ container) key="var_container_storage" ;;
+ template) key="var_template_storage" ;;
+ *)
+ msg_error "Unknown storage class: $class"
+ return 1
+ ;;
+ esac
+
+ current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
+
+ # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
+ local content="rootdir"
+ [[ "$class" == "template" ]] && content="vztmpl"
+ local count
+ count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
+
+ if [[ "$count" -eq 1 ]]; then
+ STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
+ STORAGE_INFO=""
+ else
+ # If the current value is preselectable, we could show it, but per your requirement we always offer selection
+ select_storage "$class" || return 1
+ fi
+
+ _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
+
+ # Keep environment in sync for later steps (e.g. app-default save)
+ if [[ "$class" == "container" ]]; then
+ export var_container_storage="$STORAGE_RESULT"
+ export CONTAINER_STORAGE="$STORAGE_RESULT"
+ else
+ export var_template_storage="$STORAGE_RESULT"
+ export TEMPLATE_STORAGE="$STORAGE_RESULT"
+ fi
+
+ msg_ok "Updated ${key} → ${STORAGE_RESULT}"
+}
+
+# ------------------------------------------------------------------------------
+# check_container_resources()
+#
+# - Compares host RAM/CPU with required values
+# - Warns if under-provisioned and asks user to continue or abort
+# ------------------------------------------------------------------------------
+check_container_resources() {
+ current_ram=$(free -m | awk 'NR==2{print $2}')
+ current_cpu=$(nproc)
+
+ if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
+ echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
+ echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
+ echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
+ echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ else
+ echo -e ""
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# check_container_storage()
+#
+# - Checks /boot partition usage
+# - Warns if usage >80% and asks user confirmation before proceeding
+# ------------------------------------------------------------------------------
+check_container_storage() {
+ total_size=$(df /boot --output=size | tail -n 1)
+ local used_size=$(df /boot --output=used | tail -n 1)
+ usage=$((100 * used_size / total_size))
+ if ((usage > 80)); then
+ echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
+ echo -ne "Continue anyway? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
+ echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# ssh_extract_keys_from_file()
+#
+# - Extracts valid SSH public keys from given file
+# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
+# ------------------------------------------------------------------------------
+ssh_extract_keys_from_file() {
+ local f="$1"
+ [[ -r "$f" ]] || return 0
+ tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ # nackt: typ base64 [comment]
+ /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
+ # mit Optionen: finde ab erstem Key-Typ
+ {
+ match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
+ if (RSTART>0) { print substr($0, RSTART) }
+ }
+ '
+}
+
+# ------------------------------------------------------------------------------
+# ssh_build_choices_from_files()
+#
+# - Builds interactive whiptail checklist of available SSH keys
+# - Generates fingerprint, type and comment for each key
+# ------------------------------------------------------------------------------
+ssh_build_choices_from_files() {
+ local -a files=("$@")
+ CHOICES=()
+ COUNT=0
+ MAPFILE="$(mktemp)"
+ local id key typ fp cmt base ln=0
+
+ for f in "${files[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # map every key in file
+ while IFS= read -r key; do
+ [[ -n "$key" ]] || continue
+
+ typ=""
+ fp=""
+ cmt=""
+ # Only the pure key part (without options) is already included in ‘key’.
+ read -r _typ _b64 _cmt <<<"$key"
+ typ="${_typ:-key}"
+ cmt="${_cmt:-}"
+ # Fingerprint via ssh-keygen (if available)
+ if command -v ssh-keygen >/dev/null 2>&1; then
+ fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
+ fi
+ # Label shorten
+ [[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
+
+ ln=$((ln + 1))
+ COUNT=$((COUNT + 1))
+ id="K${COUNT}"
+ echo "${id}|${key}" >>"$MAPFILE"
+ CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
+ done < <(ssh_extract_keys_from_file "$f")
+ done
+}
+
+# ------------------------------------------------------------------------------
+# ssh_discover_default_files()
+#
+# - Scans standard paths for SSH keys
+# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc.
+# ------------------------------------------------------------------------------
+ssh_discover_default_files() {
+ local -a cand=()
+ shopt -s nullglob
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ shopt -u nullglob
+ printf '%s\0' "${cand[@]}"
+}
+
+configure_ssh_settings() {
+ SSH_KEYS_FILE="$(mktemp)"
+ : >"$SSH_KEYS_FILE"
+
+ IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
+ ssh_build_choices_from_files "${_def_files[@]}"
+ local default_key_count="$COUNT"
+
+ local ssh_key_mode
+ if [[ "$default_key_count" -gt 0 ]]; then
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "Provision SSH keys for root:" 14 72 4 \
+ "found" "Select from detected keys (${default_key_count})" \
+ "manual" "Paste a single public key" \
+ "folder" "Scan another folder (path or glob)" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ else
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "No host keys detected; choose manual/none:" 12 72 2 \
+ "manual" "Paste a single public key" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ fi
+
+ case "$ssh_key_mode" in
+ found)
+ local selection
+ selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \
+ --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ ;;
+ manual)
+ SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
+ [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
+ ;;
+ folder)
+ local glob_path
+ glob_path=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
+ if [[ -n "$glob_path" ]]; then
+ shopt -s nullglob
+ read -r -a _scan_files <<<"$glob_path"
+ shopt -u nullglob
+ if [[ "${#_scan_files[@]}" -gt 0 ]]; then
+ ssh_build_choices_from_files "${_scan_files[@]}"
+ if [[ "$COUNT" -gt 0 ]]; then
+ local folder_selection
+ folder_selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \
+ --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $folder_selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60
+ fi
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60
+ fi
+ fi
+ ;;
+ none)
+ :
+ ;;
+ esac
+
+ if [[ -s "$SSH_KEYS_FILE" ]]; then
+ sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
+ printf '\n' >>"$SSH_KEYS_FILE"
+ fi
+
+ if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
+ SSH="yes"
+ else
+ SSH="no"
+ fi
+ else
+ SSH="no"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# start()
+#
+# - Entry point of script
+# - On Proxmox host: calls install_script
+# - In silent mode: runs update_script
+# - Otherwise: shows update/setting menu
+# ------------------------------------------------------------------------------
+start() {
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func)
+ if command -v pveversion >/dev/null 2>&1; then
+ install_script || return 0
+ return 0
+ elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
+ VERBOSE="no"
+ set_std_mode
+ update_script
+ else
+ CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
+ "Support/Update functions for ${APP} LXC. Choose an option:" \
+ 12 60 3 \
+ "1" "YES (Silent Mode)" \
+ "2" "YES (Verbose Mode)" \
+ "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
+
+ case "$CHOICE" in
+ 1)
+ VERBOSE="no"
+ set_std_mode
+ ;;
+ 2)
+ VERBOSE="yes"
+ set_std_mode
+ ;;
+ 3)
+ clear
+ exit_script
+ exit
+ ;;
+ esac
+ update_script
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# build_container()
+#
+# - Creates and configures the LXC container
+# - Builds network string and applies features (FUSE, TUN, VAAPI passthrough)
+# - Starts container and waits for network connectivity
+# - Installs base packages, SSH keys, and runs -install.sh
+# ------------------------------------------------------------------------------
+build_container() {
+ # if [ "$VERBOSE" == "yes" ]; then set -x; fi
+
+ NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
+
+ # MAC
+ if [[ -n "$MAC" ]]; then
+ case "$MAC" in
+ ,hwaddr=*) NET_STRING+="$MAC" ;;
+ *) NET_STRING+=",hwaddr=$MAC" ;;
+ esac
+ fi
+
+ # IP (immer zwingend, Standard dhcp)
+ NET_STRING+=",ip=${NET:-dhcp}"
+
+ # Gateway
+ if [[ -n "$GATE" ]]; then
+ case "$GATE" in
+ ,gw=*) NET_STRING+="$GATE" ;;
+ *) NET_STRING+=",gw=$GATE" ;;
+ esac
+ fi
+
+ # VLAN
+ if [[ -n "$VLAN" ]]; then
+ case "$VLAN" in
+ ,tag=*) NET_STRING+="$VLAN" ;;
+ *) NET_STRING+=",tag=$VLAN" ;;
+ esac
+ fi
+
+ # MTU
+ if [[ -n "$MTU" ]]; then
+ case "$MTU" in
+ ,mtu=*) NET_STRING+="$MTU" ;;
+ *) NET_STRING+=",mtu=$MTU" ;;
+ esac
+ fi
+
+ # IPv6 Handling
+ case "$IPV6_METHOD" in
+ auto) NET_STRING="$NET_STRING,ip6=auto" ;;
+ dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
+ static)
+ NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
+ [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
+ ;;
+ none) ;;
+ esac
+
+ if [ "$CT_TYPE" == "1" ]; then
+ FEATURES="keyctl=1,nesting=1"
+ else
+ FEATURES="nesting=1"
+ fi
+
+ if [ "$ENABLE_FUSE" == "yes" ]; then
+ FEATURES="$FEATURES,fuse=1"
+ fi
+
+ TEMP_DIR=$(mktemp -d)
+ pushd "$TEMP_DIR" >/dev/null
+ if [ "$var_os" == "alpine" ]; then
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)"
+ else
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)"
+ fi
+ export DIAGNOSTICS="$DIAGNOSTICS"
+ export RANDOM_UUID="$RANDOM_UUID"
+ export CACHER="$APT_CACHER"
+ export CACHER_IP="$APT_CACHER_IP"
+ export tz="$timezone"
+ export APPLICATION="$APP"
+ export app="$NSAPP"
+ export PASSWORD="$PW"
+ export VERBOSE="$VERBOSE"
+ export SSH_ROOT="${SSH}"
+ export SSH_AUTHORIZED_KEY
+ export CTID="$CT_ID"
+ export CTTYPE="$CT_TYPE"
+ export ENABLE_FUSE="$ENABLE_FUSE"
+ export ENABLE_TUN="$ENABLE_TUN"
+ export PCT_OSTYPE="$var_os"
+ export PCT_OSVERSION="$var_version"
+ export PCT_DISK_SIZE="$DISK_SIZE"
+ export PCT_OPTIONS="
+ -features $FEATURES
+ -hostname $HN
+ -tags $TAGS
+ $SD
+ $NS
+ $NET_STRING
+ -onboot 1
+ -cores $CORE_COUNT
+ -memory $RAM_SIZE
+ -unprivileged $CT_TYPE
+ $PW
+"
+ export TEMPLATE_STORAGE="${var_template_storage:-}"
+ export CONTAINER_STORAGE="${var_container_storage:-}"
+ create_lxc_container || exit $?
+
+ LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
+
+ # ============================================================================
+ # GPU/USB PASSTHROUGH CONFIGURATION
+ # ============================================================================
+
+ # List of applications that benefit from GPU acceleration
+ GPU_APPS=(
+ "immich" "channels" "emby" "ersatztv" "frigate"
+ "jellyfin" "plex" "scrypted" "tdarr" "unmanic"
+ "ollama" "fileflows" "open-webui" "tunarr" "debian"
+ "handbrake" "sunshine" "moonlight" "kodi" "stremio"
+ "viseron"
+ )
+
+ # Check if app needs GPU
+ is_gpu_app() {
+ local app="${1,,}"
+ for gpu_app in "${GPU_APPS[@]}"; do
+ [[ "$app" == "${gpu_app,,}" ]] && return 0
+ done
+ return 1
+ }
+
+ # Detect all available GPU devices
+ detect_gpu_devices() {
+ INTEL_DEVICES=()
+ AMD_DEVICES=()
+ NVIDIA_DEVICES=()
+
+ # Store PCI info to avoid multiple calls
+ local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D")
+
+ # Check for Intel GPU - look for Intel vendor ID [8086]
+ if echo "$pci_vga_info" | grep -q "\[8086:"; then
+ msg_info "Detected Intel GPU"
+ if [[ -d /dev/dri ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && INTEL_DEVICES+=("$d")
+ done
+ fi
+ fi
+
+ # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
+ if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then
+ msg_info "Detected AMD GPU"
+ if [[ -d /dev/dri ]]; then
+ # Only add if not already claimed by Intel
+ if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && AMD_DEVICES+=("$d")
+ done
+ fi
+ fi
+ fi
+
+ # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
+ if echo "$pci_vga_info" | grep -q "\[10de:"; then
+ msg_info "Detected NVIDIA GPU"
+ if ! check_nvidia_host_setup; then
+ msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough."
+ msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually."
+ return 0
+ fi
+
+ for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do
+ [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
+ done
+
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found"
+ msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver"
+ else
+ if [[ "$CT_TYPE" == "0" ]]; then
+ cat <>"$LXC_CONFIG"
+ # NVIDIA GPU Passthrough (privileged)
+ lxc.cgroup2.devices.allow: c 195:* rwm
+ lxc.cgroup2.devices.allow: c 243:* rwm
+ lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file
+EOF
+
+ if [[ -e /dev/dri/renderD128 ]]; then
+ echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+
+ export GPU_TYPE="NVIDIA"
+ export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
+ msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})"
+ else
+ msg_warn "NVIDIA passthrough only supported for privileged containers"
+ return 0
+ fi
+ fi
+ fi
+
+ # Debug output
+ msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
+ msg_debug "AMD devices: ${AMD_DEVICES[*]}"
+ msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
+ }
+
+ # Configure USB passthrough for privileged containers
+ configure_usb_passthrough() {
+ if [[ "$CT_TYPE" != "0" ]]; then
+ return 0
+ fi
+
+ msg_info "Configuring automatic USB passthrough (privileged container)"
+ cat <>"$LXC_CONFIG"
+# Automatic USB passthrough (privileged container)
+lxc.cgroup2.devices.allow: a
+lxc.cap.drop:
+lxc.cgroup2.devices.allow: c 188:* rwm
+lxc.cgroup2.devices.allow: c 189:* rwm
+lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
+lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
+EOF
+ msg_ok "USB passthrough configured"
+ }
+
+ # Configure GPU passthrough
+ configure_gpu_passthrough() {
+ # Skip if not a GPU app and not privileged
+ if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then
+ return 0
+ fi
+
+ detect_gpu_devices
+
+ # Count available GPU types
+ local gpu_count=0
+ local available_gpus=()
+
+ if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("INTEL")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("AMD")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("NVIDIA")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ $gpu_count -eq 0 ]]; then
+ msg_info "No GPU devices found for passthrough"
+ return 0
+ fi
+
+ local selected_gpu=""
+
+ if [[ $gpu_count -eq 1 ]]; then
+ # Automatic selection for single GPU
+ selected_gpu="${available_gpus[0]}"
+ msg_info "Automatically configuring ${selected_gpu} GPU passthrough"
+ else
+ # Multiple GPUs - ask user
+ echo -e "\n${INFO} Multiple GPU types detected:"
+ for gpu in "${available_gpus[@]}"; do
+ echo " - $gpu"
+ done
+ read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
+ selected_gpu="${selected_gpu^^}"
+
+ # Validate selection
+ local valid=0
+ for gpu in "${available_gpus[@]}"; do
+ [[ "$selected_gpu" == "$gpu" ]] && valid=1
+ done
+
+ if [[ $valid -eq 0 ]]; then
+ msg_warn "Invalid selection. Skipping GPU passthrough."
+ return 0
+ fi
+ fi
+
+ # Apply passthrough configuration based on selection
+ local dev_idx=0
+
+ case "$selected_gpu" in
+ INTEL | AMD)
+ local devices=()
+ [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
+ [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
+
+ # For Proxmox WebUI visibility, add as dev0, dev1 etc.
+ for dev in "${devices[@]}"; do
+ if [[ "$CT_TYPE" == "0" ]]; then
+ # Privileged container - use dev entries for WebUI visibility
+ # Use initial GID 104 (render) for renderD*, 44 (video) for card*
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+
+ # Also add cgroup allows for privileged containers
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ else
+ # Unprivileged container
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+ fi
+ done
+
+ export GPU_TYPE="$selected_gpu"
+ msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)"
+ ;;
+
+ NVIDIA)
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver"
+ return 1
+ fi
+
+ for dev in "${NVIDIA_DEVICES[@]}"; do
+ # NVIDIA devices typically need different handling
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ dev_idx=$((dev_idx + 1))
+
+ if [[ "$CT_TYPE" == "0" ]]; then
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ fi
+ done
+
+ export GPU_TYPE="NVIDIA"
+ msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)"
+ ;;
+ esac
+ }
+
+ # Additional device passthrough
+ configure_additional_devices() {
+ # TUN device passthrough
+ if [ "$ENABLE_TUN" == "yes" ]; then
+ cat <>"$LXC_CONFIG"
+lxc.cgroup2.devices.allow: c 10:200 rwm
+lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
+EOF
+ fi
+
+ # Coral TPU passthrough
+ if [[ -e /dev/apex_0 ]]; then
+ msg_info "Detected Coral TPU - configuring passthrough"
+ echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+ }
+
+ # Execute pre-start configurations
+ configure_usb_passthrough
+ configure_gpu_passthrough
+ configure_additional_devices
+
+ # ============================================================================
+ # START CONTAINER AND INSTALL USERLAND
+ # ============================================================================
+
+ msg_info "Starting LXC Container"
+ pct start "$CTID"
+
+ # Wait for container to be running
+ for i in {1..10}; do
+ if pct status "$CTID" | grep -q "status: running"; then
+ msg_ok "Started LXC Container"
+ break
+ fi
+ sleep 1
+ if [ "$i" -eq 10 ]; then
+ msg_error "LXC Container did not reach running state"
+ exit 1
+ fi
+ done
+
+ # Wait for network (skip for Alpine initially)
+ if [ "$var_os" != "alpine" ]; then
+ msg_info "Waiting for network in LXC container"
+
+ # Wait for IP
+ for i in {1..20}; do
+ ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+ [ -n "$ip_in_lxc" ] && break
+ sleep 1
+ done
+
+ if [ -z "$ip_in_lxc" ]; then
+ msg_error "No IP assigned to CT $CTID after 20s"
+ exit 1
+ fi
+
+ # Try to reach gateway
+ gw_ok=0
+ for i in {1..10}; do
+ if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then
+ gw_ok=1
+ break
+ fi
+ sleep 1
+ done
+
+ if [ "$gw_ok" -eq 1 ]; then
+ msg_ok "Network in LXC is reachable (IP $ip_in_lxc)"
+ else
+ msg_warn "Network reachable but gateway check failed"
+ fi
+ fi
+ # Function to get correct GID inside container
+ get_container_gid() {
+ local group="$1"
+ local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
+ echo "${gid:-44}" # Default to 44 if not found
+ }
+
+ fix_gpu_gids
+
+ # Continue with standard container setup
+ msg_info "Customizing LXC Container"
+
+ # # Install GPU userland if configured
+ # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
+ # install_gpu_userland "VAAPI"
+ # fi
+
+ # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
+ # install_gpu_userland "NVIDIA"
+ # fi
+
+ # Continue with standard container setup
+ if [ "$var_os" == "alpine" ]; then
+ sleep 3
+ pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
+EOF'
+ pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
+ else
+ sleep 3
+ pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
+ pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
+ echo LANG=\$locale_line >/etc/default/locale && \
+ locale-gen >/dev/null && \
+ export LANG=\$locale_line"
+
+ if [[ -z "${tz:-}" ]]; then
+ tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
+ fi
+
+ if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
+ pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
+ else
+ msg_warn "Skipping timezone setup – zone '$tz' not found in container"
+ fi
+
+ pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || {
+ msg_error "apt-get base packages installation failed"
+ exit 1
+ }
+ fi
+
+ msg_ok "Customized LXC Container"
+
+ # Verify GPU access if enabled
+ if [[ "${ENABLE_VAAPI:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "vainfo >/dev/null 2>&1" &&
+ msg_ok "VAAPI verified working" ||
+ msg_warn "VAAPI verification failed - may need additional configuration"
+ fi
+
+ if [[ "${ENABLE_NVIDIA:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "nvidia-smi >/dev/null 2>&1" &&
+ msg_ok "NVIDIA verified working" ||
+ msg_warn "NVIDIA verification failed - may need additional configuration"
+ fi
+
+ # Install SSH keys
+ install_ssh_keys_into_ct
+
+ # Run application installer
+ if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then
+ exit $?
+ fi
+}
+
+destroy_lxc() {
+ if [[ -z "$CT_ID" ]]; then
+ msg_error "No CT_ID found. Nothing to remove."
+ return 1
+ fi
+
+ # Abbruch bei Ctrl-C / Ctrl-D / ESC
+ trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
+
+ local prompt
+ if ! read -rp "Remove this Container? " prompt; then
+ # read gibt != 0 zurück bei Ctrl-D/ESC
+ msg_error "Aborted input (Ctrl-D/ESC)"
+ return 130
+ fi
+
+ case "${prompt,,}" in
+ y | yes)
+ if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
+ msg_ok "Removed Container $CT_ID"
+ else
+ msg_error "Failed to remove Container $CT_ID"
+ return 1
+ fi
+ ;;
+ "" | n | no)
+ msg_info "Container was not removed."
+ ;;
+ *)
+ msg_warn "Invalid response. Container was not removed."
+ ;;
+ esac
+}
+
+# ------------------------------------------------------------------------------
+# Storage discovery / selection helpers
+# ------------------------------------------------------------------------------
+# ===== Storage discovery / selection helpers (ported from create_lxc.sh) =====
+resolve_storage_preselect() {
+ local class="$1" preselect="$2" required_content=""
+ case "$class" in
+ template) required_content="vztmpl" ;;
+ container) required_content="rootdir" ;;
+ *) return 1 ;;
+ 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
+ fi
+
+ local line total used free
+ line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
+ if [[ -z "$line" ]]; then
+ STORAGE_INFO="n/a"
+ else
+ total="$(awk '{print $4}' <<<"$line")"
+ used="$(awk '{print $5}' <<<"$line")"
+ free="$(awk '{print $6}' <<<"$line")"
+ local total_h used_h free_h
+ if command -v numfmt >/dev/null 2>&1; then
+ total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
+ used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
+ free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
+ STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
+ else
+ STORAGE_INFO="Free: ${free} Used: ${used}"
+ fi
+ fi
+ STORAGE_RESULT="$preselect"
+ return 0
+}
+
+fix_gpu_gids() {
+ if [[ -z "${GPU_TYPE:-}" ]]; then
+ return 0
+ fi
+
+ msg_info "Detecting and setting correct GPU group IDs"
+
+ # Ermittle die tatsächlichen GIDs aus dem Container
+ local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+
+ # Fallbacks wenn Gruppen nicht existieren
+ if [[ -z "$video_gid" ]]; then
+ # Versuche die video Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true"
+ video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback
+ fi
+
+ if [[ -z "$render_gid" ]]; then
+ # Versuche die render Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true"
+ render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+ [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback
+ fi
+
+ msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}"
+
+ # Prüfe ob die GIDs von den Defaults abweichen
+ local need_update=0
+ if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then
+ need_update=1
+ fi
+
+ if [[ $need_update -eq 1 ]]; then
+ msg_info "Updating device GIDs in container config"
+
+ # Stoppe Container für Config-Update
+ pct stop "$CTID" >/dev/null 2>&1
+
+ # Update die dev Einträge mit korrekten GIDs
+ # Backup der Config
+ cp "$LXC_CONFIG" "${LXC_CONFIG}.bak"
+
+ # Parse und update jeden dev Eintrag
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^dev[0-9]+: ]]; then
+ # Extract device path
+ local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/')
+ local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/')
+
+ if [[ "$device_path" =~ renderD ]]; then
+ # RenderD device - use render GID
+ echo "${dev_num}: ${device_path},gid=${render_gid}"
+ elif [[ "$device_path" =~ card ]]; then
+ # Card device - use video GID
+ echo "${dev_num}: ${device_path},gid=${video_gid}"
+ else
+ # Keep original line
+ echo "$line"
+ fi
+ else
+ # Keep non-dev lines
+ echo "$line"
+ fi
+ done <"$LXC_CONFIG" >"${LXC_CONFIG}.new"
+
+ mv "${LXC_CONFIG}.new" "$LXC_CONFIG"
+
+ # Starte Container wieder
+ pct start "$CTID" >/dev/null 2>&1
+ sleep 3
+
+ msg_ok "Device GIDs updated successfully"
+ else
+ msg_ok "Device GIDs are already correct"
+ fi
+ if [[ "$CT_TYPE" == "0" ]]; then
+ pct exec "$CTID" -- bash -c "
+ if [ -d /dev/dri ]; then
+ for dev in /dev/dri/*; do
+ if [ -e \"\$dev\" ]; then
+ if [[ \"\$dev\" =~ renderD ]]; then
+ chgrp ${render_gid} \"\$dev\" 2>/dev/null || true
+ else
+ chgrp ${video_gid} \"\$dev\" 2>/dev/null || true
+ fi
+ chmod 660 \"\$dev\" 2>/dev/null || true
+ fi
+ done
+ fi
+ " >/dev/null 2>&1
+ fi
+}
+
+# NVIDIA-spezific check on host
+check_nvidia_host_setup() {
+ if ! command -v nvidia-smi >/dev/null 2>&1; then
+ msg_warn "NVIDIA GPU detected but nvidia-smi not found on host"
+ msg_warn "Please install NVIDIA drivers on host first."
+ #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run"
+ #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms"
+ #echo " 3. Verify: nvidia-smi"
+ return 1
+ fi
+
+ # check if nvidia-smi works
+ if ! nvidia-smi >/dev/null 2>&1; then
+ msg_warn "nvidia-smi installed but not working. Driver issue?"
+ return 1
+ fi
+
+ return 0
+}
+
+check_storage_support() {
+ local CONTENT="$1" VALID=0
+ while IFS= read -r line; do
+ local STORAGE_NAME
+ STORAGE_NAME=$(awk '{print $1}' <<<"$line")
+ [[ -n "$STORAGE_NAME" ]] && VALID=1
+ done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
+ [[ $VALID -eq 1 ]]
+}
+
+select_storage() {
+ local CLASS=$1 CONTENT CONTENT_LABEL
+ case $CLASS in
+ container)
+ CONTENT='rootdir'
+ CONTENT_LABEL='Container'
+ ;;
+ template)
+ CONTENT='vztmpl'
+ CONTENT_LABEL='Container template'
+ ;;
+ iso)
+ CONTENT='iso'
+ CONTENT_LABEL='ISO image'
+ ;;
+ images)
+ CONTENT='images'
+ CONTENT_LABEL='VM Disk image'
+ ;;
+ backup)
+ CONTENT='backup'
+ CONTENT_LABEL='Backup'
+ ;;
+ snippets)
+ CONTENT='snippets'
+ CONTENT_LABEL='Snippets'
+ ;;
+ *)
+ msg_error "Invalid storage class '$CLASS'"
+ return 1
+ ;;
+ esac
+
+ declare -A STORAGE_MAP
+ local -a MENU=()
+ local COL_WIDTH=0
+
+ while read -r TAG TYPE _ TOTAL USED FREE _; do
+ [[ -n "$TAG" && -n "$TYPE" ]] || continue
+ local DISPLAY="${TAG} (${TYPE})"
+ local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
+ local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
+ local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
+ STORAGE_MAP["$DISPLAY"]="$TAG"
+ MENU+=("$DISPLAY" "$INFO" "OFF")
+ ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
+ done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
+
+ if [[ ${#MENU[@]} -eq 0 ]]; then
+ msg_error "No storage found for content type '$CONTENT'."
+ return 2
+ fi
+
+ if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
+ STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
+ STORAGE_INFO="${MENU[1]}"
+ return 0
+ fi
+
+ local WIDTH=$((COL_WIDTH + 42))
+ while true; do
+ local DISPLAY_SELECTED
+ DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Storage Pools" \
+ --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
+ 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }
+
+ DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
+ if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
+ whiptail --msgbox "No valid storage selected. Please try again." 8 58
+ continue
+ fi
+ STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
+ for ((i = 0; i < ${#MENU[@]}; i += 3)); do
+ if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
+ STORAGE_INFO="${MENU[$i + 1]}"
+ break
+ fi
+ done
+ return 0
+ done
+}
+
+create_lxc_container() {
+ # ------------------------------------------------------------------------------
+ # Optional verbose mode (debug tracing)
+ # ------------------------------------------------------------------------------
+ if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
+
+ # ------------------------------------------------------------------------------
+ # Helpers (dynamic versioning / template parsing)
+ # ------------------------------------------------------------------------------
+ pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
+ pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
+
+ ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
+ ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
+ ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
+
+ # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
+ parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
+
+ # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
+ # Returns:
+ # 0 = no upgrade needed
+ # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
+ # 2 = user declined
+ # 3 = upgrade attempted but failed OR retry failed
+ offer_lxc_stack_upgrade_and_maybe_retry() {
+ local do_retry="${1:-no}" # yes|no
+ local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
+
+ _pvec_i="$(pkg_ver pve-container)"
+ _lxcp_i="$(pkg_ver lxc-pve)"
+ _pvec_c="$(pkg_cand pve-container)"
+ _lxcp_c="$(pkg_cand lxc-pve)"
+
+ if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
+ ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
+ fi
+ if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
+ ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
+ fi
+ if [[ $need -eq 0 ]]; then
+ msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
+ return 0
+ fi
+
+ echo
+ echo "An update for the Proxmox LXC stack is available:"
+ echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
+ echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
+ echo
+ read -rp "Do you want to upgrade now? [y/N] " _ans
+ case "${_ans,,}" in
+ y | yes)
+ msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
+ if apt-get update -qq >/dev/null && apt-get install -y --only-upgrade pve-container lxc-pve >/dev/null; then
+ msg_ok "LXC stack upgraded."
+ if [[ "$do_retry" == "yes" ]]; then
+ msg_info "Retrying container creation after upgrade"
+ if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container created successfully after upgrade."
+ return 0
+ else
+ msg_error "pct create still failed after upgrade. See $LOGFILE"
+ return 3
+ fi
+ fi
+ return 1
+ else
+ msg_error "Upgrade failed. Please check APT output."
+ return 3
+ fi
+ ;;
+ *) return 2 ;;
+ esac
+ }
+
+ # ------------------------------------------------------------------------------
+ # Required input variables
+ # ------------------------------------------------------------------------------
+ [[ "${CTID:-}" ]] || {
+ msg_error "You need to set 'CTID' variable."
+ exit 203
+ }
+ [[ "${PCT_OSTYPE:-}" ]] || {
+ msg_error "You need to set 'PCT_OSTYPE' variable."
+ exit 204
+ }
+
+ msg_debug "CTID=$CTID"
+ msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
+ msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
+
+ # ID checks
+ [[ "$CTID" -ge 100 ]] || {
+ msg_error "ID cannot be less than 100."
+ exit 205
+ }
+ if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
+ echo -e "ID '$CTID' is already in use."
+ unset CTID
+ msg_error "Cannot use ID that is already in use."
+ exit 206
+ fi
+
+ # Storage capability check
+ check_storage_support "rootdir" || {
+ msg_error "No valid storage found for 'rootdir' [Container]"
+ exit 1
+ }
+ check_storage_support "vztmpl" || {
+ msg_error "No valid storage found for 'vztmpl' [Template]"
+ exit 1
+ }
+
+ # Template storage selection
+ if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ else
+ while true; do
+ if [[ -z "${var_template_storage:-}" ]]; then
+ if select_storage template; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ break
+ fi
+ fi
+ done
+ fi
+
+ # Container storage selection
+ if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ else
+ if [[ -z "${var_container_storage:-}" ]]; then
+ if select_storage container; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ fi
+ fi
+ fi
+
+ # Validate content types
+ msg_info "Validating content types of storage '$CONTAINER_STORAGE'"
+ STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT"
+ grep -qw "rootdir" <<<"$STORAGE_CONTENT" || {
+ msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC."
+ exit 217
+ }
+ $STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'"
+
+ msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'"
+ TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT"
+ if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then
+ msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail."
+ else
+ $STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'"
+ fi
+
+ # Free space check
+ STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
+ REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
+ [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || {
+ msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
+ exit 214
+ }
+
+ # Cluster quorum (if cluster)
+ if [[ -f /etc/pve/corosync.conf ]]; then
+ msg_info "Checking cluster quorum"
+ if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
+ msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
+ exit 210
+ fi
+ msg_ok "Cluster is quorate"
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Template discovery & validation
+ # ------------------------------------------------------------------------------
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ case "$PCT_OSTYPE" in
+ debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
+ alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
+ *) TEMPLATE_PATTERN="" ;;
+ esac
+
+ msg_info "Searching for template '$TEMPLATE_SEARCH'"
+
+ # Build regex patterns outside awk/grep for clarity
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}"
+
+ #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'"
+ #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'"
+ #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+
+ pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)."
+
+ #echo "[DEBUG] pveam available output (first 5 lines with .tar files):"
+ #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /'
+
+ set +u
+ mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
+ #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found"
+ set -u
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #echo "[DEBUG] Online templates:"
+ for tmpl in "${ONLINE_TEMPLATES[@]}"; do
+ echo " - $tmpl"
+ done
+ fi
+
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'"
+ #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates"
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #msg_debug "First 3 online templates:"
+ count=0
+ for idx in "${!ONLINE_TEMPLATES[@]}"; do
+ #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}"
+ ((count++))
+ [[ $count -ge 3 ]] && break
+ done
+ fi
+ #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ # If still no template, try to find alternatives
+ if [[ -z "$TEMPLATE" ]]; then
+ echo ""
+ echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."
+
+ # Get all available versions for this OS type
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" |
+ sort -u -V 2>/dev/null
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo ""
+ echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ #echo "[DEBUG] Retrying with version: $PCT_OSVERSION"
+
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="online"
+ #echo "[DEBUG] Found alternative: $TEMPLATE"
+ else
+ msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ exit 225
+ fi
+ else
+ msg_info "Installation cancelled"
+ exit 0
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available at all"
+ exit 225
+ fi
+ fi
+
+ #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+ #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"
+
+ # Get available versions
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
+ grep -E '^[0-9]+\.[0-9]+$' |
+ sort -u -V 2>/dev/null || sort -u
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo -e "\n${BL}Available versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ export PCT_OSVERSION="$var_version"
+ msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"
+
+ # Retry template search with new version
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ msg_error "Template still not found after version change"
+ exit 220
+ }
+ else
+ msg_info "Installation cancelled"
+ exit 1
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available"
+ exit 220
+ fi
+ fi
+ }
+
+ # Validate that we found a template
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ msg_info "Please check:"
+ msg_info " - Is pveam catalog available? (run: pveam available -section system)"
+ msg_info " - Does the template exist for your OS version?"
+ exit 225
+ fi
+
+ msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
+ msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
+
+ NEED_DOWNLOAD=0
+ if [[ ! -f "$TEMPLATE_PATH" ]]; then
+ msg_info "Template not present locally – will download."
+ NEED_DOWNLOAD=1
+ elif [[ ! -r "$TEMPLATE_PATH" ]]; then
+ msg_error "Template file exists but is not readable – check permissions."
+ exit 221
+ elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template file too small (<1MB) – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template looks too small, but no online version exists. Keeping local file."
+ fi
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
+ fi
+ else
+ $STD msg_ok "Template $TEMPLATE is present and valid."
+ fi
+
+ if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
+ if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
+ TEMPLATE="$ONLINE_TEMPLATE"
+ NEED_DOWNLOAD=1
+ else
+ msg_info "Continuing with local template $TEMPLATE"
+ fi
+ fi
+
+ if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
+ [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
+ for attempt in {1..3}; do
+ msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
+ if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
+ msg_ok "Template download successful."
+ break
+ fi
+ if [[ $attempt -eq 3 ]]; then
+ msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
+ exit 222
+ fi
+ sleep $((attempt * 5))
+ done
+ fi
+
+ if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
+ msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
+ exit 223
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
+ # ------------------------------------------------------------------------------
+ if [[ "$PCT_OSTYPE" == "debian" ]]; then
+ OSVER="$(parse_template_osver "$TEMPLATE")"
+ if [[ -n "$OSVER" ]]; then
+ # Proactive, aber ohne Abbruch – nur Angebot
+ offer_lxc_stack_upgrade_and_maybe_retry "no" || true
+ fi
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Create LXC Container
+ # ------------------------------------------------------------------------------
+ msg_info "Creating LXC container"
+
+ # Ensure subuid/subgid entries exist
+ grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
+ grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
+
+ # Assemble pct options
+ PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
+ [[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
+
+ # Lock by template file (avoid concurrent downloads/creates)
+ lockfile="/tmp/template.${TEMPLATE}.lock"
+ exec 9>"$lockfile" || {
+ msg_error "Failed to create lock file '$lockfile'."
+ exit 200
+ }
+ flock -w 60 9 || {
+ msg_error "Timeout while waiting for template lock."
+ exit 211
+ }
+
+ LOGFILE="/tmp/pct_create_${CTID}.log"
+ msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}"
+ msg_debug "Logfile: $LOGFILE"
+
+ # First attempt
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then
+ msg_error "Container creation failed on ${TEMPLATE_STORAGE}. Checking template..."
+
+ # Validate template file
+ if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ msg_warn "Template file too small or missing – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
+ fi
+ fi
+
+ # Retry after repair
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ # Fallback to local storage
+ if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
+ msg_warn "Retrying container creation with fallback to local storage..."
+ LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
+ msg_info "Downloading template to local..."
+ pveam download local "$TEMPLATE" >/dev/null 2>&1
+ fi
+ if pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container successfully created using local fallback."
+ else
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed even with local fallback. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ else
+ msg_error "Container creation failed on local storage. See $LOGFILE"
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ fi
+ fi
+
+ # Verify container exists
+ pct list | awk '{print $1}' | grep -qx "$CTID" || {
+ msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE"
+ exit 215
+ }
+
+ # Verify config rootfs
+ grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
+ msg_error "RootFS entry missing in container config. See $LOGFILE"
+ exit 216
+ }
+
+ msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
+}
+
+# ------------------------------------------------------------------------------
+# description()
+#
+# - Sets container description with HTML content (logo, links, badges)
+# - Restarts ping-instances.service if present
+# - Posts status "done" to API
+# ------------------------------------------------------------------------------
+description() {
+ IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+
+ # Generate LXC Description
+ DESCRIPTION=$(
+ cat <
+
+
+
+
+ ${APP} LXC
+
+
+
+
+
+
+
+
+
+ GitHub
+
+
+
+ Discussions
+
+
+
+ Issues
+
+
+EOF
+ )
+ pct set "$CTID" -description "$DESCRIPTION"
+
+ if [[ -f /etc/systemd/system/ping-instances.service ]]; then
+ systemctl start ping-instances.service
+ fi
+
+ post_update_to_api "done" "none"
+}
+
+# ------------------------------------------------------------------------------
+# api_exit_script()
+#
+# - Exit trap handler
+# - Reports exit codes to API with detailed reason
+# - Handles known codes (100–209) and maps them to errors
+# ------------------------------------------------------------------------------
+api_exit_script() {
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then
+ case $exit_code in
+ 100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;;
+ 101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;;
+ 200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;;
+ 201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;;
+ 202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;;
+ 203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;;
+ 204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;;
+ 205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;;
+ 206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;;
+ 207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;;
+ 208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;;
+ 209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;;
+ *) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;;
+ esac
+ fi
+}
+
+if command -v pveversion >/dev/null 2>&1; then
+ trap 'api_exit_script' EXIT
+fi
+trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
+trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
+trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
diff --git a/misc/deferred/build.func.backup-20251029-124205 b/misc/deferred/build.func.backup-20251029-124205
new file mode 100644
index 000000000..7e0556d61
--- /dev/null
+++ b/misc/deferred/build.func.backup-20251029-124205
@@ -0,0 +1,3516 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# Revision: 1
+
+# ==============================================================================
+# SECTION 1: CORE INITIALIZATION & VARIABLES
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# variables()
+#
+# - Normalize application name (NSAPP = lowercase, no spaces)
+# - Build installer filename (var_install)
+# - Define regex for integer validation
+# - Fetch hostname of Proxmox node
+# - Set default values for diagnostics/method
+# - Generate random UUID for tracking
+# - Get Proxmox VE version and kernel version
+# ------------------------------------------------------------------------------
+variables() {
+ NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
+ var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
+ INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
+ PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
+ DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
+ METHOD="default" # sets the METHOD variable to "default", used for the API call.
+ RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
+ CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
+ #CT_TYPE=${var_unprivileged:-$CT_TYPE}
+
+ # Get Proxmox VE version and kernel version
+ if command -v pveversion >/dev/null 2>&1; then
+ PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1)
+ else
+ PVEVERSION="N/A"
+ fi
+ KERNEL_VERSION=$(uname -r)
+}
+
+# -----------------------------------------------------------------------------
+# Community-Scripts bootstrap loader
+# - Always sources build.func from remote
+# - Updates local core files only if build.func changed
+# - Local cache: /usr/local/community-scripts/core
+# -----------------------------------------------------------------------------
+
+# FUNC_DIR="/usr/local/community-scripts/core"
+# mkdir -p "$FUNC_DIR"
+
+# BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func"
+# BUILD_REV="$FUNC_DIR/build.rev"
+# DEVMODE="${DEVMODE:-no}"
+
+# # --- Step 1: fetch build.func content once, compute hash ---
+# build_content="$(curl -fsSL "$BUILD_URL")" || {
+# echo "❌ Failed to fetch build.func"
+# exit 1
+# }
+
+# newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}')
+# oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "")
+
+# # --- Step 2: if build.func changed, offer update for core files ---
+# if [ "$newhash" != "$oldhash" ]; then
+# echo "⚠️ build.func changed!"
+
+# while true; do
+# read -rp "Refresh local core files? [y/N/diff]: " ans
+# case "$ans" in
+# [Yy]*)
+# echo "$newhash" >"$BUILD_REV"
+
+# update_func_file() {
+# local file="$1"
+# local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file"
+# local local_path="$FUNC_DIR/$file"
+
+# echo "⬇️ Downloading $file ..."
+# curl -fsSL "$url" -o "$local_path" || {
+# echo "❌ Failed to fetch $file"
+# exit 1
+# }
+# echo "✔️ Updated $file"
+# }
+
+# update_func_file core.func
+# update_func_file error_handler.func
+# update_func_file tools.func
+# break
+# ;;
+# [Dd]*)
+# for file in core.func error_handler.func tools.func; do
+# local_path="$FUNC_DIR/$file"
+# url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file"
+# remote_tmp="$(mktemp)"
+
+# curl -fsSL "$url" -o "$remote_tmp" || continue
+
+# if [ -f "$local_path" ]; then
+# echo "🔍 Diff for $file:"
+# diff -u "$local_path" "$remote_tmp" || echo "(no differences)"
+# else
+# echo "📦 New file $file will be installed"
+# fi
+
+# rm -f "$remote_tmp"
+# done
+# ;;
+# *)
+# echo "❌ Skipped updating local core files"
+# break
+# ;;
+# esac
+# done
+# else
+# if [ "$DEVMODE" != "yes" ]; then
+# echo "✔️ build.func unchanged → using existing local core files"
+# fi
+# fi
+
+# if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then
+# return 0 2>/dev/null || exit 0
+# fi
+# _COMMUNITY_SCRIPTS_LOADER=1
+
+# # --- Step 3: always source local versions of the core files ---
+# source "$FUNC_DIR/core.func"
+# source "$FUNC_DIR/error_handler.func"
+# source "$FUNC_DIR/tools.func"
+
+# # --- Step 4: finally, source build.func directly from memory ---
+# # (no tmp file needed)
+# source <(printf "%s" "$build_content")
+
+# ------------------------------------------------------------------------------
+# Load core + error handler functions from community-scripts repo
+#
+# - Prefer curl if available, fallback to wget
+# - Load: core.func, error_handler.func, api.func
+# - Initialize error traps after loading
+# ------------------------------------------------------------------------------
+
+source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func)
+
+if command -v curl >/dev/null 2>&1; then
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via curl"
+elif command -v wget >/dev/null 2>&1; then
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via wget"
+fi
+
+# ------------------------------------------------------------------------------
+# maxkeys_check()
+#
+# - Reads kernel keyring limits (maxkeys, maxbytes)
+# - Checks current usage for LXC user (UID 100000)
+# - Warns if usage is close to limits and suggests sysctl tuning
+# - Exits if thresholds are exceeded
+# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
+# ------------------------------------------------------------------------------
+
+maxkeys_check() {
+ # Read kernel parameters
+ per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
+ per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
+
+ # Exit if kernel parameters are unavailable
+ if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
+ echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
+ exit 1
+ fi
+
+ # Fetch key usage for user ID 100000 (typical for containers)
+ used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
+ used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
+
+ # Calculate thresholds and suggested new limits
+ threshold_keys=$((per_user_maxkeys - 100))
+ threshold_bytes=$((per_user_maxbytes - 1000))
+ new_limit_keys=$((per_user_maxkeys * 2))
+ new_limit_bytes=$((per_user_maxbytes * 2))
+
+ # Check if key or byte usage is near limits
+ failure=0
+ if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+ if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+
+ # Provide next steps if issues are detected
+ if [[ "$failure" -eq 1 ]]; then
+ echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
+ exit 1
+ fi
+
+ echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
+}
+
+# ------------------------------------------------------------------------------
+# get_current_ip()
+#
+# - Returns current container IP depending on OS type
+# - Debian/Ubuntu: uses `hostname -I`
+# - Alpine: parses eth0 via `ip -4 addr`
+# ------------------------------------------------------------------------------
+get_current_ip() {
+ if [ -f /etc/os-release ]; then
+ # Check for Debian/Ubuntu (uses hostname -I)
+ if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
+ CURRENT_IP=$(hostname -I | awk '{print $1}')
+ # Check for Alpine (uses ip command)
+ elif grep -q 'ID=alpine' /etc/os-release; then
+ CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
+ else
+ CURRENT_IP="Unknown"
+ fi
+ fi
+ echo "$CURRENT_IP"
+}
+
+# ------------------------------------------------------------------------------
+# update_motd_ip()
+#
+# - Updates /etc/motd with current container IP
+# - Removes old IP entries to avoid duplicates
+# ------------------------------------------------------------------------------
+update_motd_ip() {
+ MOTD_FILE="/etc/motd"
+
+ if [ -f "$MOTD_FILE" ]; then
+ # Remove existing IP Address lines to prevent duplication
+ sed -i '/IP Address:/d' "$MOTD_FILE"
+
+ IP=$(get_current_ip)
+ # Add the new IP address
+ echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# install_ssh_keys_into_ct()
+#
+# - Installs SSH keys into container root account if SSH is enabled
+# - Uses pct push or direct input to authorized_keys
+# - Falls back to warning if no keys provided
+# ------------------------------------------------------------------------------
+install_ssh_keys_into_ct() {
+ [[ "$SSH" != "yes" ]] && return 0
+
+ if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
+ 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
+ }
+ 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
+ }
+ pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
+ msg_ok "Installed SSH keys into CT ${CTID}"
+ return 0
+ fi
+
+ # Fallback: nichts ausgewählt
+ msg_warn "No SSH keys to install (skipping)."
+ return 0
+}
+
+# ------------------------------------------------------------------------------
+# base_settings()
+#
+# - Defines all base/default variables for container creation
+# - Reads from environment variables (var_*)
+# - Provides fallback defaults for OS type/version
+# ------------------------------------------------------------------------------
+base_settings() {
+ # Default Settings
+ CT_TYPE=${var_unprivileged:-"1"}
+ DISK_SIZE=${var_disk:-"4"}
+ CORE_COUNT=${var_cpu:-"1"}
+ RAM_SIZE=${var_ram:-"1024"}
+ VERBOSE=${var_verbose:-"${1:-no}"}
+ PW=${var_pw:-""}
+ CT_ID=${var_ctid:-$NEXTID}
+ HN=${var_hostname:-$NSAPP}
+ BRG=${var_brg:-"vmbr0"}
+ NET=${var_net:-"dhcp"}
+ IPV6_METHOD=${var_ipv6_method:-"none"}
+ IPV6_STATIC=${var_ipv6_static:-""}
+ GATE=${var_gateway:-""}
+ APT_CACHER=${var_apt_cacher:-""}
+ APT_CACHER_IP=${var_apt_cacher_ip:-""}
+ MTU=${var_mtu:-""}
+ SD=${var_storage:-""}
+ NS=${var_ns:-""}
+ MAC=${var_mac:-""}
+ VLAN=${var_vlan:-""}
+ SSH=${var_ssh:-"no"}
+ SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
+ UDHCPC_FIX=${var_udhcpc_fix:-""}
+ TAGS="community-script,${var_tags:-}"
+ ENABLE_FUSE=${var_fuse:-"${1:-no}"}
+ ENABLE_TUN=${var_tun:-"${1:-no}"}
+
+ # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
+ if [ -z "$var_os" ]; then
+ var_os="debian"
+ fi
+ if [ -z "$var_version" ]; then
+ var_version="12"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# echo_default()
+#
+# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
+# - Uses icons and formatting for readability
+# - Convert CT_TYPE to description
+# ------------------------------------------------------------------------------
+echo_default() {
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ if [ "$VERBOSE" == "yes" ]; then
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
+ fi
+ echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
+ echo -e " "
+}
+
+# ------------------------------------------------------------------------------
+# exit_script()
+#
+# - Called when user cancels an action
+# - Clears screen and exits gracefully
+# ------------------------------------------------------------------------------
+exit_script() {
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
+}
+
+# ------------------------------------------------------------------------------
+# find_host_ssh_keys()
+#
+# - Scans system for available SSH keys
+# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
+# - Returns list of files containing valid SSH public keys
+# - Sets FOUND_HOST_KEY_COUNT to number of keys found
+# ------------------------------------------------------------------------------
+find_host_ssh_keys() {
+ local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
+ local -a files=() cand=()
+ local g="${var_ssh_import_glob:-}"
+ local total=0 f base c
+
+ shopt -s nullglob
+ if [[ -n "$g" ]]; then
+ for pat in $g; do cand+=($pat); done
+ else
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ fi
+ shopt -u nullglob
+
+ for f in "${cand[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # CRLF safe check for host keys
+ c=$(tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ {print}
+ ' | grep -E -c '"$re"' || true)
+
+ if ((c > 0)); then
+ files+=("$f")
+ total=$((total + c))
+ fi
+ done
+
+ # Fallback to /root/.ssh/authorized_keys
+ if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
+ if grep -E -q "$re" /root/.ssh/authorized_keys; then
+ files+=(/root/.ssh/authorized_keys)
+ total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
+ fi
+ fi
+
+ FOUND_HOST_KEY_COUNT="$total"
+ (
+ IFS=:
+ echo "${files[*]}"
+ )
+}
+
+# ------------------------------------------------------------------------------
+# advanced_settings()
+#
+# - Interactive whiptail menu for advanced configuration
+# - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM
+# - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode
+# - Ends with confirmation or re-entry if cancelled
+# ------------------------------------------------------------------------------
+advanced_settings() {
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
+ # Setting Default Tag for Advanced Settings
+ TAGS="community-script;${var_tags:-}"
+ CT_DEFAULT_TYPE="${CT_TYPE}"
+ CT_TYPE=""
+ while [ -z "$CT_TYPE" ]; do
+ if [ "$CT_DEFAULT_TYPE" == "1" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" ON \
+ "0" "Privileged" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os |${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ if [ "$CT_DEFAULT_TYPE" == "0" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" OFF \
+ "0" "Privileged" ON \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
+ echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ done
+
+ while true; do
+ if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
+ # Empty = Autologin
+ if [[ -z "$PW1" ]]; then
+ PW=""
+ PW1="Automatic Login"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
+ break
+ fi
+
+ # Invalid: contains spaces
+ if [[ "$PW1" == *" "* ]]; then
+ whiptail --msgbox "Password cannot contain spaces." 8 58
+ continue
+ fi
+
+ # Invalid: too short
+ if ((${#PW1} < 5)); then
+ whiptail --msgbox "Password must be at least 5 characters." 8 58
+ continue
+ fi
+
+ # Confirm password
+ if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
+ if [[ "$PW1" == "$PW2" ]]; then
+ PW="-password $PW1"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
+ break
+ else
+ whiptail --msgbox "Passwords do not match. Please try again." 8 58
+ fi
+ else
+ exit_script
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_ID" ]; then
+ CT_ID="$NEXTID"
+ fi
+ else
+ exit_script
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
+
+ while true; do
+ if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_NAME" ]; then
+ HN="$NSAPP"
+ else
+ HN=$(echo "${CT_NAME,,}" | tr -d ' ')
+ fi
+ # Hostname validate (RFC 1123)
+ if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ while true; do
+ DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$DISK_SIZE" ]; then
+ DISK_SIZE="$var_disk"
+ fi
+
+ if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ break
+ else
+ whiptail --msgbox "Disk size must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$CORE_COUNT" ]; then
+ CORE_COUNT="$var_cpu"
+ fi
+
+ if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ break
+ else
+ whiptail --msgbox "CPU core count must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$RAM_SIZE" ]; then
+ RAM_SIZE="$var_ram"
+ fi
+
+ if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ break
+ else
+ whiptail --msgbox "RAM size must be a positive integer!" 8 58
+ fi
+ done
+
+ IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
+ BRIDGES=""
+ OLD_IFS=$IFS
+ IFS=$'\n'
+ for iface_filepath in ${IFACE_FILEPATH_LIST}; do
+
+ iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
+ (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
+
+ if [ -f "${iface_indexes_tmpfile}" ]; then
+
+ while read -r pair; do
+ start=$(echo "${pair}" | cut -d':' -f1)
+ end=$(echo "${pair}" | cut -d':' -f2)
+
+ if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
+ iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
+ BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
+ fi
+
+ done <"${iface_indexes_tmpfile}"
+ rm -f "${iface_indexes_tmpfile}"
+ fi
+
+ done
+ IFS=$OLD_IFS
+ BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
+ if [[ -z "$BRIDGES" ]]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ # Build bridge menu with descriptions
+ BRIDGE_MENU_OPTIONS=()
+ while IFS= read -r bridge; do
+ if [[ -n "$bridge" ]]; then
+ # Get description from Proxmox built-in method - find comment for this specific bridge
+ description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//')
+ if [[ -n "$description" ]]; then
+ BRIDGE_MENU_OPTIONS+=("$bridge" "${description}")
+ else
+ BRIDGE_MENU_OPTIONS+=("$bridge" " ")
+ fi
+ fi
+ done <<<"$BRIDGES"
+
+ BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3)
+ if [[ -z "$BRG" ]]; then
+ exit_script
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
+ fi
+
+ # IPv4 methods: dhcp, static, none
+ while true; do
+ IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "IPv4 Address Management" \
+ --menu "Select IPv4 Address Assignment Method:" 12 60 2 \
+ "dhcp" "Automatic (DHCP, recommended)" \
+ "static" "Static (manual entry)" \
+ 3>&1 1>&2 2>&3)
+
+ exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ exit_script
+ fi
+
+ case "$IPV4_METHOD" in
+ dhcp)
+ NET="dhcp"
+ GATE=""
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}"
+ break
+ ;;
+ static)
+ # Static: call and validate CIDR address
+ while true; do
+ NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \
+ --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3)
+ if [ -z "$NET" ]; then
+ whiptail --msgbox "IPv4 address must not be empty." 8 58
+ continue
+ elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}"
+ break
+ else
+ whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58
+ fi
+ done
+
+ # call and validate Gateway
+ while true; do
+ GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \
+ --title "Gateway IP" 3>&1 1>&2 2>&3)
+ if [ -z "$GATE1" ]; then
+ whiptail --msgbox "Gateway IP address cannot be empty." 8 58
+ elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
+ whiptail --msgbox "Invalid Gateway IP address format." 8 58
+ else
+ GATE=",gw=$GATE1"
+ echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
+ break
+ fi
+ done
+ break
+ ;;
+ esac
+ done
+
+ # IPv6 Address Management selection
+ while true; do
+ IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \
+ "Select IPv6 Address Management Type:" 15 58 4 \
+ "auto" "SLAAC/AUTO (recommended, default)" \
+ "dhcp" "DHCPv6" \
+ "static" "Static (manual entry)" \
+ "none" "Disabled" \
+ --default-item "auto" 3>&1 1>&2 2>&3)
+ [ $? -ne 0 ] && exit_script
+
+ case "$IPV6_METHOD" in
+ auto)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}"
+ IPV6_ADDR=""
+ IPV6_GATE=""
+ break
+ ;;
+ dhcp)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}"
+ IPV6_ADDR="dhcp"
+ IPV6_GATE=""
+ break
+ ;;
+ static)
+ # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64)
+ while true; do
+ IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \
+ --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script
+ if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58
+ fi
+ done
+ # Optional: ask for IPv6 gateway for static config
+ while true; do
+ IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3)
+ if [ -z "$IPV6_GATE" ]; then
+ IPV6_GATE=""
+ break
+ elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "Invalid IPv6 gateway format." 8 58
+ fi
+ done
+ break
+ ;;
+ none)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}"
+ IPV6_ADDR="none"
+ IPV6_GATE=""
+ break
+ ;;
+ *)
+ exit_script
+ ;;
+ esac
+ done
+
+ if [ "$var_os" == "alpine" ]; then
+ APT_CACHER=""
+ APT_CACHER_IP=""
+ else
+ if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
+ APT_CACHER="${APT_CACHER_IP:+yes}"
+ echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
+ else
+ exit_script
+ fi
+ fi
+
+ # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then
+ # DISABLEIP6="yes"
+ # else
+ # DISABLEIP6="no"
+ # fi
+ # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}"
+
+ if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
+ if [ -z "$MTU1" ]; then
+ MTU1="Default"
+ MTU=""
+ else
+ MTU=",mtu=$MTU1"
+ fi
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ exit_script
+ fi
+
+ if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
+ if [ -z "$SD" ]; then
+ SX=Host
+ SD=""
+ else
+ SX=$SD
+ SD="-searchdomain=$SD"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
+ else
+ exit_script
+ fi
+
+ if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
+ if [ -z "$NX" ]; then
+ NX=Host
+ NS=""
+ else
+ NS="-nameserver=$NX"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
+ else
+ exit_script
+ fi
+
+ if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then
+ UDHCPC_FIX="yes"
+ else
+ UDHCPC_FIX="no"
+ fi
+ export UDHCPC_FIX
+
+ if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
+ if [ -z "$MAC1" ]; then
+ MAC1="Default"
+ MAC=""
+ else
+ MAC=",hwaddr=$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
+ else
+ exit_script
+ fi
+
+ if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
+ if [ -z "$VLAN1" ]; then
+ VLAN1="Default"
+ VLAN=""
+ else
+ VLAN=",tag=$VLAN1"
+ fi
+ echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
+ else
+ exit_script
+ fi
+
+ if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
+ if [ -n "${ADV_TAGS}" ]; then
+ ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
+ TAGS="${ADV_TAGS}"
+ else
+ TAGS=";"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
+ else
+ exit_script
+ fi
+
+ configure_ssh_settings
+ export SSH_KEYS_FILE
+ echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
+ ENABLE_FUSE="yes"
+ else
+ ENABLE_FUSE="no"
+ fi
+ echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
+ VERBOSE="yes"
+ else
+ VERBOSE="no"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
+ echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
+ else
+ clear
+ header_info
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
+ advanced_settings
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# diagnostics_check()
+#
+# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
+# - Asks user whether to send anonymous diagnostic data
+# - Saves DIAGNOSTICS=yes/no in the config file
+# ------------------------------------------------------------------------------
+diagnostics_check() {
+ if ! [ -d "/usr/local/community-scripts" ]; then
+ mkdir -p /usr/local/community-scripts
+ fi
+
+ if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=yes
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="yes"
+ else
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=no
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="no"
+ fi
+ else
+ DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
+
+ fi
+
+}
+
+# ------------------------------------------------------------------------------
+# default_var_settings
+#
+# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
+# - Loads var_* values from default.vars (safe parser, no source/eval)
+# - Precedence: ENV var_* > default.vars > built-in defaults
+# - Maps var_verbose → VERBOSE
+# - Calls base_settings "$VERBOSE" and echo_default
+# ------------------------------------------------------------------------------
+default_var_settings() {
+ # Allowed var_* keys (alphabetically sorted)
+ local VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+
+ # Snapshot: environment variables (highest precedence)
+ declare -A _HARD_ENV=()
+ local _k
+ for _k in "${VAR_WHITELIST[@]}"; do
+ if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
+ done
+
+ # Find default.vars location
+ local _find_default_vars
+ _find_default_vars() {
+ local f
+ for f in \
+ /usr/local/community-scripts/default.vars \
+ "$HOME/.config/community-scripts/default.vars" \
+ "./default.vars"; do
+ [ -f "$f" ] && {
+ echo "$f"
+ return 0
+ }
+ done
+ return 1
+ }
+ # Allow override of storages via env (for non-interactive use cases)
+ [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
+ [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
+
+ # Create once, with storages already selected, no var_ctid/var_hostname lines
+ local _ensure_default_vars
+ _ensure_default_vars() {
+ _find_default_vars >/dev/null 2>&1 && return 0
+
+ local canonical="/usr/local/community-scripts/default.vars"
+ msg_info "No default.vars found. Creating ${canonical}"
+ mkdir -p /usr/local/community-scripts
+
+ # Pick storages before writing the file (always ask unless only one)
+ # Create a minimal temp file to write into
+ : >"$canonical"
+
+ # Base content (no var_ctid / var_hostname here)
+ cat >"$canonical" <<'EOF'
+# Community-Scripts defaults (var_* only). Lines starting with # are comments.
+# Precedence: ENV var_* > default.vars > built-ins.
+# Keep keys alphabetically sorted.
+
+# Container type
+var_unprivileged=1
+
+# Resources
+var_cpu=1
+var_disk=4
+var_ram=1024
+
+# Network
+var_brg=vmbr0
+var_net=dhcp
+var_ipv6_method=none
+# var_gateway=
+# var_ipv6_static=
+# var_vlan=
+# var_mtu=
+# var_mac=
+# var_ns=
+
+# SSH
+var_ssh=no
+# var_ssh_authorized_key=
+
+# APT cacher (optional)
+# var_apt_cacher=yes
+# var_apt_cacher_ip=192.168.1.10
+
+# Features/Tags/verbosity
+var_fuse=no
+var_tun=no
+var_tags=community-script
+var_verbose=no
+
+# Security (root PW) – empty => autologin
+# var_pw=
+EOF
+
+ # Now choose storages (always prompt unless just one exists)
+ choose_and_set_storage_for_file "$canonical" template
+ choose_and_set_storage_for_file "$canonical" container
+
+ chmod 0644 "$canonical"
+ msg_ok "Created ${canonical}"
+ }
+
+ # Whitelist check
+ local _is_whitelisted_key
+ _is_whitelisted_key() {
+ local k="$1"
+ local w
+ for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
+ return 1
+ }
+
+ # Safe parser for KEY=VALUE lines
+ local _load_vars_file
+ _load_vars_file() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ msg_info "Loading defaults from ${file}"
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [[ -z "$line" || "$line" == \#* ]] && continue
+ if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
+ local var_key="${BASH_REMATCH[1]}"
+ local var_val="${BASH_REMATCH[2]}"
+
+ [[ "$var_key" != var_* ]] && continue
+ _is_whitelisted_key "$var_key" || {
+ msg_debug "Ignore non-whitelisted ${var_key}"
+ continue
+ }
+
+ # Strip quotes
+ if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ fi
+
+ # Unsafe characters
+ case $var_val in
+ \"*\")
+ var_val=${var_val#\"}
+ var_val=${var_val%\"}
+ ;;
+ \'*\')
+ var_val=${var_val#\'}
+ var_val=${var_val%\'}
+ ;;
+ esac # Hard env wins
+ [[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue
+ # Set only if not already exported
+ [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
+ else
+ msg_warn "Malformed line in ${file}: ${line}"
+ fi
+ done <"$file"
+ msg_ok "Loaded ${file}"
+ }
+
+ # 1) Ensure file exists
+ _ensure_default_vars
+
+ # 2) Load file
+ local dv
+ dv="$(_find_default_vars)" || {
+ msg_error "default.vars not found after ensure step"
+ return 1
+ }
+ _load_vars_file "$dv"
+
+ # 3) Map var_verbose → VERBOSE
+ if [[ -n "${var_verbose:-}" ]]; then
+ case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
+ else
+ VERBOSE="no"
+ fi
+
+ # 4) Apply base settings and show summary
+ METHOD="mydefaults-global"
+ base_settings "$VERBOSE"
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
+ echo_default
+}
+
+# ------------------------------------------------------------------------------
+# get_app_defaults_path()
+#
+# - Returns full path for app-specific defaults file
+# - Example: /usr/local/community-scripts/defaults/.vars
+# ------------------------------------------------------------------------------
+
+get_app_defaults_path() {
+ local n="${NSAPP:-${APP,,}}"
+ echo "/usr/local/community-scripts/defaults/${n}.vars"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults
+#
+# - Called after advanced_settings returned with fully chosen values.
+# - If no .vars exists, offers to persist current advanced settings
+# into /usr/local/community-scripts/defaults/.vars
+# - Only writes whitelisted var_* keys.
+# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
+# ------------------------------------------------------------------------------
+if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
+ declare -ag VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+fi
+
+# Note: _is_whitelisted_key() is defined above in default_var_settings section
+
+_sanitize_value() {
+ # Disallow Command-Substitution / Shell-Meta
+ case "$1" in
+ *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
+ echo ""
+ return 0
+ ;;
+ esac
+ echo "$1"
+}
+
+# Map-Parser: read var_* from file into _VARS_IN associative array
+# Note: Main _load_vars_file() with full validation is defined in default_var_settings section
+# This simplified version is used specifically for diff operations via _VARS_IN array
+declare -A _VARS_IN
+_load_vars_file_to_map() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ _VARS_IN=() # Clear array
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [ -z "$line" ] && continue
+ case "$line" in
+ \#*) continue ;;
+ esac
+ key=$(printf "%s" "$line" | cut -d= -f1)
+ val=$(printf "%s" "$line" | cut -d= -f2-)
+ case "$key" in
+ var_*)
+ if _is_whitelisted_key "$key"; then
+ _VARS_IN["$key"]="$val"
+ fi
+ ;;
+ esac
+ done <"$file"
+}
+
+# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
+_build_vars_diff() {
+ local oldf="$1" newf="$2"
+ local k
+ local -A OLD=() NEW=()
+ _load_vars_file_to_map "$oldf"
+ for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
+ _load_vars_file_to_map "$newf"
+ for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
+
+ local out
+ out+="# Diff for ${APP} (${NSAPP})\n"
+ out+="# Old: ${oldf}\n# New: ${newf}\n\n"
+
+ local found_change=0
+
+ # Changed & Removed
+ for k in "${!OLD[@]}"; do
+ if [[ -v NEW["$k"] ]]; then
+ if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
+ out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ else
+ out+="- ${k}\n - old: ${OLD[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ # Added
+ for k in "${!NEW[@]}"; do
+ if [[ ! -v OLD["$k"] ]]; then
+ out+="+ ${k}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ if [[ $found_change -eq 0 ]]; then
+ out+="(No differences)\n"
+ fi
+
+ printf "%b" "$out"
+}
+
+# Build a temporary .vars file from current advanced settings
+_build_current_app_vars_tmp() {
+ tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
+
+ # NET/GW
+ _net="${NET:-}"
+ _gate=""
+ case "${GATE:-}" in
+ ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
+ esac
+
+ # IPv6
+ _ipv6_method="${IPV6_METHOD:-auto}"
+ _ipv6_static=""
+ _ipv6_gateway=""
+ if [ "$_ipv6_method" = "static" ]; then
+ _ipv6_static="${IPV6_ADDR:-}"
+ _ipv6_gateway="${IPV6_GATE:-}"
+ fi
+
+ # MTU/VLAN/MAC
+ _mtu=""
+ _vlan=""
+ _mac=""
+ case "${MTU:-}" in
+ ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
+ esac
+ case "${VLAN:-}" in
+ ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
+ esac
+ case "${MAC:-}" in
+ ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
+ esac
+
+ # DNS / Searchdomain
+ _ns=""
+ _searchdomain=""
+ case "${NS:-}" in
+ -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
+ esac
+ case "${SD:-}" in
+ -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
+ esac
+
+ # SSH / APT / Features
+ _ssh="${SSH:-no}"
+ _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
+ _apt_cacher="${APT_CACHER:-}"
+ _apt_cacher_ip="${APT_CACHER_IP:-}"
+ _fuse="${ENABLE_FUSE:-no}"
+ _tun="${ENABLE_TUN:-no}"
+ _tags="${TAGS:-}"
+ _verbose="${VERBOSE:-no}"
+
+ # Type / Resources / Identity
+ _unpriv="${CT_TYPE:-1}"
+ _cpu="${CORE_COUNT:-1}"
+ _ram="${RAM_SIZE:-1024}"
+ _disk="${DISK_SIZE:-4}"
+ _hostname="${HN:-$NSAPP}"
+
+ # Storage
+ _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
+ _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
+
+ {
+ echo "# App-specific defaults for ${APP} (${NSAPP})"
+ echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo
+
+ echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
+ echo "var_cpu=$(_sanitize_value "$_cpu")"
+ echo "var_ram=$(_sanitize_value "$_ram")"
+ echo "var_disk=$(_sanitize_value "$_disk")"
+
+ [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
+ [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
+ [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
+ [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
+ [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
+ [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
+ [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
+
+ [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
+ [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")"
+
+ [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
+ [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
+
+ [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
+ [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
+
+ [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
+ [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
+ [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
+ [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
+
+ [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
+ [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
+
+ [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
+ [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
+ } >"$tmpf"
+
+ echo "$tmpf"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults()
+#
+# - Called after advanced_settings()
+# - Offers to save current values as app defaults if not existing
+# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
+# ------------------------------------------------------------------------------
+maybe_offer_save_app_defaults() {
+ local app_vars_path
+ app_vars_path="$(get_app_defaults_path)"
+
+ # always build from current settings
+ local new_tmp diff_tmp
+ new_tmp="$(_build_current_app_vars_tmp)"
+ diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
+
+ # 1) if no file → offer to create
+ if [[ ! -f "$app_vars_path" ]]; then
+ if whiptail --backtitle "[dev] 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"
+ msg_ok "Saved app defaults: ${app_vars_path}"
+ fi
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 2) if file exists → build diff
+ _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
+
+ # if no differences → do nothing
+ if grep -q "^(No differences)$" "$diff_tmp"; then
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 3) if file exists → show menu with default selection "Update Defaults"
+ local app_vars_file
+ app_vars_file="$(basename "$app_vars_path")"
+
+ while true; do
+ local sel
+ sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "APP DEFAULTS – ${APP}" \
+ --menu "Differences detected. What do you want to do?" 20 78 10 \
+ "Update Defaults" "Write new values to ${app_vars_file}" \
+ "Keep Current" "Keep existing defaults (no changes)" \
+ "View Diff" "Show a detailed diff" \
+ "Cancel" "Abort without changes" \
+ --default-item "Update Defaults" \
+ 3>&1 1>&2 2>&3)" || { sel="Cancel"; }
+
+ case "$sel" in
+ "Update Defaults")
+ install -m 0644 "$new_tmp" "$app_vars_path"
+ msg_ok "Updated app defaults: ${app_vars_path}"
+ break
+ ;;
+ "Keep Current")
+ msg_info "Keeping current app defaults: ${app_vars_path}"
+ break
+ ;;
+ "View Diff")
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Diff – ${APP}" \
+ --scrolltext --textbox "$diff_tmp" 25 100
+ ;;
+ "Cancel" | *)
+ msg_info "Canceled. No changes to app defaults."
+ break
+ ;;
+ esac
+ done
+
+ rm -f "$new_tmp" "$diff_tmp"
+}
+
+ensure_storage_selection_for_vars_file() {
+ local vf="$1"
+
+ # 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-)
+
+ if [[ -n "$tpl" && -n "$ct" ]]; then
+ TEMPLATE_STORAGE="$tpl"
+ CONTAINER_STORAGE="$ct"
+ return 0
+ fi
+
+ choose_and_set_storage_for_file "$vf" template
+ choose_and_set_storage_for_file "$vf" container
+
+ msg_ok "Storage configuration saved to $(basename "$vf")"
+}
+
+diagnostics_menu() {
+ if [ "${DIAGNOSTICS:-no}" = "yes" ]; then
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "No" --no-button "Back"; then
+ DIAGNOSTICS="no"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ else
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "Yes" --no-button "Back"; then
+ DIAGNOSTICS="yes"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ fi
+}
+
+ensure_global_default_vars_file() {
+ local vars_path="/usr/local/community-scripts/default.vars"
+ if [[ ! -f "$vars_path" ]]; then
+ mkdir -p "$(dirname "$vars_path")"
+ touch "$vars_path"
+ fi
+ echo "$vars_path"
+}
+
+# ------------------------------------------------------------------------------
+# install_script()
+#
+# - Main entrypoint for installation mode
+# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
+# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
+# - Applies chosen settings and triggers container build
+# ------------------------------------------------------------------------------
+install_script() {
+ pve_check
+ shell_check
+ root_check
+ arch_check
+ ssh_check
+ maxkeys_check
+ diagnostics_check
+
+ if systemctl is-active -q ping-instances.service; then
+ systemctl -q stop ping-instances.service
+ fi
+
+ NEXTID=$(pvesh get /cluster/nextid)
+ timezone=$(cat /etc/timezone)
+
+ # Show APP Header
+ header_info
+
+ # --- Support CLI argument as direct preset (default, advanced, …) ---
+ CHOICE="${mode:-${1:-}}"
+
+ # If no CLI argument → show whiptail menu
+ # Build menu dynamically based on available options
+ local appdefaults_option=""
+ local settings_option=""
+ local menu_items=(
+ "1" "Default Install"
+ "2" "Advanced Install"
+ "3" "My Defaults"
+ )
+
+ if [ -f "$(get_app_defaults_path)" ]; then
+ appdefaults_option="4"
+ menu_items+=("4" "App Defaults for ${APP}")
+ settings_option="5"
+ menu_items+=("5" "Settings")
+ else
+ settings_option="4"
+ menu_items+=("4" "Settings")
+ fi
+
+ if [ -z "$CHOICE" ]; then
+
+ TMP_CHOICE=$(whiptail \
+ --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts Options" \
+ --ok-button "Select" --cancel-button "Exit Script" \
+ --notags \
+ --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
+ 20 60 9 \
+ "${menu_items[@]}" \
+ --default-item "1" \
+ 3>&1 1>&2 2>&3) || exit_script
+ CHOICE="$TMP_CHOICE"
+ fi
+
+ APPDEFAULTS_OPTION="$appdefaults_option"
+ SETTINGS_OPTION="$settings_option"
+
+ # --- Main case ---
+ local defaults_target=""
+ local run_maybe_offer="no"
+ case "$CHOICE" in
+ 1 | default | DEFAULT)
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
+ VERBOSE="no"
+ METHOD="default"
+ base_settings "$VERBOSE"
+ echo_default
+ defaults_target="$(ensure_global_default_vars_file)"
+ ;;
+ 2 | advanced | ADVANCED)
+ header_info
+ echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}"
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
+ METHOD="advanced"
+ base_settings
+ advanced_settings
+ defaults_target="$(ensure_global_default_vars_file)"
+ run_maybe_offer="yes"
+ ;;
+ 3 | mydefaults | MYDEFAULTS)
+ default_var_settings || {
+ msg_error "Failed to apply default.vars"
+ exit 1
+ }
+ defaults_target="/usr/local/community-scripts/default.vars"
+ ;;
+ "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
+ METHOD="appdefaults"
+ base_settings
+ _load_vars_file "$(get_app_defaults_path)"
+ echo_default
+ defaults_target="$(get_app_defaults_path)"
+ else
+ msg_error "No App Defaults available for ${APP}"
+ exit 1
+ fi
+ ;;
+ "$SETTINGS_OPTION" | settings | SETTINGS)
+ settings_menu
+ defaults_target=""
+ ;;
+ *)
+ echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}"
+ exit 1
+ ;;
+ esac
+
+ if [[ -n "$defaults_target" ]]; then
+ ensure_storage_selection_for_vars_file "$defaults_target"
+ fi
+
+ if [[ "$run_maybe_offer" == "yes" ]]; then
+ maybe_offer_save_app_defaults
+ fi
+}
+
+edit_default_storage() {
+ local vf="/usr/local/community-scripts/default.vars"
+
+ # Ensure file exists
+ if [[ ! -f "$vf" ]]; then
+ mkdir -p "$(dirname "$vf")"
+ touch "$vf"
+ fi
+
+ # Let ensure_storage_selection_for_vars_file handle everything
+ ensure_storage_selection_for_vars_file "$vf"
+}
+
+settings_menu() {
+ while true; do
+ local settings_items=(
+ "1" "Manage API-Diagnostic Setting"
+ "2" "Edit Default.vars"
+ "3" "Edit Default Storage"
+ )
+ if [ -f "$(get_app_defaults_path)" ]; then
+ settings_items+=("4" "Edit App.vars for ${APP}")
+ settings_items+=("5" "Exit")
+ else
+ settings_items+=("4" "Exit")
+ fi
+
+ local choice
+ choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts SETTINGS Menu" \
+ --ok-button "OK" --cancel-button "Back" \
+ --menu "\n\nChoose a settings option:\n\nUse TAB or Arrow keys to navigate, ENTER to select." 20 60 9 \
+ "${settings_items[@]}" \
+ 3>&1 1>&2 2>&3) || break
+
+ case "$choice" in
+ 1) diagnostics_menu ;;
+ 2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
+ 3) edit_default_storage ;;
+ 4)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ ${EDITOR:-nano} "$(get_app_defaults_path)"
+ else
+ exit_script
+ fi
+ ;;
+ 5) exit_script ;;
+ esac
+ done
+}
+
+# ===== Unified storage selection & writing to vars files =====
+_write_storage_to_vars() {
+ # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
+ local vf="$1" key="$2" val="$3"
+ # remove uncommented and commented versions to avoid duplicates
+ sed -i "/^[#[:space:]]*${key}=/d" "$vf"
+ echo "${key}=${val}" >>"$vf"
+}
+
+choose_and_set_storage_for_file() {
+ # $1 = vars_file, $2 = class ('container'|'template')
+ local vf="$1" class="$2" key="" current=""
+ case "$class" in
+ container) key="var_container_storage" ;;
+ template) key="var_template_storage" ;;
+ *)
+ msg_error "Unknown storage class: $class"
+ return 1
+ ;;
+ esac
+
+ current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
+
+ # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
+ local content="rootdir"
+ [[ "$class" == "template" ]] && content="vztmpl"
+ local count
+ count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
+
+ if [[ "$count" -eq 1 ]]; then
+ STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
+ STORAGE_INFO=""
+ else
+ # If the current value is preselectable, we could show it, but per your requirement we always offer selection
+ select_storage "$class" || return 1
+ fi
+
+ _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
+
+ # Keep environment in sync for later steps (e.g. app-default save)
+ if [[ "$class" == "container" ]]; then
+ export var_container_storage="$STORAGE_RESULT"
+ export CONTAINER_STORAGE="$STORAGE_RESULT"
+ else
+ export var_template_storage="$STORAGE_RESULT"
+ export TEMPLATE_STORAGE="$STORAGE_RESULT"
+ fi
+
+ msg_ok "Updated ${key} → ${STORAGE_RESULT}"
+}
+
+# ------------------------------------------------------------------------------
+# check_container_resources()
+#
+# - Compares host RAM/CPU with required values
+# - Warns if under-provisioned and asks user to continue or abort
+# ------------------------------------------------------------------------------
+check_container_resources() {
+ current_ram=$(free -m | awk 'NR==2{print $2}')
+ current_cpu=$(nproc)
+
+ if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
+ echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
+ echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
+ echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
+ echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ else
+ echo -e ""
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# check_container_storage()
+#
+# - Checks /boot partition usage
+# - Warns if usage >80% and asks user confirmation before proceeding
+# ------------------------------------------------------------------------------
+check_container_storage() {
+ total_size=$(df /boot --output=size | tail -n 1)
+ local used_size=$(df /boot --output=used | tail -n 1)
+ usage=$((100 * used_size / total_size))
+ if ((usage > 80)); then
+ echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
+ echo -ne "Continue anyway? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
+ echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# ssh_extract_keys_from_file()
+#
+# - Extracts valid SSH public keys from given file
+# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
+# ------------------------------------------------------------------------------
+ssh_extract_keys_from_file() {
+ local f="$1"
+ [[ -r "$f" ]] || return 0
+ tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ # nackt: typ base64 [comment]
+ /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
+ # mit Optionen: finde ab erstem Key-Typ
+ {
+ match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
+ if (RSTART>0) { print substr($0, RSTART) }
+ }
+ '
+}
+
+# ------------------------------------------------------------------------------
+# ssh_build_choices_from_files()
+#
+# - Builds interactive whiptail checklist of available SSH keys
+# - Generates fingerprint, type and comment for each key
+# ------------------------------------------------------------------------------
+ssh_build_choices_from_files() {
+ local -a files=("$@")
+ CHOICES=()
+ COUNT=0
+ MAPFILE="$(mktemp)"
+ local id key typ fp cmt base ln=0
+
+ for f in "${files[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # map every key in file
+ while IFS= read -r key; do
+ [[ -n "$key" ]] || continue
+
+ typ=""
+ fp=""
+ cmt=""
+ # Only the pure key part (without options) is already included in ‘key’.
+ read -r _typ _b64 _cmt <<<"$key"
+ typ="${_typ:-key}"
+ cmt="${_cmt:-}"
+ # Fingerprint via ssh-keygen (if available)
+ if command -v ssh-keygen >/dev/null 2>&1; then
+ fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
+ fi
+ # Label shorten
+ [[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
+
+ ln=$((ln + 1))
+ COUNT=$((COUNT + 1))
+ id="K${COUNT}"
+ echo "${id}|${key}" >>"$MAPFILE"
+ CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
+ done < <(ssh_extract_keys_from_file "$f")
+ done
+}
+
+# ------------------------------------------------------------------------------
+# ssh_discover_default_files()
+#
+# - Scans standard paths for SSH keys
+# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc.
+# ------------------------------------------------------------------------------
+ssh_discover_default_files() {
+ local -a cand=()
+ shopt -s nullglob
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ shopt -u nullglob
+ printf '%s\0' "${cand[@]}"
+}
+
+configure_ssh_settings() {
+ SSH_KEYS_FILE="$(mktemp)"
+ : >"$SSH_KEYS_FILE"
+
+ IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
+ ssh_build_choices_from_files "${_def_files[@]}"
+ local default_key_count="$COUNT"
+
+ local ssh_key_mode
+ if [[ "$default_key_count" -gt 0 ]]; then
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "Provision SSH keys for root:" 14 72 4 \
+ "found" "Select from detected keys (${default_key_count})" \
+ "manual" "Paste a single public key" \
+ "folder" "Scan another folder (path or glob)" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ else
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "No host keys detected; choose manual/none:" 12 72 2 \
+ "manual" "Paste a single public key" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ fi
+
+ case "$ssh_key_mode" in
+ found)
+ local selection
+ selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \
+ --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ ;;
+ manual)
+ SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
+ [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
+ ;;
+ folder)
+ local glob_path
+ glob_path=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
+ if [[ -n "$glob_path" ]]; then
+ shopt -s nullglob
+ read -r -a _scan_files <<<"$glob_path"
+ shopt -u nullglob
+ if [[ "${#_scan_files[@]}" -gt 0 ]]; then
+ ssh_build_choices_from_files "${_scan_files[@]}"
+ if [[ "$COUNT" -gt 0 ]]; then
+ local folder_selection
+ folder_selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \
+ --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $folder_selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60
+ fi
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60
+ fi
+ fi
+ ;;
+ none)
+ :
+ ;;
+ esac
+
+ if [[ -s "$SSH_KEYS_FILE" ]]; then
+ sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
+ printf '\n' >>"$SSH_KEYS_FILE"
+ fi
+
+ if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
+ SSH="yes"
+ else
+ SSH="no"
+ fi
+ else
+ SSH="no"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# start()
+#
+# - Entry point of script
+# - On Proxmox host: calls install_script
+# - In silent mode: runs update_script
+# - Otherwise: shows update/setting menu
+# ------------------------------------------------------------------------------
+start() {
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func)
+ if command -v pveversion >/dev/null 2>&1; then
+ install_script || return 0
+ return 0
+ elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
+ VERBOSE="no"
+ set_std_mode
+ update_script
+ else
+ CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
+ "Support/Update functions for ${APP} LXC. Choose an option:" \
+ 12 60 3 \
+ "1" "YES (Silent Mode)" \
+ "2" "YES (Verbose Mode)" \
+ "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
+
+ case "$CHOICE" in
+ 1)
+ VERBOSE="no"
+ set_std_mode
+ ;;
+ 2)
+ VERBOSE="yes"
+ set_std_mode
+ ;;
+ 3)
+ clear
+ exit_script
+ exit
+ ;;
+ esac
+ update_script
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# build_container()
+#
+# - Creates and configures the LXC container
+# - Builds network string and applies features (FUSE, TUN, VAAPI passthrough)
+# - Starts container and waits for network connectivity
+# - Installs base packages, SSH keys, and runs -install.sh
+# ------------------------------------------------------------------------------
+build_container() {
+ # if [ "$VERBOSE" == "yes" ]; then set -x; fi
+
+ NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
+
+ # MAC
+ if [[ -n "$MAC" ]]; then
+ case "$MAC" in
+ ,hwaddr=*) NET_STRING+="$MAC" ;;
+ *) NET_STRING+=",hwaddr=$MAC" ;;
+ esac
+ fi
+
+ # IP (immer zwingend, Standard dhcp)
+ NET_STRING+=",ip=${NET:-dhcp}"
+
+ # Gateway
+ if [[ -n "$GATE" ]]; then
+ case "$GATE" in
+ ,gw=*) NET_STRING+="$GATE" ;;
+ *) NET_STRING+=",gw=$GATE" ;;
+ esac
+ fi
+
+ # VLAN
+ if [[ -n "$VLAN" ]]; then
+ case "$VLAN" in
+ ,tag=*) NET_STRING+="$VLAN" ;;
+ *) NET_STRING+=",tag=$VLAN" ;;
+ esac
+ fi
+
+ # MTU
+ if [[ -n "$MTU" ]]; then
+ case "$MTU" in
+ ,mtu=*) NET_STRING+="$MTU" ;;
+ *) NET_STRING+=",mtu=$MTU" ;;
+ esac
+ fi
+
+ # IPv6 Handling
+ case "$IPV6_METHOD" in
+ auto) NET_STRING="$NET_STRING,ip6=auto" ;;
+ dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
+ static)
+ NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
+ [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
+ ;;
+ none) ;;
+ esac
+
+ if [ "$CT_TYPE" == "1" ]; then
+ FEATURES="keyctl=1,nesting=1"
+ else
+ FEATURES="nesting=1"
+ fi
+
+ if [ "$ENABLE_FUSE" == "yes" ]; then
+ FEATURES="$FEATURES,fuse=1"
+ fi
+
+ TEMP_DIR=$(mktemp -d)
+ pushd "$TEMP_DIR" >/dev/null
+ if [ "$var_os" == "alpine" ]; then
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)"
+ else
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)"
+ fi
+ export DIAGNOSTICS="$DIAGNOSTICS"
+ export RANDOM_UUID="$RANDOM_UUID"
+ export CACHER="$APT_CACHER"
+ export CACHER_IP="$APT_CACHER_IP"
+ export tz="$timezone"
+ export APPLICATION="$APP"
+ export app="$NSAPP"
+ export PASSWORD="$PW"
+ export VERBOSE="$VERBOSE"
+ export SSH_ROOT="${SSH}"
+ export SSH_AUTHORIZED_KEY
+ export CTID="$CT_ID"
+ export CTTYPE="$CT_TYPE"
+ export ENABLE_FUSE="$ENABLE_FUSE"
+ export ENABLE_TUN="$ENABLE_TUN"
+ export PCT_OSTYPE="$var_os"
+ export PCT_OSVERSION="$var_version"
+ export PCT_DISK_SIZE="$DISK_SIZE"
+ export PCT_OPTIONS="
+ -features $FEATURES
+ -hostname $HN
+ -tags $TAGS
+ $SD
+ $NS
+ $NET_STRING
+ -onboot 1
+ -cores $CORE_COUNT
+ -memory $RAM_SIZE
+ -unprivileged $CT_TYPE
+ $PW
+"
+ export TEMPLATE_STORAGE="${var_template_storage:-}"
+ export CONTAINER_STORAGE="${var_container_storage:-}"
+ create_lxc_container || exit $?
+
+ LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
+
+ # ============================================================================
+ # GPU/USB PASSTHROUGH CONFIGURATION
+ # ============================================================================
+
+ # List of applications that benefit from GPU acceleration
+ GPU_APPS=(
+ "immich" "channels" "emby" "ersatztv" "frigate"
+ "jellyfin" "plex" "scrypted" "tdarr" "unmanic"
+ "ollama" "fileflows" "open-webui" "tunarr" "debian"
+ "handbrake" "sunshine" "moonlight" "kodi" "stremio"
+ "viseron"
+ )
+
+ # Check if app needs GPU
+ is_gpu_app() {
+ local app="${1,,}"
+ for gpu_app in "${GPU_APPS[@]}"; do
+ [[ "$app" == "${gpu_app,,}" ]] && return 0
+ done
+ return 1
+ }
+
+ # Detect all available GPU devices
+ detect_gpu_devices() {
+ INTEL_DEVICES=()
+ AMD_DEVICES=()
+ NVIDIA_DEVICES=()
+
+ # Store PCI info to avoid multiple calls
+ local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D")
+
+ # Check for Intel GPU - look for Intel vendor ID [8086]
+ if echo "$pci_vga_info" | grep -q "\[8086:"; then
+ msg_info "Detected Intel GPU"
+ if [[ -d /dev/dri ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && INTEL_DEVICES+=("$d")
+ done
+ fi
+ fi
+
+ # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
+ if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then
+ msg_info "Detected AMD GPU"
+ if [[ -d /dev/dri ]]; then
+ # Only add if not already claimed by Intel
+ if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && AMD_DEVICES+=("$d")
+ done
+ fi
+ fi
+ fi
+
+ # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
+ if echo "$pci_vga_info" | grep -q "\[10de:"; then
+ msg_info "Detected NVIDIA GPU"
+ if ! check_nvidia_host_setup; then
+ msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough."
+ msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually."
+ return 0
+ fi
+
+ for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do
+ [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
+ done
+
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found"
+ msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver"
+ else
+ if [[ "$CT_TYPE" == "0" ]]; then
+ cat <>"$LXC_CONFIG"
+ # NVIDIA GPU Passthrough (privileged)
+ lxc.cgroup2.devices.allow: c 195:* rwm
+ lxc.cgroup2.devices.allow: c 243:* rwm
+ lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file
+EOF
+
+ if [[ -e /dev/dri/renderD128 ]]; then
+ echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+
+ export GPU_TYPE="NVIDIA"
+ export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
+ msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})"
+ else
+ msg_warn "NVIDIA passthrough only supported for privileged containers"
+ return 0
+ fi
+ fi
+ fi
+
+ # Debug output
+ msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
+ msg_debug "AMD devices: ${AMD_DEVICES[*]}"
+ msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
+ }
+
+ # Configure USB passthrough for privileged containers
+ configure_usb_passthrough() {
+ if [[ "$CT_TYPE" != "0" ]]; then
+ return 0
+ fi
+
+ msg_info "Configuring automatic USB passthrough (privileged container)"
+ cat <>"$LXC_CONFIG"
+# Automatic USB passthrough (privileged container)
+lxc.cgroup2.devices.allow: a
+lxc.cap.drop:
+lxc.cgroup2.devices.allow: c 188:* rwm
+lxc.cgroup2.devices.allow: c 189:* rwm
+lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
+lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
+EOF
+ msg_ok "USB passthrough configured"
+ }
+
+ # Configure GPU passthrough
+ configure_gpu_passthrough() {
+ # Skip if not a GPU app and not privileged
+ if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then
+ return 0
+ fi
+
+ detect_gpu_devices
+
+ # Count available GPU types
+ local gpu_count=0
+ local available_gpus=()
+
+ if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("INTEL")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("AMD")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("NVIDIA")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ $gpu_count -eq 0 ]]; then
+ msg_info "No GPU devices found for passthrough"
+ return 0
+ fi
+
+ local selected_gpu=""
+
+ if [[ $gpu_count -eq 1 ]]; then
+ # Automatic selection for single GPU
+ selected_gpu="${available_gpus[0]}"
+ msg_info "Automatically configuring ${selected_gpu} GPU passthrough"
+ else
+ # Multiple GPUs - ask user
+ echo -e "\n${INFO} Multiple GPU types detected:"
+ for gpu in "${available_gpus[@]}"; do
+ echo " - $gpu"
+ done
+ read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
+ selected_gpu="${selected_gpu^^}"
+
+ # Validate selection
+ local valid=0
+ for gpu in "${available_gpus[@]}"; do
+ [[ "$selected_gpu" == "$gpu" ]] && valid=1
+ done
+
+ if [[ $valid -eq 0 ]]; then
+ msg_warn "Invalid selection. Skipping GPU passthrough."
+ return 0
+ fi
+ fi
+
+ # Apply passthrough configuration based on selection
+ local dev_idx=0
+
+ case "$selected_gpu" in
+ INTEL | AMD)
+ local devices=()
+ [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
+ [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
+
+ # For Proxmox WebUI visibility, add as dev0, dev1 etc.
+ for dev in "${devices[@]}"; do
+ if [[ "$CT_TYPE" == "0" ]]; then
+ # Privileged container - use dev entries for WebUI visibility
+ # Use initial GID 104 (render) for renderD*, 44 (video) for card*
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+
+ # Also add cgroup allows for privileged containers
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ else
+ # Unprivileged container
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+ fi
+ done
+
+ export GPU_TYPE="$selected_gpu"
+ msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)"
+ ;;
+
+ NVIDIA)
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver"
+ return 1
+ fi
+
+ for dev in "${NVIDIA_DEVICES[@]}"; do
+ # NVIDIA devices typically need different handling
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ dev_idx=$((dev_idx + 1))
+
+ if [[ "$CT_TYPE" == "0" ]]; then
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ fi
+ done
+
+ export GPU_TYPE="NVIDIA"
+ msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)"
+ ;;
+ esac
+ }
+
+ # Additional device passthrough
+ configure_additional_devices() {
+ # TUN device passthrough
+ if [ "$ENABLE_TUN" == "yes" ]; then
+ cat <>"$LXC_CONFIG"
+lxc.cgroup2.devices.allow: c 10:200 rwm
+lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
+EOF
+ fi
+
+ # Coral TPU passthrough
+ if [[ -e /dev/apex_0 ]]; then
+ msg_info "Detected Coral TPU - configuring passthrough"
+ echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+ }
+
+ # Execute pre-start configurations
+ configure_usb_passthrough
+ configure_gpu_passthrough
+ configure_additional_devices
+
+ # ============================================================================
+ # START CONTAINER AND INSTALL USERLAND
+ # ============================================================================
+
+ msg_info "Starting LXC Container"
+ pct start "$CTID"
+
+ # Wait for container to be running
+ for i in {1..10}; do
+ if pct status "$CTID" | grep -q "status: running"; then
+ msg_ok "Started LXC Container"
+ break
+ fi
+ sleep 1
+ if [ "$i" -eq 10 ]; then
+ msg_error "LXC Container did not reach running state"
+ exit 1
+ fi
+ done
+
+ # Wait for network (skip for Alpine initially)
+ if [ "$var_os" != "alpine" ]; then
+ msg_info "Waiting for network in LXC container"
+
+ # Wait for IP
+ for i in {1..20}; do
+ ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+ [ -n "$ip_in_lxc" ] && break
+ sleep 1
+ done
+
+ if [ -z "$ip_in_lxc" ]; then
+ msg_error "No IP assigned to CT $CTID after 20s"
+ exit 1
+ fi
+
+ # Try to reach gateway
+ gw_ok=0
+ for i in {1..10}; do
+ if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then
+ gw_ok=1
+ break
+ fi
+ sleep 1
+ done
+
+ if [ "$gw_ok" -eq 1 ]; then
+ msg_ok "Network in LXC is reachable (IP $ip_in_lxc)"
+ else
+ msg_warn "Network reachable but gateway check failed"
+ fi
+ fi
+ # Function to get correct GID inside container
+ get_container_gid() {
+ local group="$1"
+ local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
+ echo "${gid:-44}" # Default to 44 if not found
+ }
+
+ fix_gpu_gids
+
+ # Continue with standard container setup
+ msg_info "Customizing LXC Container"
+
+ # # Install GPU userland if configured
+ # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
+ # install_gpu_userland "VAAPI"
+ # fi
+
+ # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
+ # install_gpu_userland "NVIDIA"
+ # fi
+
+ # Continue with standard container setup
+ if [ "$var_os" == "alpine" ]; then
+ sleep 3
+ pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
+EOF'
+ pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
+ else
+ sleep 3
+ pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
+ pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
+ echo LANG=\$locale_line >/etc/default/locale && \
+ locale-gen >/dev/null && \
+ export LANG=\$locale_line"
+
+ if [[ -z "${tz:-}" ]]; then
+ tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
+ fi
+
+ if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
+ pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
+ else
+ msg_warn "Skipping timezone setup – zone '$tz' not found in container"
+ fi
+
+ pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || {
+ msg_error "apt-get base packages installation failed"
+ exit 1
+ }
+ fi
+
+ msg_ok "Customized LXC Container"
+
+ # Verify GPU access if enabled
+ if [[ "${ENABLE_VAAPI:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "vainfo >/dev/null 2>&1" &&
+ msg_ok "VAAPI verified working" ||
+ msg_warn "VAAPI verification failed - may need additional configuration"
+ fi
+
+ if [[ "${ENABLE_NVIDIA:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "nvidia-smi >/dev/null 2>&1" &&
+ msg_ok "NVIDIA verified working" ||
+ msg_warn "NVIDIA verification failed - may need additional configuration"
+ fi
+
+ # Install SSH keys
+ install_ssh_keys_into_ct
+
+ # Run application installer
+ if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then
+ exit $?
+ fi
+}
+
+destroy_lxc() {
+ if [[ -z "$CT_ID" ]]; then
+ msg_error "No CT_ID found. Nothing to remove."
+ return 1
+ fi
+
+ # Abbruch bei Ctrl-C / Ctrl-D / ESC
+ trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
+
+ local prompt
+ if ! read -rp "Remove this Container? " prompt; then
+ # read gibt != 0 zurück bei Ctrl-D/ESC
+ msg_error "Aborted input (Ctrl-D/ESC)"
+ return 130
+ fi
+
+ case "${prompt,,}" in
+ y | yes)
+ if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
+ msg_ok "Removed Container $CT_ID"
+ else
+ msg_error "Failed to remove Container $CT_ID"
+ return 1
+ fi
+ ;;
+ "" | n | no)
+ msg_info "Container was not removed."
+ ;;
+ *)
+ msg_warn "Invalid response. Container was not removed."
+ ;;
+ esac
+}
+
+# ------------------------------------------------------------------------------
+# Storage discovery / selection helpers
+# ------------------------------------------------------------------------------
+# ===== Storage discovery / selection helpers (ported from create_lxc.sh) =====
+resolve_storage_preselect() {
+ local class="$1" preselect="$2" required_content=""
+ case "$class" in
+ template) required_content="vztmpl" ;;
+ container) required_content="rootdir" ;;
+ *) return 1 ;;
+ 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
+ fi
+
+ local line total used free
+ line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
+ if [[ -z "$line" ]]; then
+ STORAGE_INFO="n/a"
+ else
+ total="$(awk '{print $4}' <<<"$line")"
+ used="$(awk '{print $5}' <<<"$line")"
+ free="$(awk '{print $6}' <<<"$line")"
+ local total_h used_h free_h
+ if command -v numfmt >/dev/null 2>&1; then
+ total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
+ used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
+ free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
+ STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
+ else
+ STORAGE_INFO="Free: ${free} Used: ${used}"
+ fi
+ fi
+ STORAGE_RESULT="$preselect"
+ return 0
+}
+
+fix_gpu_gids() {
+ if [[ -z "${GPU_TYPE:-}" ]]; then
+ return 0
+ fi
+
+ msg_info "Detecting and setting correct GPU group IDs"
+
+ # Ermittle die tatsächlichen GIDs aus dem Container
+ local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+
+ # Fallbacks wenn Gruppen nicht existieren
+ if [[ -z "$video_gid" ]]; then
+ # Versuche die video Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true"
+ video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback
+ fi
+
+ if [[ -z "$render_gid" ]]; then
+ # Versuche die render Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true"
+ render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+ [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback
+ fi
+
+ msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}"
+
+ # Prüfe ob die GIDs von den Defaults abweichen
+ local need_update=0
+ if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then
+ need_update=1
+ fi
+
+ if [[ $need_update -eq 1 ]]; then
+ msg_info "Updating device GIDs in container config"
+
+ # Stoppe Container für Config-Update
+ pct stop "$CTID" >/dev/null 2>&1
+
+ # Update die dev Einträge mit korrekten GIDs
+ # Backup der Config
+ cp "$LXC_CONFIG" "${LXC_CONFIG}.bak"
+
+ # Parse und update jeden dev Eintrag
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^dev[0-9]+: ]]; then
+ # Extract device path
+ local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/')
+ local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/')
+
+ if [[ "$device_path" =~ renderD ]]; then
+ # RenderD device - use render GID
+ echo "${dev_num}: ${device_path},gid=${render_gid}"
+ elif [[ "$device_path" =~ card ]]; then
+ # Card device - use video GID
+ echo "${dev_num}: ${device_path},gid=${video_gid}"
+ else
+ # Keep original line
+ echo "$line"
+ fi
+ else
+ # Keep non-dev lines
+ echo "$line"
+ fi
+ done <"$LXC_CONFIG" >"${LXC_CONFIG}.new"
+
+ mv "${LXC_CONFIG}.new" "$LXC_CONFIG"
+
+ # Starte Container wieder
+ pct start "$CTID" >/dev/null 2>&1
+ sleep 3
+
+ msg_ok "Device GIDs updated successfully"
+ else
+ msg_ok "Device GIDs are already correct"
+ fi
+ if [[ "$CT_TYPE" == "0" ]]; then
+ pct exec "$CTID" -- bash -c "
+ if [ -d /dev/dri ]; then
+ for dev in /dev/dri/*; do
+ if [ -e \"\$dev\" ]; then
+ if [[ \"\$dev\" =~ renderD ]]; then
+ chgrp ${render_gid} \"\$dev\" 2>/dev/null || true
+ else
+ chgrp ${video_gid} \"\$dev\" 2>/dev/null || true
+ fi
+ chmod 660 \"\$dev\" 2>/dev/null || true
+ fi
+ done
+ fi
+ " >/dev/null 2>&1
+ fi
+}
+
+# NVIDIA-spezific check on host
+check_nvidia_host_setup() {
+ if ! command -v nvidia-smi >/dev/null 2>&1; then
+ msg_warn "NVIDIA GPU detected but nvidia-smi not found on host"
+ msg_warn "Please install NVIDIA drivers on host first."
+ #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run"
+ #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms"
+ #echo " 3. Verify: nvidia-smi"
+ return 1
+ fi
+
+ # check if nvidia-smi works
+ if ! nvidia-smi >/dev/null 2>&1; then
+ msg_warn "nvidia-smi installed but not working. Driver issue?"
+ return 1
+ fi
+
+ return 0
+}
+
+check_storage_support() {
+ local CONTENT="$1" VALID=0
+ while IFS= read -r line; do
+ local STORAGE_NAME
+ STORAGE_NAME=$(awk '{print $1}' <<<"$line")
+ [[ -n "$STORAGE_NAME" ]] && VALID=1
+ done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
+ [[ $VALID -eq 1 ]]
+}
+
+select_storage() {
+ local CLASS=$1 CONTENT CONTENT_LABEL
+ case $CLASS in
+ container)
+ CONTENT='rootdir'
+ CONTENT_LABEL='Container'
+ ;;
+ template)
+ CONTENT='vztmpl'
+ CONTENT_LABEL='Container template'
+ ;;
+ iso)
+ CONTENT='iso'
+ CONTENT_LABEL='ISO image'
+ ;;
+ images)
+ CONTENT='images'
+ CONTENT_LABEL='VM Disk image'
+ ;;
+ backup)
+ CONTENT='backup'
+ CONTENT_LABEL='Backup'
+ ;;
+ snippets)
+ CONTENT='snippets'
+ CONTENT_LABEL='Snippets'
+ ;;
+ *)
+ msg_error "Invalid storage class '$CLASS'"
+ return 1
+ ;;
+ esac
+
+ declare -A STORAGE_MAP
+ local -a MENU=()
+ local COL_WIDTH=0
+
+ while read -r TAG TYPE _ TOTAL USED FREE _; do
+ [[ -n "$TAG" && -n "$TYPE" ]] || continue
+ local DISPLAY="${TAG} (${TYPE})"
+ local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
+ local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
+ local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
+ STORAGE_MAP["$DISPLAY"]="$TAG"
+ MENU+=("$DISPLAY" "$INFO" "OFF")
+ ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
+ done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
+
+ if [[ ${#MENU[@]} -eq 0 ]]; then
+ msg_error "No storage found for content type '$CONTENT'."
+ return 2
+ fi
+
+ if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
+ STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
+ STORAGE_INFO="${MENU[1]}"
+ return 0
+ fi
+
+ local WIDTH=$((COL_WIDTH + 42))
+ while true; do
+ local DISPLAY_SELECTED
+ DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Storage Pools" \
+ --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
+ 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }
+
+ DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
+ if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
+ whiptail --msgbox "No valid storage selected. Please try again." 8 58
+ continue
+ fi
+ STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
+ for ((i = 0; i < ${#MENU[@]}; i += 3)); do
+ if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
+ STORAGE_INFO="${MENU[$i + 1]}"
+ break
+ fi
+ done
+ return 0
+ done
+}
+
+create_lxc_container() {
+ # ------------------------------------------------------------------------------
+ # Optional verbose mode (debug tracing)
+ # ------------------------------------------------------------------------------
+ if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
+
+ # ------------------------------------------------------------------------------
+ # Helpers (dynamic versioning / template parsing)
+ # ------------------------------------------------------------------------------
+ pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
+ pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
+
+ ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
+ ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
+ ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
+
+ # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
+ parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
+
+ # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
+ # Returns:
+ # 0 = no upgrade needed
+ # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
+ # 2 = user declined
+ # 3 = upgrade attempted but failed OR retry failed
+ offer_lxc_stack_upgrade_and_maybe_retry() {
+ local do_retry="${1:-no}" # yes|no
+ local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
+
+ _pvec_i="$(pkg_ver pve-container)"
+ _lxcp_i="$(pkg_ver lxc-pve)"
+ _pvec_c="$(pkg_cand pve-container)"
+ _lxcp_c="$(pkg_cand lxc-pve)"
+
+ if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
+ ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
+ fi
+ if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
+ ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
+ fi
+ if [[ $need -eq 0 ]]; then
+ msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
+ return 0
+ fi
+
+ echo
+ echo "An update for the Proxmox LXC stack is available:"
+ echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
+ echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
+ echo
+ read -rp "Do you want to upgrade now? [y/N] " _ans
+ case "${_ans,,}" in
+ y | yes)
+ msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
+ if apt-get update -qq >/dev/null && apt-get install -y --only-upgrade pve-container lxc-pve >/dev/null; then
+ msg_ok "LXC stack upgraded."
+ if [[ "$do_retry" == "yes" ]]; then
+ msg_info "Retrying container creation after upgrade"
+ if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container created successfully after upgrade."
+ return 0
+ else
+ msg_error "pct create still failed after upgrade. See $LOGFILE"
+ return 3
+ fi
+ fi
+ return 1
+ else
+ msg_error "Upgrade failed. Please check APT output."
+ return 3
+ fi
+ ;;
+ *) return 2 ;;
+ esac
+ }
+
+ # ------------------------------------------------------------------------------
+ # Required input variables
+ # ------------------------------------------------------------------------------
+ [[ "${CTID:-}" ]] || {
+ msg_error "You need to set 'CTID' variable."
+ exit 203
+ }
+ [[ "${PCT_OSTYPE:-}" ]] || {
+ msg_error "You need to set 'PCT_OSTYPE' variable."
+ exit 204
+ }
+
+ msg_debug "CTID=$CTID"
+ msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
+ msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
+
+ # ID checks
+ [[ "$CTID" -ge 100 ]] || {
+ msg_error "ID cannot be less than 100."
+ exit 205
+ }
+ if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
+ echo -e "ID '$CTID' is already in use."
+ unset CTID
+ msg_error "Cannot use ID that is already in use."
+ exit 206
+ fi
+
+ # Storage capability check
+ check_storage_support "rootdir" || {
+ msg_error "No valid storage found for 'rootdir' [Container]"
+ exit 1
+ }
+ check_storage_support "vztmpl" || {
+ msg_error "No valid storage found for 'vztmpl' [Template]"
+ exit 1
+ }
+
+ # Template storage selection
+ if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ else
+ while true; do
+ if [[ -z "${var_template_storage:-}" ]]; then
+ if select_storage template; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ break
+ fi
+ fi
+ done
+ fi
+
+ # Container storage selection
+ if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ else
+ if [[ -z "${var_container_storage:-}" ]]; then
+ if select_storage container; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ fi
+ fi
+ fi
+
+ # Validate content types
+ msg_info "Validating content types of storage '$CONTAINER_STORAGE'"
+ STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT"
+ grep -qw "rootdir" <<<"$STORAGE_CONTENT" || {
+ msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC."
+ exit 217
+ }
+ $STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'"
+
+ msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'"
+ TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT"
+ if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then
+ msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail."
+ else
+ $STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'"
+ fi
+
+ # Free space check
+ STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
+ REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
+ [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || {
+ msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
+ exit 214
+ }
+
+ # Cluster quorum (if cluster)
+ if [[ -f /etc/pve/corosync.conf ]]; then
+ msg_info "Checking cluster quorum"
+ if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
+ msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
+ exit 210
+ fi
+ msg_ok "Cluster is quorate"
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Template discovery & validation
+ # ------------------------------------------------------------------------------
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ case "$PCT_OSTYPE" in
+ debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
+ alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
+ *) TEMPLATE_PATTERN="" ;;
+ esac
+
+ msg_info "Searching for template '$TEMPLATE_SEARCH'"
+
+ # Build regex patterns outside awk/grep for clarity
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}"
+
+ #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'"
+ #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'"
+ #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+
+ pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)."
+
+ #echo "[DEBUG] pveam available output (first 5 lines with .tar files):"
+ #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /'
+
+ set +u
+ mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
+ #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found"
+ set -u
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #echo "[DEBUG] Online templates:"
+ for tmpl in "${ONLINE_TEMPLATES[@]}"; do
+ echo " - $tmpl"
+ done
+ fi
+
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'"
+ #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates"
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #msg_debug "First 3 online templates:"
+ count=0
+ for idx in "${!ONLINE_TEMPLATES[@]}"; do
+ #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}"
+ ((count++))
+ [[ $count -ge 3 ]] && break
+ done
+ fi
+ #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ # If still no template, try to find alternatives
+ if [[ -z "$TEMPLATE" ]]; then
+ echo ""
+ echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."
+
+ # Get all available versions for this OS type
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" |
+ sort -u -V 2>/dev/null
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo ""
+ echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ #echo "[DEBUG] Retrying with version: $PCT_OSVERSION"
+
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="online"
+ #echo "[DEBUG] Found alternative: $TEMPLATE"
+ else
+ msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ exit 225
+ fi
+ else
+ msg_info "Installation cancelled"
+ exit 0
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available at all"
+ exit 225
+ fi
+ fi
+
+ #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+ #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"
+
+ # Get available versions
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
+ grep -E '^[0-9]+\.[0-9]+$' |
+ sort -u -V 2>/dev/null || sort -u
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo -e "\n${BL}Available versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ export PCT_OSVERSION="$var_version"
+ msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"
+
+ # Retry template search with new version
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ msg_error "Template still not found after version change"
+ exit 220
+ }
+ else
+ msg_info "Installation cancelled"
+ exit 1
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available"
+ exit 220
+ fi
+ fi
+ }
+
+ # Validate that we found a template
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ msg_info "Please check:"
+ msg_info " - Is pveam catalog available? (run: pveam available -section system)"
+ msg_info " - Does the template exist for your OS version?"
+ exit 225
+ fi
+
+ msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
+ msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
+
+ NEED_DOWNLOAD=0
+ if [[ ! -f "$TEMPLATE_PATH" ]]; then
+ msg_info "Template not present locally – will download."
+ NEED_DOWNLOAD=1
+ elif [[ ! -r "$TEMPLATE_PATH" ]]; then
+ msg_error "Template file exists but is not readable – check permissions."
+ exit 221
+ elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template file too small (<1MB) – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template looks too small, but no online version exists. Keeping local file."
+ fi
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
+ fi
+ else
+ $STD msg_ok "Template $TEMPLATE is present and valid."
+ fi
+
+ if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
+ if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
+ TEMPLATE="$ONLINE_TEMPLATE"
+ NEED_DOWNLOAD=1
+ else
+ msg_info "Continuing with local template $TEMPLATE"
+ fi
+ fi
+
+ if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
+ [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
+ for attempt in {1..3}; do
+ msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
+ if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
+ msg_ok "Template download successful."
+ break
+ fi
+ if [[ $attempt -eq 3 ]]; then
+ msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
+ exit 222
+ fi
+ sleep $((attempt * 5))
+ done
+ fi
+
+ if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
+ msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
+ exit 223
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
+ # ------------------------------------------------------------------------------
+ if [[ "$PCT_OSTYPE" == "debian" ]]; then
+ OSVER="$(parse_template_osver "$TEMPLATE")"
+ if [[ -n "$OSVER" ]]; then
+ # Proactive, aber ohne Abbruch – nur Angebot
+ offer_lxc_stack_upgrade_and_maybe_retry "no" || true
+ fi
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Create LXC Container
+ # ------------------------------------------------------------------------------
+ msg_info "Creating LXC container"
+
+ # Ensure subuid/subgid entries exist
+ grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
+ grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
+
+ # Assemble pct options
+ PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
+ [[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
+
+ # Lock by template file (avoid concurrent downloads/creates)
+ lockfile="/tmp/template.${TEMPLATE}.lock"
+ exec 9>"$lockfile" || {
+ msg_error "Failed to create lock file '$lockfile'."
+ exit 200
+ }
+ flock -w 60 9 || {
+ msg_error "Timeout while waiting for template lock."
+ exit 211
+ }
+
+ LOGFILE="/tmp/pct_create_${CTID}.log"
+ msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}"
+ msg_debug "Logfile: $LOGFILE"
+
+ # First attempt
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then
+ msg_error "Container creation failed on ${TEMPLATE_STORAGE}. Checking template..."
+
+ # Validate template file
+ if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ msg_warn "Template file too small or missing – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
+ fi
+ fi
+
+ # Retry after repair
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ # Fallback to local storage
+ if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
+ msg_warn "Retrying container creation with fallback to local storage..."
+ LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
+ msg_info "Downloading template to local..."
+ pveam download local "$TEMPLATE" >/dev/null 2>&1
+ fi
+ if pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container successfully created using local fallback."
+ else
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed even with local fallback. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ else
+ msg_error "Container creation failed on local storage. See $LOGFILE"
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ fi
+ fi
+
+ # Verify container exists
+ pct list | awk '{print $1}' | grep -qx "$CTID" || {
+ msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE"
+ exit 215
+ }
+
+ # Verify config rootfs
+ grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
+ msg_error "RootFS entry missing in container config. See $LOGFILE"
+ exit 216
+ }
+
+ msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
+}
+
+# ------------------------------------------------------------------------------
+# description()
+#
+# - Sets container description with HTML content (logo, links, badges)
+# - Restarts ping-instances.service if present
+# - Posts status "done" to API
+# ------------------------------------------------------------------------------
+description() {
+ IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+
+ # Generate LXC Description
+ DESCRIPTION=$(
+ cat <
+
+
+
+
+ ${APP} LXC
+
+
+
+
+
+
+
+
+
+ GitHub
+
+
+
+ Discussions
+
+
+
+ Issues
+
+
+EOF
+ )
+ pct set "$CTID" -description "$DESCRIPTION"
+
+ if [[ -f /etc/systemd/system/ping-instances.service ]]; then
+ systemctl start ping-instances.service
+ fi
+
+ post_update_to_api "done" "none"
+}
+
+# ------------------------------------------------------------------------------
+# api_exit_script()
+#
+# - Exit trap handler
+# - Reports exit codes to API with detailed reason
+# - Handles known codes (100–209) and maps them to errors
+# ------------------------------------------------------------------------------
+api_exit_script() {
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then
+ case $exit_code in
+ 100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;;
+ 101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;;
+ 200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;;
+ 201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;;
+ 202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;;
+ 203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;;
+ 204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;;
+ 205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;;
+ 206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;;
+ 207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;;
+ 208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;;
+ 209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;;
+ *) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;;
+ esac
+ fi
+}
+
+if command -v pveversion >/dev/null 2>&1; then
+ trap 'api_exit_script' EXIT
+fi
+trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
+trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
+trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
diff --git a/misc/deferred/build.func.backup-20251029-124307 b/misc/deferred/build.func.backup-20251029-124307
new file mode 100644
index 000000000..d452f4637
--- /dev/null
+++ b/misc/deferred/build.func.backup-20251029-124307
@@ -0,0 +1,3517 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# Revision: 1
+
+# ==============================================================================
+# SECTION 1: CORE INITIALIZATION & VARIABLES
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# variables()
+#
+# - Normalize application name (NSAPP = lowercase, no spaces)
+# - Build installer filename (var_install)
+# - Define regex for integer validation
+# - Fetch hostname of Proxmox node
+# - Set default values for diagnostics/method
+# - Generate random UUID for tracking
+# - Get Proxmox VE version and kernel version
+# ------------------------------------------------------------------------------
+variables() {
+ NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
+ var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
+ INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
+ PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
+ DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
+ METHOD="default" # sets the METHOD variable to "default", used for the API call.
+ RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
+ CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
+ #CT_TYPE=${var_unprivileged:-$CT_TYPE}
+
+ # Get Proxmox VE version and kernel version
+ if command -v pveversion >/dev/null 2>&1; then
+ PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1)
+ else
+ PVEVERSION="N/A"
+ fi
+ KERNEL_VERSION=$(uname -r)
+}
+
+# -----------------------------------------------------------------------------
+# Community-Scripts bootstrap loader
+# - Always sources build.func from remote
+# - Updates local core files only if build.func changed
+# - Local cache: /usr/local/community-scripts/core
+# -----------------------------------------------------------------------------
+
+# FUNC_DIR="/usr/local/community-scripts/core"
+# mkdir -p "$FUNC_DIR"
+
+# BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func"
+# BUILD_REV="$FUNC_DIR/build.rev"
+# DEVMODE="${DEVMODE:-no}"
+
+# # --- Step 1: fetch build.func content once, compute hash ---
+# build_content="$(curl -fsSL "$BUILD_URL")" || {
+# echo "❌ Failed to fetch build.func"
+# exit 1
+# }
+
+# newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}')
+# oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "")
+
+# # --- Step 2: if build.func changed, offer update for core files ---
+# if [ "$newhash" != "$oldhash" ]; then
+# echo "⚠️ build.func changed!"
+
+# while true; do
+# read -rp "Refresh local core files? [y/N/diff]: " ans
+# case "$ans" in
+# [Yy]*)
+# echo "$newhash" >"$BUILD_REV"
+
+# update_func_file() {
+# local file="$1"
+# local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file"
+# local local_path="$FUNC_DIR/$file"
+
+# echo "⬇️ Downloading $file ..."
+# curl -fsSL "$url" -o "$local_path" || {
+# echo "❌ Failed to fetch $file"
+# exit 1
+# }
+# echo "✔️ Updated $file"
+# }
+
+# update_func_file core.func
+# update_func_file error_handler.func
+# update_func_file tools.func
+# break
+# ;;
+# [Dd]*)
+# for file in core.func error_handler.func tools.func; do
+# local_path="$FUNC_DIR/$file"
+# url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file"
+# remote_tmp="$(mktemp)"
+
+# curl -fsSL "$url" -o "$remote_tmp" || continue
+
+# if [ -f "$local_path" ]; then
+# echo "🔍 Diff for $file:"
+# diff -u "$local_path" "$remote_tmp" || echo "(no differences)"
+# else
+# echo "📦 New file $file will be installed"
+# fi
+
+# rm -f "$remote_tmp"
+# done
+# ;;
+# *)
+# echo "❌ Skipped updating local core files"
+# break
+# ;;
+# esac
+# done
+# else
+# if [ "$DEVMODE" != "yes" ]; then
+# echo "✔️ build.func unchanged → using existing local core files"
+# fi
+# fi
+
+# if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then
+# return 0 2>/dev/null || exit 0
+# fi
+# _COMMUNITY_SCRIPTS_LOADER=1
+
+# # --- Step 3: always source local versions of the core files ---
+# source "$FUNC_DIR/core.func"
+# source "$FUNC_DIR/error_handler.func"
+# source "$FUNC_DIR/tools.func"
+
+# # --- Step 4: finally, source build.func directly from memory ---
+# # (no tmp file needed)
+# source <(printf "%s" "$build_content")
+
+# ------------------------------------------------------------------------------
+# Load core + error handler functions from community-scripts repo
+#
+# - Prefer curl if available, fallback to wget
+# - Load: core.func, error_handler.func, api.func
+# - Initialize error traps after loading
+# ------------------------------------------------------------------------------
+
+source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func)
+
+if command -v curl >/dev/null 2>&1; then
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via curl"
+elif command -v wget >/dev/null 2>&1; then
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via wget"
+fi
+
+# ------------------------------------------------------------------------------
+# maxkeys_check()
+#
+# - Reads kernel keyring limits (maxkeys, maxbytes)
+# - Checks current usage for LXC user (UID 100000)
+# - Warns if usage is close to limits and suggests sysctl tuning
+# - Exits if thresholds are exceeded
+# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
+# ------------------------------------------------------------------------------
+
+maxkeys_check() {
+ # Read kernel parameters
+ per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
+ per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
+
+ # Exit if kernel parameters are unavailable
+ if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
+ echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
+ exit 1
+ fi
+
+ # Fetch key usage for user ID 100000 (typical for containers)
+ used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
+ used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
+
+ # Calculate thresholds and suggested new limits
+ threshold_keys=$((per_user_maxkeys - 100))
+ threshold_bytes=$((per_user_maxbytes - 1000))
+ new_limit_keys=$((per_user_maxkeys * 2))
+ new_limit_bytes=$((per_user_maxbytes * 2))
+
+ # Check if key or byte usage is near limits
+ failure=0
+ if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+ if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+
+ # Provide next steps if issues are detected
+ if [[ "$failure" -eq 1 ]]; then
+ echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
+ exit 1
+ fi
+
+ echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
+}
+
+# ------------------------------------------------------------------------------
+# get_current_ip()
+#
+# - Returns current container IP depending on OS type
+# - Debian/Ubuntu: uses `hostname -I`
+# - Alpine: parses eth0 via `ip -4 addr`
+# ------------------------------------------------------------------------------
+get_current_ip() {
+ if [ -f /etc/os-release ]; then
+ # Check for Debian/Ubuntu (uses hostname -I)
+ if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
+ CURRENT_IP=$(hostname -I | awk '{print $1}')
+ # Check for Alpine (uses ip command)
+ elif grep -q 'ID=alpine' /etc/os-release; then
+ CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
+ else
+ CURRENT_IP="Unknown"
+ fi
+ fi
+ echo "$CURRENT_IP"
+}
+
+# ------------------------------------------------------------------------------
+# update_motd_ip()
+#
+# - Updates /etc/motd with current container IP
+# - Removes old IP entries to avoid duplicates
+# ------------------------------------------------------------------------------
+update_motd_ip() {
+ MOTD_FILE="/etc/motd"
+
+ if [ -f "$MOTD_FILE" ]; then
+ # Remove existing IP Address lines to prevent duplication
+ sed -i '/IP Address:/d' "$MOTD_FILE"
+
+ IP=$(get_current_ip)
+ # Add the new IP address
+ echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# install_ssh_keys_into_ct()
+#
+# - Installs SSH keys into container root account if SSH is enabled
+# - Uses pct push or direct input to authorized_keys
+# - Falls back to warning if no keys provided
+# ------------------------------------------------------------------------------
+install_ssh_keys_into_ct() {
+ [[ "$SSH" != "yes" ]] && return 0
+
+ if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
+ 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
+ }
+ 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
+ }
+ pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
+ msg_ok "Installed SSH keys into CT ${CTID}"
+ return 0
+ fi
+
+ # Fallback: nichts ausgewählt
+ msg_warn "No SSH keys to install (skipping)."
+ return 0
+}
+
+# ------------------------------------------------------------------------------
+# base_settings()
+#
+# - Defines all base/default variables for container creation
+# - Reads from environment variables (var_*)
+# - Provides fallback defaults for OS type/version
+# ------------------------------------------------------------------------------
+base_settings() {
+ # Default Settings
+ CT_TYPE=${var_unprivileged:-"1"}
+ DISK_SIZE=${var_disk:-"4"}
+ CORE_COUNT=${var_cpu:-"1"}
+ RAM_SIZE=${var_ram:-"1024"}
+ VERBOSE=${var_verbose:-"${1:-no}"}
+ PW=${var_pw:-""}
+ CT_ID=${var_ctid:-$NEXTID}
+ HN=${var_hostname:-$NSAPP}
+ BRG=${var_brg:-"vmbr0"}
+ NET=${var_net:-"dhcp"}
+ IPV6_METHOD=${var_ipv6_method:-"none"}
+ IPV6_STATIC=${var_ipv6_static:-""}
+ GATE=${var_gateway:-""}
+ APT_CACHER=${var_apt_cacher:-""}
+ APT_CACHER_IP=${var_apt_cacher_ip:-""}
+ MTU=${var_mtu:-""}
+ SD=${var_storage:-""}
+ NS=${var_ns:-""}
+ MAC=${var_mac:-""}
+ VLAN=${var_vlan:-""}
+ SSH=${var_ssh:-"no"}
+ SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
+ UDHCPC_FIX=${var_udhcpc_fix:-""}
+ TAGS="community-script,${var_tags:-}"
+ ENABLE_FUSE=${var_fuse:-"${1:-no}"}
+ ENABLE_TUN=${var_tun:-"${1:-no}"}
+
+ # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
+ if [ -z "$var_os" ]; then
+ var_os="debian"
+ fi
+ if [ -z "$var_version" ]; then
+ var_version="12"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# echo_default()
+#
+# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
+# - Uses icons and formatting for readability
+# - Convert CT_TYPE to description
+# ------------------------------------------------------------------------------
+echo_default() {
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ if [ "$VERBOSE" == "yes" ]; then
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
+ fi
+ echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
+ echo -e " "
+}
+
+# ------------------------------------------------------------------------------
+# exit_script()
+#
+# - Called when user cancels an action
+# - Clears screen and exits gracefully
+# ------------------------------------------------------------------------------
+exit_script() {
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
+}
+
+# ------------------------------------------------------------------------------
+# find_host_ssh_keys()
+#
+# - Scans system for available SSH keys
+# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
+# - Returns list of files containing valid SSH public keys
+# - Sets FOUND_HOST_KEY_COUNT to number of keys found
+# ------------------------------------------------------------------------------
+find_host_ssh_keys() {
+ local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
+ local -a files=() cand=()
+ local g="${var_ssh_import_glob:-}"
+ local total=0 f base c
+
+ shopt -s nullglob
+ if [[ -n "$g" ]]; then
+ for pat in $g; do cand+=($pat); done
+ else
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ fi
+ shopt -u nullglob
+
+ for f in "${cand[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # CRLF safe check for host keys
+ c=$(tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ {print}
+ ' | grep -E -c '"$re"' || true)
+
+ if ((c > 0)); then
+ files+=("$f")
+ total=$((total + c))
+ fi
+ done
+
+ # Fallback to /root/.ssh/authorized_keys
+ if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
+ if grep -E -q "$re" /root/.ssh/authorized_keys; then
+ files+=(/root/.ssh/authorized_keys)
+ total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
+ fi
+ fi
+
+ FOUND_HOST_KEY_COUNT="$total"
+ (
+ IFS=:
+ echo "${files[*]}"
+ )
+}
+
+# ------------------------------------------------------------------------------
+# advanced_settings()
+#
+# - Interactive whiptail menu for advanced configuration
+# - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM
+# - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode
+# - Ends with confirmation or re-entry if cancelled
+# ------------------------------------------------------------------------------
+advanced_settings() {
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
+ # Setting Default Tag for Advanced Settings
+ TAGS="community-script;${var_tags:-}"
+ CT_DEFAULT_TYPE="${CT_TYPE}"
+ CT_TYPE=""
+ while [ -z "$CT_TYPE" ]; do
+ if [ "$CT_DEFAULT_TYPE" == "1" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" ON \
+ "0" "Privileged" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os |${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ if [ "$CT_DEFAULT_TYPE" == "0" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" OFF \
+ "0" "Privileged" ON \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
+ echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ done
+
+ while true; do
+ if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
+ # Empty = Autologin
+ if [[ -z "$PW1" ]]; then
+ PW=""
+ PW1="Automatic Login"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
+ break
+ fi
+
+ # Invalid: contains spaces
+ if [[ "$PW1" == *" "* ]]; then
+ whiptail --msgbox "Password cannot contain spaces." 8 58
+ continue
+ fi
+
+ # Invalid: too short
+ if ((${#PW1} < 5)); then
+ whiptail --msgbox "Password must be at least 5 characters." 8 58
+ continue
+ fi
+
+ # Confirm password
+ if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
+ if [[ "$PW1" == "$PW2" ]]; then
+ PW="-password $PW1"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
+ break
+ else
+ whiptail --msgbox "Passwords do not match. Please try again." 8 58
+ fi
+ else
+ exit_script
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_ID" ]; then
+ CT_ID="$NEXTID"
+ fi
+ else
+ exit_script
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
+
+ while true; do
+ if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_NAME" ]; then
+ HN="$NSAPP"
+ else
+ HN=$(echo "${CT_NAME,,}" | tr -d ' ')
+ fi
+ # Hostname validate (RFC 1123)
+ if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ while true; do
+ DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$DISK_SIZE" ]; then
+ DISK_SIZE="$var_disk"
+ fi
+
+ if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ break
+ else
+ whiptail --msgbox "Disk size must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$CORE_COUNT" ]; then
+ CORE_COUNT="$var_cpu"
+ fi
+
+ if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ break
+ else
+ whiptail --msgbox "CPU core count must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$RAM_SIZE" ]; then
+ RAM_SIZE="$var_ram"
+ fi
+
+ if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ break
+ else
+ whiptail --msgbox "RAM size must be a positive integer!" 8 58
+ fi
+ done
+
+ IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
+ BRIDGES=""
+ OLD_IFS=$IFS
+ IFS=$'\n'
+ for iface_filepath in ${IFACE_FILEPATH_LIST}; do
+
+ iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
+ (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
+
+ if [ -f "${iface_indexes_tmpfile}" ]; then
+
+ while read -r pair; do
+ start=$(echo "${pair}" | cut -d':' -f1)
+ end=$(echo "${pair}" | cut -d':' -f2)
+
+ if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
+ iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
+ BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
+ fi
+
+ done <"${iface_indexes_tmpfile}"
+ rm -f "${iface_indexes_tmpfile}"
+ fi
+
+ done
+ IFS=$OLD_IFS
+ BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
+ if [[ -z "$BRIDGES" ]]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ # Build bridge menu with descriptions
+ BRIDGE_MENU_OPTIONS=()
+ while IFS= read -r bridge; do
+ if [[ -n "$bridge" ]]; then
+ # Get description from Proxmox built-in method - find comment for this specific bridge
+ description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//')
+ if [[ -n "$description" ]]; then
+ BRIDGE_MENU_OPTIONS+=("$bridge" "${description}")
+ else
+ BRIDGE_MENU_OPTIONS+=("$bridge" " ")
+ fi
+ fi
+ done <<<"$BRIDGES"
+
+ BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3)
+ if [[ -z "$BRG" ]]; then
+ exit_script
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
+ fi
+
+ # IPv4 methods: dhcp, static, none
+ while true; do
+ IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "IPv4 Address Management" \
+ --menu "Select IPv4 Address Assignment Method:" 12 60 2 \
+ "dhcp" "Automatic (DHCP, recommended)" \
+ "static" "Static (manual entry)" \
+ 3>&1 1>&2 2>&3)
+
+ exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ exit_script
+ fi
+
+ case "$IPV4_METHOD" in
+ dhcp)
+ NET="dhcp"
+ GATE=""
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}"
+ break
+ ;;
+ static)
+ # Static: call and validate CIDR address
+ while true; do
+ NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \
+ --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3)
+ if [ -z "$NET" ]; then
+ whiptail --msgbox "IPv4 address must not be empty." 8 58
+ continue
+ elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}"
+ break
+ else
+ whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58
+ fi
+ done
+
+ # call and validate Gateway
+ while true; do
+ GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \
+ --title "Gateway IP" 3>&1 1>&2 2>&3)
+ if [ -z "$GATE1" ]; then
+ whiptail --msgbox "Gateway IP address cannot be empty." 8 58
+ elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
+ whiptail --msgbox "Invalid Gateway IP address format." 8 58
+ else
+ GATE=",gw=$GATE1"
+ echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
+ break
+ fi
+ done
+ break
+ ;;
+ esac
+ done
+
+ # IPv6 Address Management selection
+ while true; do
+ IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \
+ "Select IPv6 Address Management Type:" 15 58 4 \
+ "auto" "SLAAC/AUTO (recommended, default)" \
+ "dhcp" "DHCPv6" \
+ "static" "Static (manual entry)" \
+ "none" "Disabled" \
+ --default-item "auto" 3>&1 1>&2 2>&3)
+ [ $? -ne 0 ] && exit_script
+
+ case "$IPV6_METHOD" in
+ auto)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}"
+ IPV6_ADDR=""
+ IPV6_GATE=""
+ break
+ ;;
+ dhcp)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}"
+ IPV6_ADDR="dhcp"
+ IPV6_GATE=""
+ break
+ ;;
+ static)
+ # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64)
+ while true; do
+ IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \
+ --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script
+ if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58
+ fi
+ done
+ # Optional: ask for IPv6 gateway for static config
+ while true; do
+ IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3)
+ if [ -z "$IPV6_GATE" ]; then
+ IPV6_GATE=""
+ break
+ elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "Invalid IPv6 gateway format." 8 58
+ fi
+ done
+ break
+ ;;
+ none)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}"
+ IPV6_ADDR="none"
+ IPV6_GATE=""
+ break
+ ;;
+ *)
+ exit_script
+ ;;
+ esac
+ done
+
+ if [ "$var_os" == "alpine" ]; then
+ APT_CACHER=""
+ APT_CACHER_IP=""
+ else
+ if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
+ APT_CACHER="${APT_CACHER_IP:+yes}"
+ echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
+ else
+ exit_script
+ fi
+ fi
+
+ # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then
+ # DISABLEIP6="yes"
+ # else
+ # DISABLEIP6="no"
+ # fi
+ # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}"
+
+ if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
+ if [ -z "$MTU1" ]; then
+ MTU1="Default"
+ MTU=""
+ else
+ MTU=",mtu=$MTU1"
+ fi
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ exit_script
+ fi
+
+ if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
+ if [ -z "$SD" ]; then
+ SX=Host
+ SD=""
+ else
+ SX=$SD
+ SD="-searchdomain=$SD"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
+ else
+ exit_script
+ fi
+
+ if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
+ if [ -z "$NX" ]; then
+ NX=Host
+ NS=""
+ else
+ NS="-nameserver=$NX"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
+ else
+ exit_script
+ fi
+
+ if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then
+ UDHCPC_FIX="yes"
+ else
+ UDHCPC_FIX="no"
+ fi
+ export UDHCPC_FIX
+
+ if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
+ if [ -z "$MAC1" ]; then
+ MAC1="Default"
+ MAC=""
+ else
+ MAC=",hwaddr=$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
+ else
+ exit_script
+ fi
+
+ if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
+ if [ -z "$VLAN1" ]; then
+ VLAN1="Default"
+ VLAN=""
+ else
+ VLAN=",tag=$VLAN1"
+ fi
+ echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
+ else
+ exit_script
+ fi
+
+ if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
+ if [ -n "${ADV_TAGS}" ]; then
+ ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
+ TAGS="${ADV_TAGS}"
+ else
+ TAGS=";"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
+ else
+ exit_script
+ fi
+
+ configure_ssh_settings
+ export SSH_KEYS_FILE
+ echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
+ ENABLE_FUSE="yes"
+ else
+ ENABLE_FUSE="no"
+ fi
+ echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
+ VERBOSE="yes"
+ else
+ VERBOSE="no"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
+ echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
+ else
+ clear
+ header_info
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
+ advanced_settings
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# diagnostics_check()
+#
+# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
+# - Asks user whether to send anonymous diagnostic data
+# - Saves DIAGNOSTICS=yes/no in the config file
+# ------------------------------------------------------------------------------
+diagnostics_check() {
+ if ! [ -d "/usr/local/community-scripts" ]; then
+ mkdir -p /usr/local/community-scripts
+ fi
+
+ if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=yes
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="yes"
+ else
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=no
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="no"
+ fi
+ else
+ DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
+
+ fi
+
+}
+
+# ------------------------------------------------------------------------------
+# default_var_settings
+#
+# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
+# - Loads var_* values from default.vars (safe parser, no source/eval)
+# - Precedence: ENV var_* > default.vars > built-in defaults
+# - Maps var_verbose → VERBOSE
+# - Calls base_settings "$VERBOSE" and echo_default
+# ------------------------------------------------------------------------------
+default_var_settings() {
+ # Allowed var_* keys (alphabetically sorted)
+ local VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+
+ # Snapshot: environment variables (highest precedence)
+ declare -A _HARD_ENV=()
+ local _k
+ for _k in "${VAR_WHITELIST[@]}"; do
+ if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
+ done
+
+ # Find default.vars location
+ local _find_default_vars
+ _find_default_vars() {
+ local f
+ for f in \
+ /usr/local/community-scripts/default.vars \
+ "$HOME/.config/community-scripts/default.vars" \
+ "./default.vars"; do
+ [ -f "$f" ] && {
+ echo "$f"
+ return 0
+ }
+ done
+ return 1
+ }
+ # Allow override of storages via env (for non-interactive use cases)
+ [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
+ [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
+
+ # Create once, with storages already selected, no var_ctid/var_hostname lines
+ local _ensure_default_vars
+ _ensure_default_vars() {
+ _find_default_vars >/dev/null 2>&1 && return 0
+
+ local canonical="/usr/local/community-scripts/default.vars"
+ msg_info "No default.vars found. Creating ${canonical}"
+ mkdir -p /usr/local/community-scripts
+
+ # Pick storages before writing the file (always ask unless only one)
+ # Create a minimal temp file to write into
+ : >"$canonical"
+
+ # Base content (no var_ctid / var_hostname here)
+ cat >"$canonical" <<'EOF'
+# Community-Scripts defaults (var_* only). Lines starting with # are comments.
+# Precedence: ENV var_* > default.vars > built-ins.
+# Keep keys alphabetically sorted.
+
+# Container type
+var_unprivileged=1
+
+# Resources
+var_cpu=1
+var_disk=4
+var_ram=1024
+
+# Network
+var_brg=vmbr0
+var_net=dhcp
+var_ipv6_method=none
+# var_gateway=
+# var_ipv6_static=
+# var_vlan=
+# var_mtu=
+# var_mac=
+# var_ns=
+
+# SSH
+var_ssh=no
+# var_ssh_authorized_key=
+
+# APT cacher (optional)
+# var_apt_cacher=yes
+# var_apt_cacher_ip=192.168.1.10
+
+# Features/Tags/verbosity
+var_fuse=no
+var_tun=no
+var_tags=community-script
+var_verbose=no
+
+# Security (root PW) – empty => autologin
+# var_pw=
+EOF
+
+ # Now choose storages (always prompt unless just one exists)
+ choose_and_set_storage_for_file "$canonical" template
+ choose_and_set_storage_for_file "$canonical" container
+
+ chmod 0644 "$canonical"
+ msg_ok "Created ${canonical}"
+ }
+
+ # Whitelist check
+ local _is_whitelisted_key
+ _is_whitelisted_key() {
+ local k="$1"
+ local w
+ for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
+ return 1
+ }
+
+ # Safe parser for KEY=VALUE lines
+ local _load_vars_file
+ _load_vars_file() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ msg_info "Loading defaults from ${file}"
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [[ -z "$line" || "$line" == \#* ]] && continue
+ if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
+ local var_key="${BASH_REMATCH[1]}"
+ local var_val="${BASH_REMATCH[2]}"
+
+ [[ "$var_key" != var_* ]] && continue
+ _is_whitelisted_key "$var_key" || {
+ msg_debug "Ignore non-whitelisted ${var_key}"
+ continue
+ }
+
+ # Strip quotes
+ if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ fi
+
+ # Unsafe characters
+ case $var_val in
+ \"*\")
+ var_val=${var_val#\"}
+ var_val=${var_val%\"}
+ ;;
+ \'*\')
+ var_val=${var_val#\'}
+ var_val=${var_val%\'}
+ ;;
+ esac # Hard env wins
+ [[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue
+ # Set only if not already exported
+ [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
+ else
+ msg_warn "Malformed line in ${file}: ${line}"
+ fi
+ done <"$file"
+ msg_ok "Loaded ${file}"
+ }
+
+ # 1) Ensure file exists
+ _ensure_default_vars
+
+ # 2) Load file
+ local dv
+ dv="$(_find_default_vars)" || {
+ msg_error "default.vars not found after ensure step"
+ return 1
+ }
+ _load_vars_file "$dv"
+
+ # 3) Map var_verbose → VERBOSE
+ if [[ -n "${var_verbose:-}" ]]; then
+ case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
+ else
+ VERBOSE="no"
+ fi
+
+ # 4) Apply base settings and show summary
+ METHOD="mydefaults-global"
+ base_settings "$VERBOSE"
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
+ echo_default
+}
+
+# ------------------------------------------------------------------------------
+# get_app_defaults_path()
+#
+# - Returns full path for app-specific defaults file
+# - Example: /usr/local/community-scripts/defaults/.vars
+# ------------------------------------------------------------------------------
+
+get_app_defaults_path() {
+ local n="${NSAPP:-${APP,,}}"
+ echo "/usr/local/community-scripts/defaults/${n}.vars"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults
+#
+# - Called after advanced_settings returned with fully chosen values.
+# - If no .vars exists, offers to persist current advanced settings
+# into /usr/local/community-scripts/defaults/.vars
+# - Only writes whitelisted var_* keys.
+# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
+# ------------------------------------------------------------------------------
+if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
+ declare -ag VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+fi
+
+# Note: _is_whitelisted_key() is defined above in default_var_settings section
+
+_sanitize_value() {
+ # Disallow Command-Substitution / Shell-Meta
+ case "$1" in
+ *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
+ echo ""
+ return 0
+ ;;
+ esac
+ echo "$1"
+}
+
+# Map-Parser: read var_* from file into _VARS_IN associative array
+# Note: Main _load_vars_file() with full validation is defined in default_var_settings section
+# This simplified version is used specifically for diff operations via _VARS_IN array
+declare -A _VARS_IN
+_load_vars_file_to_map() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ _VARS_IN=() # Clear array
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [ -z "$line" ] && continue
+ case "$line" in
+ \#*) continue ;;
+ esac
+ key=$(printf "%s" "$line" | cut -d= -f1)
+ val=$(printf "%s" "$line" | cut -d= -f2-)
+ case "$key" in
+ var_*)
+ if _is_whitelisted_key "$key"; then
+ _VARS_IN["$key"]="$val"
+ fi
+ ;;
+ esac
+ done <"$file"
+}
+
+# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
+_build_vars_diff() {
+ local oldf="$1" newf="$2"
+ local k
+ local -A OLD=() NEW=()
+ _load_vars_file_to_map "$oldf"
+ for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
+ _load_vars_file_to_map "$newf"
+ for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
+
+ local out
+ out+="# Diff for ${APP} (${NSAPP})\n"
+ out+="# Old: ${oldf}\n# New: ${newf}\n\n"
+
+ local found_change=0
+
+ # Changed & Removed
+ for k in "${!OLD[@]}"; do
+ if [[ -v NEW["$k"] ]]; then
+ if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
+ out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ else
+ out+="- ${k}\n - old: ${OLD[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ # Added
+ for k in "${!NEW[@]}"; do
+ if [[ ! -v OLD["$k"] ]]; then
+ out+="+ ${k}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ if [[ $found_change -eq 0 ]]; then
+ out+="(No differences)\n"
+ fi
+
+ printf "%b" "$out"
+}
+
+# Build a temporary .vars file from current advanced settings
+_build_current_app_vars_tmp() {
+ tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
+
+ # NET/GW
+ _net="${NET:-}"
+ _gate=""
+ case "${GATE:-}" in
+ ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
+ esac
+
+ # IPv6
+ _ipv6_method="${IPV6_METHOD:-auto}"
+ _ipv6_static=""
+ _ipv6_gateway=""
+ if [ "$_ipv6_method" = "static" ]; then
+ _ipv6_static="${IPV6_ADDR:-}"
+ _ipv6_gateway="${IPV6_GATE:-}"
+ fi
+
+ # MTU/VLAN/MAC
+ _mtu=""
+ _vlan=""
+ _mac=""
+ case "${MTU:-}" in
+ ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
+ esac
+ case "${VLAN:-}" in
+ ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
+ esac
+ case "${MAC:-}" in
+ ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
+ esac
+
+ # DNS / Searchdomain
+ _ns=""
+ _searchdomain=""
+ case "${NS:-}" in
+ -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
+ esac
+ case "${SD:-}" in
+ -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
+ esac
+
+ # SSH / APT / Features
+ _ssh="${SSH:-no}"
+ _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
+ _apt_cacher="${APT_CACHER:-}"
+ _apt_cacher_ip="${APT_CACHER_IP:-}"
+ _fuse="${ENABLE_FUSE:-no}"
+ _tun="${ENABLE_TUN:-no}"
+ _tags="${TAGS:-}"
+ _verbose="${VERBOSE:-no}"
+
+ # Type / Resources / Identity
+ _unpriv="${CT_TYPE:-1}"
+ _cpu="${CORE_COUNT:-1}"
+ _ram="${RAM_SIZE:-1024}"
+ _disk="${DISK_SIZE:-4}"
+ _hostname="${HN:-$NSAPP}"
+
+ # Storage
+ _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
+ _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
+
+ {
+ echo "# App-specific defaults for ${APP} (${NSAPP})"
+ echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo
+
+ echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
+ echo "var_cpu=$(_sanitize_value "$_cpu")"
+ echo "var_ram=$(_sanitize_value "$_ram")"
+ echo "var_disk=$(_sanitize_value "$_disk")"
+
+ [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
+ [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
+ [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
+ [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
+ [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
+ [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
+ [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
+
+ [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
+ [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")"
+
+ [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
+ [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
+
+ [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
+ [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
+
+ [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
+ [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
+ [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
+ [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
+
+ [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
+ [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
+
+ [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
+ [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
+ } >"$tmpf"
+
+ echo "$tmpf"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults()
+#
+# - Called after advanced_settings()
+# - Offers to save current values as app defaults if not existing
+# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
+# ------------------------------------------------------------------------------
+maybe_offer_save_app_defaults() {
+ local app_vars_path
+ app_vars_path="$(get_app_defaults_path)"
+
+ # always build from current settings
+ local new_tmp diff_tmp
+ new_tmp="$(_build_current_app_vars_tmp)"
+ diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
+
+ # 1) if no file → offer to create
+ if [[ ! -f "$app_vars_path" ]]; then
+ if whiptail --backtitle "[dev] 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"
+ msg_ok "Saved app defaults: ${app_vars_path}"
+ fi
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 2) if file exists → build diff
+ _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
+
+ # if no differences → do nothing
+ if grep -q "^(No differences)$" "$diff_tmp"; then
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 3) if file exists → show menu with default selection "Update Defaults"
+ local app_vars_file
+ app_vars_file="$(basename "$app_vars_path")"
+
+ while true; do
+ local sel
+ sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "APP DEFAULTS – ${APP}" \
+ --menu "Differences detected. What do you want to do?" 20 78 10 \
+ "Update Defaults" "Write new values to ${app_vars_file}" \
+ "Keep Current" "Keep existing defaults (no changes)" \
+ "View Diff" "Show a detailed diff" \
+ "Cancel" "Abort without changes" \
+ --default-item "Update Defaults" \
+ 3>&1 1>&2 2>&3)" || { sel="Cancel"; }
+
+ case "$sel" in
+ "Update Defaults")
+ install -m 0644 "$new_tmp" "$app_vars_path"
+ msg_ok "Updated app defaults: ${app_vars_path}"
+ break
+ ;;
+ "Keep Current")
+ msg_info "Keeping current app defaults: ${app_vars_path}"
+ break
+ ;;
+ "View Diff")
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Diff – ${APP}" \
+ --scrolltext --textbox "$diff_tmp" 25 100
+ ;;
+ "Cancel" | *)
+ msg_info "Canceled. No changes to app defaults."
+ break
+ ;;
+ esac
+ done
+
+ rm -f "$new_tmp" "$diff_tmp"
+}
+
+ensure_storage_selection_for_vars_file() {
+ local vf="$1"
+
+ # 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-)
+
+ if [[ -n "$tpl" && -n "$ct" ]]; then
+ TEMPLATE_STORAGE="$tpl"
+ CONTAINER_STORAGE="$ct"
+ return 0
+ fi
+
+ choose_and_set_storage_for_file "$vf" template
+ choose_and_set_storage_for_file "$vf" container
+
+ msg_ok "Storage configuration saved to $(basename "$vf")"
+}
+
+diagnostics_menu() {
+ if [ "${DIAGNOSTICS:-no}" = "yes" ]; then
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "No" --no-button "Back"; then
+ DIAGNOSTICS="no"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ else
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "Yes" --no-button "Back"; then
+ DIAGNOSTICS="yes"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ fi
+}
+
+ensure_global_default_vars_file() {
+ local vars_path="/usr/local/community-scripts/default.vars"
+ if [[ ! -f "$vars_path" ]]; then
+ mkdir -p "$(dirname "$vars_path")"
+ touch "$vars_path"
+ fi
+ echo "$vars_path"
+}
+
+# ------------------------------------------------------------------------------
+# install_script()
+#
+# - Main entrypoint for installation mode
+# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
+# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
+# - Applies chosen settings and triggers container build
+# ------------------------------------------------------------------------------
+install_script() {
+ pve_check
+ shell_check
+ root_check
+ arch_check
+ ssh_check
+ maxkeys_check
+ diagnostics_check
+
+ if systemctl is-active -q ping-instances.service; then
+ systemctl -q stop ping-instances.service
+ fi
+
+ NEXTID=$(pvesh get /cluster/nextid)
+ timezone=$(cat /etc/timezone)
+
+ # Show APP Header
+ header_info
+
+ # --- Support CLI argument as direct preset (default, advanced, …) ---
+ CHOICE="${mode:-${1:-}}"
+
+ # If no CLI argument → show whiptail menu
+ # Build menu dynamically based on available options
+ local appdefaults_option=""
+ local settings_option=""
+ local menu_items=(
+ "1" "Default Install"
+ "2" "Advanced Install"
+ "3" "My Defaults"
+ )
+
+ if [ -f "$(get_app_defaults_path)" ]; then
+ appdefaults_option="4"
+ menu_items+=("4" "App Defaults for ${APP}")
+ settings_option="5"
+ menu_items+=("5" "Settings")
+ else
+ settings_option="4"
+ menu_items+=("4" "Settings")
+ fi
+
+ if [ -z "$CHOICE" ]; then
+
+ TMP_CHOICE=$(whiptail \
+ --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts Options" \
+ --ok-button "Select" --cancel-button "Exit Script" \
+ --notags \
+ --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
+ 20 60 9 \
+ "${menu_items[@]}" \
+ --default-item "1" \
+ 3>&1 1>&2 2>&3) || exit_script
+ CHOICE="$TMP_CHOICE"
+ fi
+
+ APPDEFAULTS_OPTION="$appdefaults_option"
+ SETTINGS_OPTION="$settings_option"
+
+ # --- Main case ---
+ local defaults_target=""
+ local run_maybe_offer="no"
+ case "$CHOICE" in
+ 1 | default | DEFAULT)
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
+ VERBOSE="no"
+ METHOD="default"
+ base_settings "$VERBOSE"
+ echo_default
+ defaults_target="$(ensure_global_default_vars_file)"
+ ;;
+ 2 | advanced | ADVANCED)
+ header_info
+
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ METHOD="advanced"
+ base_settings
+ advanced_settings
+ defaults_target="$(ensure_global_default_vars_file)"
+ run_maybe_offer="yes"
+ ;;
+ 3 | mydefaults | MYDEFAULTS)
+ default_var_settings || {
+ msg_error "Failed to apply default.vars"
+ exit 1
+ }
+ defaults_target="/usr/local/community-scripts/default.vars"
+ ;;
+ "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
+ METHOD="appdefaults"
+ base_settings
+ _load_vars_file "$(get_app_defaults_path)"
+ echo_default
+ defaults_target="$(get_app_defaults_path)"
+ else
+ msg_error "No App Defaults available for ${APP}"
+ exit 1
+ fi
+ ;;
+ "$SETTINGS_OPTION" | settings | SETTINGS)
+ settings_menu
+ defaults_target=""
+ ;;
+ *)
+ echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}"
+ exit 1
+ ;;
+ esac
+
+ if [[ -n "$defaults_target" ]]; then
+ ensure_storage_selection_for_vars_file "$defaults_target"
+ fi
+
+ if [[ "$run_maybe_offer" == "yes" ]]; then
+ maybe_offer_save_app_defaults
+ fi
+}
+
+edit_default_storage() {
+ local vf="/usr/local/community-scripts/default.vars"
+
+ # Ensure file exists
+ if [[ ! -f "$vf" ]]; then
+ mkdir -p "$(dirname "$vf")"
+ touch "$vf"
+ fi
+
+ # Let ensure_storage_selection_for_vars_file handle everything
+ ensure_storage_selection_for_vars_file "$vf"
+}
+
+settings_menu() {
+ while true; do
+ local settings_items=(
+ "1" "Manage API-Diagnostic Setting"
+ "2" "Edit Default.vars"
+ "3" "Edit Default Storage"
+ )
+ if [ -f "$(get_app_defaults_path)" ]; then
+ settings_items+=("4" "Edit App.vars for ${APP}")
+ settings_items+=("5" "Exit")
+ else
+ settings_items+=("4" "Exit")
+ fi
+
+ local choice
+ choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts SETTINGS Menu" \
+ --ok-button "OK" --cancel-button "Back" \
+ --menu "\n\nChoose a settings option:\n\nUse TAB or Arrow keys to navigate, ENTER to select." 20 60 9 \
+ "${settings_items[@]}" \
+ 3>&1 1>&2 2>&3) || break
+
+ case "$choice" in
+ 1) diagnostics_menu ;;
+ 2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
+ 3) edit_default_storage ;;
+ 4)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ ${EDITOR:-nano} "$(get_app_defaults_path)"
+ else
+ exit_script
+ fi
+ ;;
+ 5) exit_script ;;
+ esac
+ done
+}
+
+# ===== Unified storage selection & writing to vars files =====
+_write_storage_to_vars() {
+ # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
+ local vf="$1" key="$2" val="$3"
+ # remove uncommented and commented versions to avoid duplicates
+ sed -i "/^[#[:space:]]*${key}=/d" "$vf"
+ echo "${key}=${val}" >>"$vf"
+}
+
+choose_and_set_storage_for_file() {
+ # $1 = vars_file, $2 = class ('container'|'template')
+ local vf="$1" class="$2" key="" current=""
+ case "$class" in
+ container) key="var_container_storage" ;;
+ template) key="var_template_storage" ;;
+ *)
+ msg_error "Unknown storage class: $class"
+ return 1
+ ;;
+ esac
+
+ current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
+
+ # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
+ local content="rootdir"
+ [[ "$class" == "template" ]] && content="vztmpl"
+ local count
+ count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
+
+ if [[ "$count" -eq 1 ]]; then
+ STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
+ STORAGE_INFO=""
+ else
+ # If the current value is preselectable, we could show it, but per your requirement we always offer selection
+ select_storage "$class" || return 1
+ fi
+
+ _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
+
+ # Keep environment in sync for later steps (e.g. app-default save)
+ if [[ "$class" == "container" ]]; then
+ export var_container_storage="$STORAGE_RESULT"
+ export CONTAINER_STORAGE="$STORAGE_RESULT"
+ else
+ export var_template_storage="$STORAGE_RESULT"
+ export TEMPLATE_STORAGE="$STORAGE_RESULT"
+ fi
+
+ msg_ok "Updated ${key} → ${STORAGE_RESULT}"
+}
+
+# ------------------------------------------------------------------------------
+# check_container_resources()
+#
+# - Compares host RAM/CPU with required values
+# - Warns if under-provisioned and asks user to continue or abort
+# ------------------------------------------------------------------------------
+check_container_resources() {
+ current_ram=$(free -m | awk 'NR==2{print $2}')
+ current_cpu=$(nproc)
+
+ if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
+ echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
+ echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
+ echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
+ echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ else
+ echo -e ""
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# check_container_storage()
+#
+# - Checks /boot partition usage
+# - Warns if usage >80% and asks user confirmation before proceeding
+# ------------------------------------------------------------------------------
+check_container_storage() {
+ total_size=$(df /boot --output=size | tail -n 1)
+ local used_size=$(df /boot --output=used | tail -n 1)
+ usage=$((100 * used_size / total_size))
+ if ((usage > 80)); then
+ echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
+ echo -ne "Continue anyway? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
+ echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# ssh_extract_keys_from_file()
+#
+# - Extracts valid SSH public keys from given file
+# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
+# ------------------------------------------------------------------------------
+ssh_extract_keys_from_file() {
+ local f="$1"
+ [[ -r "$f" ]] || return 0
+ tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ # nackt: typ base64 [comment]
+ /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
+ # mit Optionen: finde ab erstem Key-Typ
+ {
+ match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
+ if (RSTART>0) { print substr($0, RSTART) }
+ }
+ '
+}
+
+# ------------------------------------------------------------------------------
+# ssh_build_choices_from_files()
+#
+# - Builds interactive whiptail checklist of available SSH keys
+# - Generates fingerprint, type and comment for each key
+# ------------------------------------------------------------------------------
+ssh_build_choices_from_files() {
+ local -a files=("$@")
+ CHOICES=()
+ COUNT=0
+ MAPFILE="$(mktemp)"
+ local id key typ fp cmt base ln=0
+
+ for f in "${files[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # map every key in file
+ while IFS= read -r key; do
+ [[ -n "$key" ]] || continue
+
+ typ=""
+ fp=""
+ cmt=""
+ # Only the pure key part (without options) is already included in ‘key’.
+ read -r _typ _b64 _cmt <<<"$key"
+ typ="${_typ:-key}"
+ cmt="${_cmt:-}"
+ # Fingerprint via ssh-keygen (if available)
+ if command -v ssh-keygen >/dev/null 2>&1; then
+ fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
+ fi
+ # Label shorten
+ [[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
+
+ ln=$((ln + 1))
+ COUNT=$((COUNT + 1))
+ id="K${COUNT}"
+ echo "${id}|${key}" >>"$MAPFILE"
+ CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
+ done < <(ssh_extract_keys_from_file "$f")
+ done
+}
+
+# ------------------------------------------------------------------------------
+# ssh_discover_default_files()
+#
+# - Scans standard paths for SSH keys
+# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc.
+# ------------------------------------------------------------------------------
+ssh_discover_default_files() {
+ local -a cand=()
+ shopt -s nullglob
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ shopt -u nullglob
+ printf '%s\0' "${cand[@]}"
+}
+
+configure_ssh_settings() {
+ SSH_KEYS_FILE="$(mktemp)"
+ : >"$SSH_KEYS_FILE"
+
+ IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
+ ssh_build_choices_from_files "${_def_files[@]}"
+ local default_key_count="$COUNT"
+
+ local ssh_key_mode
+ if [[ "$default_key_count" -gt 0 ]]; then
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "Provision SSH keys for root:" 14 72 4 \
+ "found" "Select from detected keys (${default_key_count})" \
+ "manual" "Paste a single public key" \
+ "folder" "Scan another folder (path or glob)" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ else
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "No host keys detected; choose manual/none:" 12 72 2 \
+ "manual" "Paste a single public key" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ fi
+
+ case "$ssh_key_mode" in
+ found)
+ local selection
+ selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \
+ --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ ;;
+ manual)
+ SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
+ [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
+ ;;
+ folder)
+ local glob_path
+ glob_path=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
+ if [[ -n "$glob_path" ]]; then
+ shopt -s nullglob
+ read -r -a _scan_files <<<"$glob_path"
+ shopt -u nullglob
+ if [[ "${#_scan_files[@]}" -gt 0 ]]; then
+ ssh_build_choices_from_files "${_scan_files[@]}"
+ if [[ "$COUNT" -gt 0 ]]; then
+ local folder_selection
+ folder_selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \
+ --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $folder_selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60
+ fi
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60
+ fi
+ fi
+ ;;
+ none)
+ :
+ ;;
+ esac
+
+ if [[ -s "$SSH_KEYS_FILE" ]]; then
+ sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
+ printf '\n' >>"$SSH_KEYS_FILE"
+ fi
+
+ if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
+ SSH="yes"
+ else
+ SSH="no"
+ fi
+ else
+ SSH="no"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# start()
+#
+# - Entry point of script
+# - On Proxmox host: calls install_script
+# - In silent mode: runs update_script
+# - Otherwise: shows update/setting menu
+# ------------------------------------------------------------------------------
+start() {
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func)
+ if command -v pveversion >/dev/null 2>&1; then
+ install_script || return 0
+ return 0
+ elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
+ VERBOSE="no"
+ set_std_mode
+ update_script
+ else
+ CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
+ "Support/Update functions for ${APP} LXC. Choose an option:" \
+ 12 60 3 \
+ "1" "YES (Silent Mode)" \
+ "2" "YES (Verbose Mode)" \
+ "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
+
+ case "$CHOICE" in
+ 1)
+ VERBOSE="no"
+ set_std_mode
+ ;;
+ 2)
+ VERBOSE="yes"
+ set_std_mode
+ ;;
+ 3)
+ clear
+ exit_script
+ exit
+ ;;
+ esac
+ update_script
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# build_container()
+#
+# - Creates and configures the LXC container
+# - Builds network string and applies features (FUSE, TUN, VAAPI passthrough)
+# - Starts container and waits for network connectivity
+# - Installs base packages, SSH keys, and runs -install.sh
+# ------------------------------------------------------------------------------
+build_container() {
+ # if [ "$VERBOSE" == "yes" ]; then set -x; fi
+
+ NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
+
+ # MAC
+ if [[ -n "$MAC" ]]; then
+ case "$MAC" in
+ ,hwaddr=*) NET_STRING+="$MAC" ;;
+ *) NET_STRING+=",hwaddr=$MAC" ;;
+ esac
+ fi
+
+ # IP (immer zwingend, Standard dhcp)
+ NET_STRING+=",ip=${NET:-dhcp}"
+
+ # Gateway
+ if [[ -n "$GATE" ]]; then
+ case "$GATE" in
+ ,gw=*) NET_STRING+="$GATE" ;;
+ *) NET_STRING+=",gw=$GATE" ;;
+ esac
+ fi
+
+ # VLAN
+ if [[ -n "$VLAN" ]]; then
+ case "$VLAN" in
+ ,tag=*) NET_STRING+="$VLAN" ;;
+ *) NET_STRING+=",tag=$VLAN" ;;
+ esac
+ fi
+
+ # MTU
+ if [[ -n "$MTU" ]]; then
+ case "$MTU" in
+ ,mtu=*) NET_STRING+="$MTU" ;;
+ *) NET_STRING+=",mtu=$MTU" ;;
+ esac
+ fi
+
+ # IPv6 Handling
+ case "$IPV6_METHOD" in
+ auto) NET_STRING="$NET_STRING,ip6=auto" ;;
+ dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
+ static)
+ NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
+ [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
+ ;;
+ none) ;;
+ esac
+
+ if [ "$CT_TYPE" == "1" ]; then
+ FEATURES="keyctl=1,nesting=1"
+ else
+ FEATURES="nesting=1"
+ fi
+
+ if [ "$ENABLE_FUSE" == "yes" ]; then
+ FEATURES="$FEATURES,fuse=1"
+ fi
+
+ TEMP_DIR=$(mktemp -d)
+ pushd "$TEMP_DIR" >/dev/null
+ if [ "$var_os" == "alpine" ]; then
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)"
+ else
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)"
+ fi
+ export DIAGNOSTICS="$DIAGNOSTICS"
+ export RANDOM_UUID="$RANDOM_UUID"
+ export CACHER="$APT_CACHER"
+ export CACHER_IP="$APT_CACHER_IP"
+ export tz="$timezone"
+ export APPLICATION="$APP"
+ export app="$NSAPP"
+ export PASSWORD="$PW"
+ export VERBOSE="$VERBOSE"
+ export SSH_ROOT="${SSH}"
+ export SSH_AUTHORIZED_KEY
+ export CTID="$CT_ID"
+ export CTTYPE="$CT_TYPE"
+ export ENABLE_FUSE="$ENABLE_FUSE"
+ export ENABLE_TUN="$ENABLE_TUN"
+ export PCT_OSTYPE="$var_os"
+ export PCT_OSVERSION="$var_version"
+ export PCT_DISK_SIZE="$DISK_SIZE"
+ export PCT_OPTIONS="
+ -features $FEATURES
+ -hostname $HN
+ -tags $TAGS
+ $SD
+ $NS
+ $NET_STRING
+ -onboot 1
+ -cores $CORE_COUNT
+ -memory $RAM_SIZE
+ -unprivileged $CT_TYPE
+ $PW
+"
+ export TEMPLATE_STORAGE="${var_template_storage:-}"
+ export CONTAINER_STORAGE="${var_container_storage:-}"
+ create_lxc_container || exit $?
+
+ LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
+
+ # ============================================================================
+ # GPU/USB PASSTHROUGH CONFIGURATION
+ # ============================================================================
+
+ # List of applications that benefit from GPU acceleration
+ GPU_APPS=(
+ "immich" "channels" "emby" "ersatztv" "frigate"
+ "jellyfin" "plex" "scrypted" "tdarr" "unmanic"
+ "ollama" "fileflows" "open-webui" "tunarr" "debian"
+ "handbrake" "sunshine" "moonlight" "kodi" "stremio"
+ "viseron"
+ )
+
+ # Check if app needs GPU
+ is_gpu_app() {
+ local app="${1,,}"
+ for gpu_app in "${GPU_APPS[@]}"; do
+ [[ "$app" == "${gpu_app,,}" ]] && return 0
+ done
+ return 1
+ }
+
+ # Detect all available GPU devices
+ detect_gpu_devices() {
+ INTEL_DEVICES=()
+ AMD_DEVICES=()
+ NVIDIA_DEVICES=()
+
+ # Store PCI info to avoid multiple calls
+ local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D")
+
+ # Check for Intel GPU - look for Intel vendor ID [8086]
+ if echo "$pci_vga_info" | grep -q "\[8086:"; then
+ msg_info "Detected Intel GPU"
+ if [[ -d /dev/dri ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && INTEL_DEVICES+=("$d")
+ done
+ fi
+ fi
+
+ # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
+ if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then
+ msg_info "Detected AMD GPU"
+ if [[ -d /dev/dri ]]; then
+ # Only add if not already claimed by Intel
+ if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && AMD_DEVICES+=("$d")
+ done
+ fi
+ fi
+ fi
+
+ # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
+ if echo "$pci_vga_info" | grep -q "\[10de:"; then
+ msg_info "Detected NVIDIA GPU"
+ if ! check_nvidia_host_setup; then
+ msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough."
+ msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually."
+ return 0
+ fi
+
+ for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do
+ [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
+ done
+
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found"
+ msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver"
+ else
+ if [[ "$CT_TYPE" == "0" ]]; then
+ cat <>"$LXC_CONFIG"
+ # NVIDIA GPU Passthrough (privileged)
+ lxc.cgroup2.devices.allow: c 195:* rwm
+ lxc.cgroup2.devices.allow: c 243:* rwm
+ lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file
+EOF
+
+ if [[ -e /dev/dri/renderD128 ]]; then
+ echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+
+ export GPU_TYPE="NVIDIA"
+ export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
+ msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})"
+ else
+ msg_warn "NVIDIA passthrough only supported for privileged containers"
+ return 0
+ fi
+ fi
+ fi
+
+ # Debug output
+ msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
+ msg_debug "AMD devices: ${AMD_DEVICES[*]}"
+ msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
+ }
+
+ # Configure USB passthrough for privileged containers
+ configure_usb_passthrough() {
+ if [[ "$CT_TYPE" != "0" ]]; then
+ return 0
+ fi
+
+ msg_info "Configuring automatic USB passthrough (privileged container)"
+ cat <>"$LXC_CONFIG"
+# Automatic USB passthrough (privileged container)
+lxc.cgroup2.devices.allow: a
+lxc.cap.drop:
+lxc.cgroup2.devices.allow: c 188:* rwm
+lxc.cgroup2.devices.allow: c 189:* rwm
+lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
+lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
+EOF
+ msg_ok "USB passthrough configured"
+ }
+
+ # Configure GPU passthrough
+ configure_gpu_passthrough() {
+ # Skip if not a GPU app and not privileged
+ if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then
+ return 0
+ fi
+
+ detect_gpu_devices
+
+ # Count available GPU types
+ local gpu_count=0
+ local available_gpus=()
+
+ if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("INTEL")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("AMD")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("NVIDIA")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ $gpu_count -eq 0 ]]; then
+ msg_info "No GPU devices found for passthrough"
+ return 0
+ fi
+
+ local selected_gpu=""
+
+ if [[ $gpu_count -eq 1 ]]; then
+ # Automatic selection for single GPU
+ selected_gpu="${available_gpus[0]}"
+ msg_info "Automatically configuring ${selected_gpu} GPU passthrough"
+ else
+ # Multiple GPUs - ask user
+ echo -e "\n${INFO} Multiple GPU types detected:"
+ for gpu in "${available_gpus[@]}"; do
+ echo " - $gpu"
+ done
+ read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
+ selected_gpu="${selected_gpu^^}"
+
+ # Validate selection
+ local valid=0
+ for gpu in "${available_gpus[@]}"; do
+ [[ "$selected_gpu" == "$gpu" ]] && valid=1
+ done
+
+ if [[ $valid -eq 0 ]]; then
+ msg_warn "Invalid selection. Skipping GPU passthrough."
+ return 0
+ fi
+ fi
+
+ # Apply passthrough configuration based on selection
+ local dev_idx=0
+
+ case "$selected_gpu" in
+ INTEL | AMD)
+ local devices=()
+ [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
+ [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
+
+ # For Proxmox WebUI visibility, add as dev0, dev1 etc.
+ for dev in "${devices[@]}"; do
+ if [[ "$CT_TYPE" == "0" ]]; then
+ # Privileged container - use dev entries for WebUI visibility
+ # Use initial GID 104 (render) for renderD*, 44 (video) for card*
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+
+ # Also add cgroup allows for privileged containers
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ else
+ # Unprivileged container
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+ fi
+ done
+
+ export GPU_TYPE="$selected_gpu"
+ msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)"
+ ;;
+
+ NVIDIA)
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver"
+ return 1
+ fi
+
+ for dev in "${NVIDIA_DEVICES[@]}"; do
+ # NVIDIA devices typically need different handling
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ dev_idx=$((dev_idx + 1))
+
+ if [[ "$CT_TYPE" == "0" ]]; then
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ fi
+ done
+
+ export GPU_TYPE="NVIDIA"
+ msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)"
+ ;;
+ esac
+ }
+
+ # Additional device passthrough
+ configure_additional_devices() {
+ # TUN device passthrough
+ if [ "$ENABLE_TUN" == "yes" ]; then
+ cat <>"$LXC_CONFIG"
+lxc.cgroup2.devices.allow: c 10:200 rwm
+lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
+EOF
+ fi
+
+ # Coral TPU passthrough
+ if [[ -e /dev/apex_0 ]]; then
+ msg_info "Detected Coral TPU - configuring passthrough"
+ echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+ }
+
+ # Execute pre-start configurations
+ configure_usb_passthrough
+ configure_gpu_passthrough
+ configure_additional_devices
+
+ # ============================================================================
+ # START CONTAINER AND INSTALL USERLAND
+ # ============================================================================
+
+ msg_info "Starting LXC Container"
+ pct start "$CTID"
+
+ # Wait for container to be running
+ for i in {1..10}; do
+ if pct status "$CTID" | grep -q "status: running"; then
+ msg_ok "Started LXC Container"
+ break
+ fi
+ sleep 1
+ if [ "$i" -eq 10 ]; then
+ msg_error "LXC Container did not reach running state"
+ exit 1
+ fi
+ done
+
+ # Wait for network (skip for Alpine initially)
+ if [ "$var_os" != "alpine" ]; then
+ msg_info "Waiting for network in LXC container"
+
+ # Wait for IP
+ for i in {1..20}; do
+ ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+ [ -n "$ip_in_lxc" ] && break
+ sleep 1
+ done
+
+ if [ -z "$ip_in_lxc" ]; then
+ msg_error "No IP assigned to CT $CTID after 20s"
+ exit 1
+ fi
+
+ # Try to reach gateway
+ gw_ok=0
+ for i in {1..10}; do
+ if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then
+ gw_ok=1
+ break
+ fi
+ sleep 1
+ done
+
+ if [ "$gw_ok" -eq 1 ]; then
+ msg_ok "Network in LXC is reachable (IP $ip_in_lxc)"
+ else
+ msg_warn "Network reachable but gateway check failed"
+ fi
+ fi
+ # Function to get correct GID inside container
+ get_container_gid() {
+ local group="$1"
+ local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
+ echo "${gid:-44}" # Default to 44 if not found
+ }
+
+ fix_gpu_gids
+
+ # Continue with standard container setup
+ msg_info "Customizing LXC Container"
+
+ # # Install GPU userland if configured
+ # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
+ # install_gpu_userland "VAAPI"
+ # fi
+
+ # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
+ # install_gpu_userland "NVIDIA"
+ # fi
+
+ # Continue with standard container setup
+ if [ "$var_os" == "alpine" ]; then
+ sleep 3
+ pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
+EOF'
+ pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
+ else
+ sleep 3
+ pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
+ pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
+ echo LANG=\$locale_line >/etc/default/locale && \
+ locale-gen >/dev/null && \
+ export LANG=\$locale_line"
+
+ if [[ -z "${tz:-}" ]]; then
+ tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
+ fi
+
+ if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
+ pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
+ else
+ msg_warn "Skipping timezone setup – zone '$tz' not found in container"
+ fi
+
+ pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || {
+ msg_error "apt-get base packages installation failed"
+ exit 1
+ }
+ fi
+
+ msg_ok "Customized LXC Container"
+
+ # Verify GPU access if enabled
+ if [[ "${ENABLE_VAAPI:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "vainfo >/dev/null 2>&1" &&
+ msg_ok "VAAPI verified working" ||
+ msg_warn "VAAPI verification failed - may need additional configuration"
+ fi
+
+ if [[ "${ENABLE_NVIDIA:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "nvidia-smi >/dev/null 2>&1" &&
+ msg_ok "NVIDIA verified working" ||
+ msg_warn "NVIDIA verification failed - may need additional configuration"
+ fi
+
+ # Install SSH keys
+ install_ssh_keys_into_ct
+
+ # Run application installer
+ if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then
+ exit $?
+ fi
+}
+
+destroy_lxc() {
+ if [[ -z "$CT_ID" ]]; then
+ msg_error "No CT_ID found. Nothing to remove."
+ return 1
+ fi
+
+ # Abbruch bei Ctrl-C / Ctrl-D / ESC
+ trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
+
+ local prompt
+ if ! read -rp "Remove this Container? " prompt; then
+ # read gibt != 0 zurück bei Ctrl-D/ESC
+ msg_error "Aborted input (Ctrl-D/ESC)"
+ return 130
+ fi
+
+ case "${prompt,,}" in
+ y | yes)
+ if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
+ msg_ok "Removed Container $CT_ID"
+ else
+ msg_error "Failed to remove Container $CT_ID"
+ return 1
+ fi
+ ;;
+ "" | n | no)
+ msg_info "Container was not removed."
+ ;;
+ *)
+ msg_warn "Invalid response. Container was not removed."
+ ;;
+ esac
+}
+
+# ------------------------------------------------------------------------------
+# Storage discovery / selection helpers
+# ------------------------------------------------------------------------------
+# ===== Storage discovery / selection helpers (ported from create_lxc.sh) =====
+resolve_storage_preselect() {
+ local class="$1" preselect="$2" required_content=""
+ case "$class" in
+ template) required_content="vztmpl" ;;
+ container) required_content="rootdir" ;;
+ *) return 1 ;;
+ 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
+ fi
+
+ local line total used free
+ line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
+ if [[ -z "$line" ]]; then
+ STORAGE_INFO="n/a"
+ else
+ total="$(awk '{print $4}' <<<"$line")"
+ used="$(awk '{print $5}' <<<"$line")"
+ free="$(awk '{print $6}' <<<"$line")"
+ local total_h used_h free_h
+ if command -v numfmt >/dev/null 2>&1; then
+ total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
+ used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
+ free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
+ STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
+ else
+ STORAGE_INFO="Free: ${free} Used: ${used}"
+ fi
+ fi
+ STORAGE_RESULT="$preselect"
+ return 0
+}
+
+fix_gpu_gids() {
+ if [[ -z "${GPU_TYPE:-}" ]]; then
+ return 0
+ fi
+
+ msg_info "Detecting and setting correct GPU group IDs"
+
+ # Ermittle die tatsächlichen GIDs aus dem Container
+ local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+
+ # Fallbacks wenn Gruppen nicht existieren
+ if [[ -z "$video_gid" ]]; then
+ # Versuche die video Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true"
+ video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback
+ fi
+
+ if [[ -z "$render_gid" ]]; then
+ # Versuche die render Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true"
+ render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+ [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback
+ fi
+
+ msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}"
+
+ # Prüfe ob die GIDs von den Defaults abweichen
+ local need_update=0
+ if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then
+ need_update=1
+ fi
+
+ if [[ $need_update -eq 1 ]]; then
+ msg_info "Updating device GIDs in container config"
+
+ # Stoppe Container für Config-Update
+ pct stop "$CTID" >/dev/null 2>&1
+
+ # Update die dev Einträge mit korrekten GIDs
+ # Backup der Config
+ cp "$LXC_CONFIG" "${LXC_CONFIG}.bak"
+
+ # Parse und update jeden dev Eintrag
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^dev[0-9]+: ]]; then
+ # Extract device path
+ local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/')
+ local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/')
+
+ if [[ "$device_path" =~ renderD ]]; then
+ # RenderD device - use render GID
+ echo "${dev_num}: ${device_path},gid=${render_gid}"
+ elif [[ "$device_path" =~ card ]]; then
+ # Card device - use video GID
+ echo "${dev_num}: ${device_path},gid=${video_gid}"
+ else
+ # Keep original line
+ echo "$line"
+ fi
+ else
+ # Keep non-dev lines
+ echo "$line"
+ fi
+ done <"$LXC_CONFIG" >"${LXC_CONFIG}.new"
+
+ mv "${LXC_CONFIG}.new" "$LXC_CONFIG"
+
+ # Starte Container wieder
+ pct start "$CTID" >/dev/null 2>&1
+ sleep 3
+
+ msg_ok "Device GIDs updated successfully"
+ else
+ msg_ok "Device GIDs are already correct"
+ fi
+ if [[ "$CT_TYPE" == "0" ]]; then
+ pct exec "$CTID" -- bash -c "
+ if [ -d /dev/dri ]; then
+ for dev in /dev/dri/*; do
+ if [ -e \"\$dev\" ]; then
+ if [[ \"\$dev\" =~ renderD ]]; then
+ chgrp ${render_gid} \"\$dev\" 2>/dev/null || true
+ else
+ chgrp ${video_gid} \"\$dev\" 2>/dev/null || true
+ fi
+ chmod 660 \"\$dev\" 2>/dev/null || true
+ fi
+ done
+ fi
+ " >/dev/null 2>&1
+ fi
+}
+
+# NVIDIA-spezific check on host
+check_nvidia_host_setup() {
+ if ! command -v nvidia-smi >/dev/null 2>&1; then
+ msg_warn "NVIDIA GPU detected but nvidia-smi not found on host"
+ msg_warn "Please install NVIDIA drivers on host first."
+ #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run"
+ #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms"
+ #echo " 3. Verify: nvidia-smi"
+ return 1
+ fi
+
+ # check if nvidia-smi works
+ if ! nvidia-smi >/dev/null 2>&1; then
+ msg_warn "nvidia-smi installed but not working. Driver issue?"
+ return 1
+ fi
+
+ return 0
+}
+
+check_storage_support() {
+ local CONTENT="$1" VALID=0
+ while IFS= read -r line; do
+ local STORAGE_NAME
+ STORAGE_NAME=$(awk '{print $1}' <<<"$line")
+ [[ -n "$STORAGE_NAME" ]] && VALID=1
+ done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
+ [[ $VALID -eq 1 ]]
+}
+
+select_storage() {
+ local CLASS=$1 CONTENT CONTENT_LABEL
+ case $CLASS in
+ container)
+ CONTENT='rootdir'
+ CONTENT_LABEL='Container'
+ ;;
+ template)
+ CONTENT='vztmpl'
+ CONTENT_LABEL='Container template'
+ ;;
+ iso)
+ CONTENT='iso'
+ CONTENT_LABEL='ISO image'
+ ;;
+ images)
+ CONTENT='images'
+ CONTENT_LABEL='VM Disk image'
+ ;;
+ backup)
+ CONTENT='backup'
+ CONTENT_LABEL='Backup'
+ ;;
+ snippets)
+ CONTENT='snippets'
+ CONTENT_LABEL='Snippets'
+ ;;
+ *)
+ msg_error "Invalid storage class '$CLASS'"
+ return 1
+ ;;
+ esac
+
+ declare -A STORAGE_MAP
+ local -a MENU=()
+ local COL_WIDTH=0
+
+ while read -r TAG TYPE _ TOTAL USED FREE _; do
+ [[ -n "$TAG" && -n "$TYPE" ]] || continue
+ local DISPLAY="${TAG} (${TYPE})"
+ local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
+ local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
+ local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
+ STORAGE_MAP["$DISPLAY"]="$TAG"
+ MENU+=("$DISPLAY" "$INFO" "OFF")
+ ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
+ done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
+
+ if [[ ${#MENU[@]} -eq 0 ]]; then
+ msg_error "No storage found for content type '$CONTENT'."
+ return 2
+ fi
+
+ if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
+ STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
+ STORAGE_INFO="${MENU[1]}"
+ return 0
+ fi
+
+ local WIDTH=$((COL_WIDTH + 42))
+ while true; do
+ local DISPLAY_SELECTED
+ DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Storage Pools" \
+ --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
+ 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }
+
+ DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
+ if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
+ whiptail --msgbox "No valid storage selected. Please try again." 8 58
+ continue
+ fi
+ STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
+ for ((i = 0; i < ${#MENU[@]}; i += 3)); do
+ if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
+ STORAGE_INFO="${MENU[$i + 1]}"
+ break
+ fi
+ done
+ return 0
+ done
+}
+
+create_lxc_container() {
+ # ------------------------------------------------------------------------------
+ # Optional verbose mode (debug tracing)
+ # ------------------------------------------------------------------------------
+ if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
+
+ # ------------------------------------------------------------------------------
+ # Helpers (dynamic versioning / template parsing)
+ # ------------------------------------------------------------------------------
+ pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
+ pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
+
+ ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
+ ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
+ ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
+
+ # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
+ parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
+
+ # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
+ # Returns:
+ # 0 = no upgrade needed
+ # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
+ # 2 = user declined
+ # 3 = upgrade attempted but failed OR retry failed
+ offer_lxc_stack_upgrade_and_maybe_retry() {
+ local do_retry="${1:-no}" # yes|no
+ local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
+
+ _pvec_i="$(pkg_ver pve-container)"
+ _lxcp_i="$(pkg_ver lxc-pve)"
+ _pvec_c="$(pkg_cand pve-container)"
+ _lxcp_c="$(pkg_cand lxc-pve)"
+
+ if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
+ ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
+ fi
+ if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
+ ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
+ fi
+ if [[ $need -eq 0 ]]; then
+ msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
+ return 0
+ fi
+
+ echo
+ echo "An update for the Proxmox LXC stack is available:"
+ echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
+ echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
+ echo
+ read -rp "Do you want to upgrade now? [y/N] " _ans
+ case "${_ans,,}" in
+ y | yes)
+ msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
+ if apt-get update -qq >/dev/null && apt-get install -y --only-upgrade pve-container lxc-pve >/dev/null; then
+ msg_ok "LXC stack upgraded."
+ if [[ "$do_retry" == "yes" ]]; then
+ msg_info "Retrying container creation after upgrade"
+ if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container created successfully after upgrade."
+ return 0
+ else
+ msg_error "pct create still failed after upgrade. See $LOGFILE"
+ return 3
+ fi
+ fi
+ return 1
+ else
+ msg_error "Upgrade failed. Please check APT output."
+ return 3
+ fi
+ ;;
+ *) return 2 ;;
+ esac
+ }
+
+ # ------------------------------------------------------------------------------
+ # Required input variables
+ # ------------------------------------------------------------------------------
+ [[ "${CTID:-}" ]] || {
+ msg_error "You need to set 'CTID' variable."
+ exit 203
+ }
+ [[ "${PCT_OSTYPE:-}" ]] || {
+ msg_error "You need to set 'PCT_OSTYPE' variable."
+ exit 204
+ }
+
+ msg_debug "CTID=$CTID"
+ msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
+ msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
+
+ # ID checks
+ [[ "$CTID" -ge 100 ]] || {
+ msg_error "ID cannot be less than 100."
+ exit 205
+ }
+ if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
+ echo -e "ID '$CTID' is already in use."
+ unset CTID
+ msg_error "Cannot use ID that is already in use."
+ exit 206
+ fi
+
+ # Storage capability check
+ check_storage_support "rootdir" || {
+ msg_error "No valid storage found for 'rootdir' [Container]"
+ exit 1
+ }
+ check_storage_support "vztmpl" || {
+ msg_error "No valid storage found for 'vztmpl' [Template]"
+ exit 1
+ }
+
+ # Template storage selection
+ if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ else
+ while true; do
+ if [[ -z "${var_template_storage:-}" ]]; then
+ if select_storage template; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ break
+ fi
+ fi
+ done
+ fi
+
+ # Container storage selection
+ if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ else
+ if [[ -z "${var_container_storage:-}" ]]; then
+ if select_storage container; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ fi
+ fi
+ fi
+
+ # Validate content types
+ msg_info "Validating content types of storage '$CONTAINER_STORAGE'"
+ STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT"
+ grep -qw "rootdir" <<<"$STORAGE_CONTENT" || {
+ msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC."
+ exit 217
+ }
+ $STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'"
+
+ msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'"
+ TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT"
+ if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then
+ msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail."
+ else
+ $STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'"
+ fi
+
+ # Free space check
+ STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
+ REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
+ [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || {
+ msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
+ exit 214
+ }
+
+ # Cluster quorum (if cluster)
+ if [[ -f /etc/pve/corosync.conf ]]; then
+ msg_info "Checking cluster quorum"
+ if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
+ msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
+ exit 210
+ fi
+ msg_ok "Cluster is quorate"
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Template discovery & validation
+ # ------------------------------------------------------------------------------
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ case "$PCT_OSTYPE" in
+ debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
+ alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
+ *) TEMPLATE_PATTERN="" ;;
+ esac
+
+ msg_info "Searching for template '$TEMPLATE_SEARCH'"
+
+ # Build regex patterns outside awk/grep for clarity
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}"
+
+ #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'"
+ #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'"
+ #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+
+ pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)."
+
+ #echo "[DEBUG] pveam available output (first 5 lines with .tar files):"
+ #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /'
+
+ set +u
+ mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
+ #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found"
+ set -u
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #echo "[DEBUG] Online templates:"
+ for tmpl in "${ONLINE_TEMPLATES[@]}"; do
+ echo " - $tmpl"
+ done
+ fi
+
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'"
+ #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates"
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #msg_debug "First 3 online templates:"
+ count=0
+ for idx in "${!ONLINE_TEMPLATES[@]}"; do
+ #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}"
+ ((count++))
+ [[ $count -ge 3 ]] && break
+ done
+ fi
+ #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ # If still no template, try to find alternatives
+ if [[ -z "$TEMPLATE" ]]; then
+ echo ""
+ echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."
+
+ # Get all available versions for this OS type
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" |
+ sort -u -V 2>/dev/null
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo ""
+ echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ #echo "[DEBUG] Retrying with version: $PCT_OSVERSION"
+
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="online"
+ #echo "[DEBUG] Found alternative: $TEMPLATE"
+ else
+ msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ exit 225
+ fi
+ else
+ msg_info "Installation cancelled"
+ exit 0
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available at all"
+ exit 225
+ fi
+ fi
+
+ #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+ #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"
+
+ # Get available versions
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
+ grep -E '^[0-9]+\.[0-9]+$' |
+ sort -u -V 2>/dev/null || sort -u
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo -e "\n${BL}Available versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ export PCT_OSVERSION="$var_version"
+ msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"
+
+ # Retry template search with new version
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ msg_error "Template still not found after version change"
+ exit 220
+ }
+ else
+ msg_info "Installation cancelled"
+ exit 1
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available"
+ exit 220
+ fi
+ fi
+ }
+
+ # Validate that we found a template
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ msg_info "Please check:"
+ msg_info " - Is pveam catalog available? (run: pveam available -section system)"
+ msg_info " - Does the template exist for your OS version?"
+ exit 225
+ fi
+
+ msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
+ msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
+
+ NEED_DOWNLOAD=0
+ if [[ ! -f "$TEMPLATE_PATH" ]]; then
+ msg_info "Template not present locally – will download."
+ NEED_DOWNLOAD=1
+ elif [[ ! -r "$TEMPLATE_PATH" ]]; then
+ msg_error "Template file exists but is not readable – check permissions."
+ exit 221
+ elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template file too small (<1MB) – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template looks too small, but no online version exists. Keeping local file."
+ fi
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
+ fi
+ else
+ $STD msg_ok "Template $TEMPLATE is present and valid."
+ fi
+
+ if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
+ if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
+ TEMPLATE="$ONLINE_TEMPLATE"
+ NEED_DOWNLOAD=1
+ else
+ msg_info "Continuing with local template $TEMPLATE"
+ fi
+ fi
+
+ if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
+ [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
+ for attempt in {1..3}; do
+ msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
+ if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
+ msg_ok "Template download successful."
+ break
+ fi
+ if [[ $attempt -eq 3 ]]; then
+ msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
+ exit 222
+ fi
+ sleep $((attempt * 5))
+ done
+ fi
+
+ if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
+ msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
+ exit 223
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
+ # ------------------------------------------------------------------------------
+ if [[ "$PCT_OSTYPE" == "debian" ]]; then
+ OSVER="$(parse_template_osver "$TEMPLATE")"
+ if [[ -n "$OSVER" ]]; then
+ # Proactive, aber ohne Abbruch – nur Angebot
+ offer_lxc_stack_upgrade_and_maybe_retry "no" || true
+ fi
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Create LXC Container
+ # ------------------------------------------------------------------------------
+ msg_info "Creating LXC container"
+
+ # Ensure subuid/subgid entries exist
+ grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
+ grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
+
+ # Assemble pct options
+ PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
+ [[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
+
+ # Lock by template file (avoid concurrent downloads/creates)
+ lockfile="/tmp/template.${TEMPLATE}.lock"
+ exec 9>"$lockfile" || {
+ msg_error "Failed to create lock file '$lockfile'."
+ exit 200
+ }
+ flock -w 60 9 || {
+ msg_error "Timeout while waiting for template lock."
+ exit 211
+ }
+
+ LOGFILE="/tmp/pct_create_${CTID}.log"
+ msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}"
+ msg_debug "Logfile: $LOGFILE"
+
+ # First attempt
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then
+ msg_error "Container creation failed on ${TEMPLATE_STORAGE}. Checking template..."
+
+ # Validate template file
+ if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ msg_warn "Template file too small or missing – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
+ fi
+ fi
+
+ # Retry after repair
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ # Fallback to local storage
+ if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
+ msg_warn "Retrying container creation with fallback to local storage..."
+ LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
+ msg_info "Downloading template to local..."
+ pveam download local "$TEMPLATE" >/dev/null 2>&1
+ fi
+ if pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container successfully created using local fallback."
+ else
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed even with local fallback. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ else
+ msg_error "Container creation failed on local storage. See $LOGFILE"
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ fi
+ fi
+
+ # Verify container exists
+ pct list | awk '{print $1}' | grep -qx "$CTID" || {
+ msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE"
+ exit 215
+ }
+
+ # Verify config rootfs
+ grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
+ msg_error "RootFS entry missing in container config. See $LOGFILE"
+ exit 216
+ }
+
+ msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
+}
+
+# ------------------------------------------------------------------------------
+# description()
+#
+# - Sets container description with HTML content (logo, links, badges)
+# - Restarts ping-instances.service if present
+# - Posts status "done" to API
+# ------------------------------------------------------------------------------
+description() {
+ IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+
+ # Generate LXC Description
+ DESCRIPTION=$(
+ cat <
+
+
+
+
+ ${APP} LXC
+
+
+
+
+
+
+
+
+
+ GitHub
+
+
+
+ Discussions
+
+
+
+ Issues
+
+
+EOF
+ )
+ pct set "$CTID" -description "$DESCRIPTION"
+
+ if [[ -f /etc/systemd/system/ping-instances.service ]]; then
+ systemctl start ping-instances.service
+ fi
+
+ post_update_to_api "done" "none"
+}
+
+# ------------------------------------------------------------------------------
+# api_exit_script()
+#
+# - Exit trap handler
+# - Reports exit codes to API with detailed reason
+# - Handles known codes (100–209) and maps them to errors
+# ------------------------------------------------------------------------------
+api_exit_script() {
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then
+ case $exit_code in
+ 100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;;
+ 101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;;
+ 200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;;
+ 201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;;
+ 202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;;
+ 203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;;
+ 204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;;
+ 205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;;
+ 206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;;
+ 207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;;
+ 208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;;
+ 209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;;
+ *) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;;
+ esac
+ fi
+}
+
+if command -v pveversion >/dev/null 2>&1; then
+ trap 'api_exit_script' EXIT
+fi
+trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
+trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
+trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
diff --git a/misc/deferred/build.func.backup-20251029-124334 b/misc/deferred/build.func.backup-20251029-124334
new file mode 100644
index 000000000..d452f4637
--- /dev/null
+++ b/misc/deferred/build.func.backup-20251029-124334
@@ -0,0 +1,3517 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# Revision: 1
+
+# ==============================================================================
+# SECTION 1: CORE INITIALIZATION & VARIABLES
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# variables()
+#
+# - Normalize application name (NSAPP = lowercase, no spaces)
+# - Build installer filename (var_install)
+# - Define regex for integer validation
+# - Fetch hostname of Proxmox node
+# - Set default values for diagnostics/method
+# - Generate random UUID for tracking
+# - Get Proxmox VE version and kernel version
+# ------------------------------------------------------------------------------
+variables() {
+ NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
+ var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
+ INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
+ PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
+ DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
+ METHOD="default" # sets the METHOD variable to "default", used for the API call.
+ RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
+ CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
+ #CT_TYPE=${var_unprivileged:-$CT_TYPE}
+
+ # Get Proxmox VE version and kernel version
+ if command -v pveversion >/dev/null 2>&1; then
+ PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1)
+ else
+ PVEVERSION="N/A"
+ fi
+ KERNEL_VERSION=$(uname -r)
+}
+
+# -----------------------------------------------------------------------------
+# Community-Scripts bootstrap loader
+# - Always sources build.func from remote
+# - Updates local core files only if build.func changed
+# - Local cache: /usr/local/community-scripts/core
+# -----------------------------------------------------------------------------
+
+# FUNC_DIR="/usr/local/community-scripts/core"
+# mkdir -p "$FUNC_DIR"
+
+# BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func"
+# BUILD_REV="$FUNC_DIR/build.rev"
+# DEVMODE="${DEVMODE:-no}"
+
+# # --- Step 1: fetch build.func content once, compute hash ---
+# build_content="$(curl -fsSL "$BUILD_URL")" || {
+# echo "❌ Failed to fetch build.func"
+# exit 1
+# }
+
+# newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}')
+# oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "")
+
+# # --- Step 2: if build.func changed, offer update for core files ---
+# if [ "$newhash" != "$oldhash" ]; then
+# echo "⚠️ build.func changed!"
+
+# while true; do
+# read -rp "Refresh local core files? [y/N/diff]: " ans
+# case "$ans" in
+# [Yy]*)
+# echo "$newhash" >"$BUILD_REV"
+
+# update_func_file() {
+# local file="$1"
+# local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file"
+# local local_path="$FUNC_DIR/$file"
+
+# echo "⬇️ Downloading $file ..."
+# curl -fsSL "$url" -o "$local_path" || {
+# echo "❌ Failed to fetch $file"
+# exit 1
+# }
+# echo "✔️ Updated $file"
+# }
+
+# update_func_file core.func
+# update_func_file error_handler.func
+# update_func_file tools.func
+# break
+# ;;
+# [Dd]*)
+# for file in core.func error_handler.func tools.func; do
+# local_path="$FUNC_DIR/$file"
+# url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file"
+# remote_tmp="$(mktemp)"
+
+# curl -fsSL "$url" -o "$remote_tmp" || continue
+
+# if [ -f "$local_path" ]; then
+# echo "🔍 Diff for $file:"
+# diff -u "$local_path" "$remote_tmp" || echo "(no differences)"
+# else
+# echo "📦 New file $file will be installed"
+# fi
+
+# rm -f "$remote_tmp"
+# done
+# ;;
+# *)
+# echo "❌ Skipped updating local core files"
+# break
+# ;;
+# esac
+# done
+# else
+# if [ "$DEVMODE" != "yes" ]; then
+# echo "✔️ build.func unchanged → using existing local core files"
+# fi
+# fi
+
+# if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then
+# return 0 2>/dev/null || exit 0
+# fi
+# _COMMUNITY_SCRIPTS_LOADER=1
+
+# # --- Step 3: always source local versions of the core files ---
+# source "$FUNC_DIR/core.func"
+# source "$FUNC_DIR/error_handler.func"
+# source "$FUNC_DIR/tools.func"
+
+# # --- Step 4: finally, source build.func directly from memory ---
+# # (no tmp file needed)
+# source <(printf "%s" "$build_content")
+
+# ------------------------------------------------------------------------------
+# Load core + error handler functions from community-scripts repo
+#
+# - Prefer curl if available, fallback to wget
+# - Load: core.func, error_handler.func, api.func
+# - Initialize error traps after loading
+# ------------------------------------------------------------------------------
+
+source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func)
+
+if command -v curl >/dev/null 2>&1; then
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via curl"
+elif command -v wget >/dev/null 2>&1; then
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via wget"
+fi
+
+# ------------------------------------------------------------------------------
+# maxkeys_check()
+#
+# - Reads kernel keyring limits (maxkeys, maxbytes)
+# - Checks current usage for LXC user (UID 100000)
+# - Warns if usage is close to limits and suggests sysctl tuning
+# - Exits if thresholds are exceeded
+# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
+# ------------------------------------------------------------------------------
+
+maxkeys_check() {
+ # Read kernel parameters
+ per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
+ per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
+
+ # Exit if kernel parameters are unavailable
+ if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
+ echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
+ exit 1
+ fi
+
+ # Fetch key usage for user ID 100000 (typical for containers)
+ used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
+ used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
+
+ # Calculate thresholds and suggested new limits
+ threshold_keys=$((per_user_maxkeys - 100))
+ threshold_bytes=$((per_user_maxbytes - 1000))
+ new_limit_keys=$((per_user_maxkeys * 2))
+ new_limit_bytes=$((per_user_maxbytes * 2))
+
+ # Check if key or byte usage is near limits
+ failure=0
+ if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+ if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+
+ # Provide next steps if issues are detected
+ if [[ "$failure" -eq 1 ]]; then
+ echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
+ exit 1
+ fi
+
+ echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
+}
+
+# ------------------------------------------------------------------------------
+# get_current_ip()
+#
+# - Returns current container IP depending on OS type
+# - Debian/Ubuntu: uses `hostname -I`
+# - Alpine: parses eth0 via `ip -4 addr`
+# ------------------------------------------------------------------------------
+get_current_ip() {
+ if [ -f /etc/os-release ]; then
+ # Check for Debian/Ubuntu (uses hostname -I)
+ if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
+ CURRENT_IP=$(hostname -I | awk '{print $1}')
+ # Check for Alpine (uses ip command)
+ elif grep -q 'ID=alpine' /etc/os-release; then
+ CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
+ else
+ CURRENT_IP="Unknown"
+ fi
+ fi
+ echo "$CURRENT_IP"
+}
+
+# ------------------------------------------------------------------------------
+# update_motd_ip()
+#
+# - Updates /etc/motd with current container IP
+# - Removes old IP entries to avoid duplicates
+# ------------------------------------------------------------------------------
+update_motd_ip() {
+ MOTD_FILE="/etc/motd"
+
+ if [ -f "$MOTD_FILE" ]; then
+ # Remove existing IP Address lines to prevent duplication
+ sed -i '/IP Address:/d' "$MOTD_FILE"
+
+ IP=$(get_current_ip)
+ # Add the new IP address
+ echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# install_ssh_keys_into_ct()
+#
+# - Installs SSH keys into container root account if SSH is enabled
+# - Uses pct push or direct input to authorized_keys
+# - Falls back to warning if no keys provided
+# ------------------------------------------------------------------------------
+install_ssh_keys_into_ct() {
+ [[ "$SSH" != "yes" ]] && return 0
+
+ if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
+ 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
+ }
+ 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
+ }
+ pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
+ msg_ok "Installed SSH keys into CT ${CTID}"
+ return 0
+ fi
+
+ # Fallback: nichts ausgewählt
+ msg_warn "No SSH keys to install (skipping)."
+ return 0
+}
+
+# ------------------------------------------------------------------------------
+# base_settings()
+#
+# - Defines all base/default variables for container creation
+# - Reads from environment variables (var_*)
+# - Provides fallback defaults for OS type/version
+# ------------------------------------------------------------------------------
+base_settings() {
+ # Default Settings
+ CT_TYPE=${var_unprivileged:-"1"}
+ DISK_SIZE=${var_disk:-"4"}
+ CORE_COUNT=${var_cpu:-"1"}
+ RAM_SIZE=${var_ram:-"1024"}
+ VERBOSE=${var_verbose:-"${1:-no}"}
+ PW=${var_pw:-""}
+ CT_ID=${var_ctid:-$NEXTID}
+ HN=${var_hostname:-$NSAPP}
+ BRG=${var_brg:-"vmbr0"}
+ NET=${var_net:-"dhcp"}
+ IPV6_METHOD=${var_ipv6_method:-"none"}
+ IPV6_STATIC=${var_ipv6_static:-""}
+ GATE=${var_gateway:-""}
+ APT_CACHER=${var_apt_cacher:-""}
+ APT_CACHER_IP=${var_apt_cacher_ip:-""}
+ MTU=${var_mtu:-""}
+ SD=${var_storage:-""}
+ NS=${var_ns:-""}
+ MAC=${var_mac:-""}
+ VLAN=${var_vlan:-""}
+ SSH=${var_ssh:-"no"}
+ SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
+ UDHCPC_FIX=${var_udhcpc_fix:-""}
+ TAGS="community-script,${var_tags:-}"
+ ENABLE_FUSE=${var_fuse:-"${1:-no}"}
+ ENABLE_TUN=${var_tun:-"${1:-no}"}
+
+ # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
+ if [ -z "$var_os" ]; then
+ var_os="debian"
+ fi
+ if [ -z "$var_version" ]; then
+ var_version="12"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# echo_default()
+#
+# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
+# - Uses icons and formatting for readability
+# - Convert CT_TYPE to description
+# ------------------------------------------------------------------------------
+echo_default() {
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ if [ "$VERBOSE" == "yes" ]; then
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
+ fi
+ echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
+ echo -e " "
+}
+
+# ------------------------------------------------------------------------------
+# exit_script()
+#
+# - Called when user cancels an action
+# - Clears screen and exits gracefully
+# ------------------------------------------------------------------------------
+exit_script() {
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
+}
+
+# ------------------------------------------------------------------------------
+# find_host_ssh_keys()
+#
+# - Scans system for available SSH keys
+# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
+# - Returns list of files containing valid SSH public keys
+# - Sets FOUND_HOST_KEY_COUNT to number of keys found
+# ------------------------------------------------------------------------------
+find_host_ssh_keys() {
+ local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
+ local -a files=() cand=()
+ local g="${var_ssh_import_glob:-}"
+ local total=0 f base c
+
+ shopt -s nullglob
+ if [[ -n "$g" ]]; then
+ for pat in $g; do cand+=($pat); done
+ else
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ fi
+ shopt -u nullglob
+
+ for f in "${cand[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # CRLF safe check for host keys
+ c=$(tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ {print}
+ ' | grep -E -c '"$re"' || true)
+
+ if ((c > 0)); then
+ files+=("$f")
+ total=$((total + c))
+ fi
+ done
+
+ # Fallback to /root/.ssh/authorized_keys
+ if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
+ if grep -E -q "$re" /root/.ssh/authorized_keys; then
+ files+=(/root/.ssh/authorized_keys)
+ total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
+ fi
+ fi
+
+ FOUND_HOST_KEY_COUNT="$total"
+ (
+ IFS=:
+ echo "${files[*]}"
+ )
+}
+
+# ------------------------------------------------------------------------------
+# advanced_settings()
+#
+# - Interactive whiptail menu for advanced configuration
+# - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM
+# - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode
+# - Ends with confirmation or re-entry if cancelled
+# ------------------------------------------------------------------------------
+advanced_settings() {
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
+ # Setting Default Tag for Advanced Settings
+ TAGS="community-script;${var_tags:-}"
+ CT_DEFAULT_TYPE="${CT_TYPE}"
+ CT_TYPE=""
+ while [ -z "$CT_TYPE" ]; do
+ if [ "$CT_DEFAULT_TYPE" == "1" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" ON \
+ "0" "Privileged" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os |${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ if [ "$CT_DEFAULT_TYPE" == "0" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" OFF \
+ "0" "Privileged" ON \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
+ echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ done
+
+ while true; do
+ if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
+ # Empty = Autologin
+ if [[ -z "$PW1" ]]; then
+ PW=""
+ PW1="Automatic Login"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
+ break
+ fi
+
+ # Invalid: contains spaces
+ if [[ "$PW1" == *" "* ]]; then
+ whiptail --msgbox "Password cannot contain spaces." 8 58
+ continue
+ fi
+
+ # Invalid: too short
+ if ((${#PW1} < 5)); then
+ whiptail --msgbox "Password must be at least 5 characters." 8 58
+ continue
+ fi
+
+ # Confirm password
+ if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
+ if [[ "$PW1" == "$PW2" ]]; then
+ PW="-password $PW1"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
+ break
+ else
+ whiptail --msgbox "Passwords do not match. Please try again." 8 58
+ fi
+ else
+ exit_script
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_ID" ]; then
+ CT_ID="$NEXTID"
+ fi
+ else
+ exit_script
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
+
+ while true; do
+ if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_NAME" ]; then
+ HN="$NSAPP"
+ else
+ HN=$(echo "${CT_NAME,,}" | tr -d ' ')
+ fi
+ # Hostname validate (RFC 1123)
+ if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ while true; do
+ DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$DISK_SIZE" ]; then
+ DISK_SIZE="$var_disk"
+ fi
+
+ if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ break
+ else
+ whiptail --msgbox "Disk size must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$CORE_COUNT" ]; then
+ CORE_COUNT="$var_cpu"
+ fi
+
+ if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ break
+ else
+ whiptail --msgbox "CPU core count must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$RAM_SIZE" ]; then
+ RAM_SIZE="$var_ram"
+ fi
+
+ if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ break
+ else
+ whiptail --msgbox "RAM size must be a positive integer!" 8 58
+ fi
+ done
+
+ IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
+ BRIDGES=""
+ OLD_IFS=$IFS
+ IFS=$'\n'
+ for iface_filepath in ${IFACE_FILEPATH_LIST}; do
+
+ iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
+ (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
+
+ if [ -f "${iface_indexes_tmpfile}" ]; then
+
+ while read -r pair; do
+ start=$(echo "${pair}" | cut -d':' -f1)
+ end=$(echo "${pair}" | cut -d':' -f2)
+
+ if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
+ iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
+ BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
+ fi
+
+ done <"${iface_indexes_tmpfile}"
+ rm -f "${iface_indexes_tmpfile}"
+ fi
+
+ done
+ IFS=$OLD_IFS
+ BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
+ if [[ -z "$BRIDGES" ]]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ # Build bridge menu with descriptions
+ BRIDGE_MENU_OPTIONS=()
+ while IFS= read -r bridge; do
+ if [[ -n "$bridge" ]]; then
+ # Get description from Proxmox built-in method - find comment for this specific bridge
+ description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//')
+ if [[ -n "$description" ]]; then
+ BRIDGE_MENU_OPTIONS+=("$bridge" "${description}")
+ else
+ BRIDGE_MENU_OPTIONS+=("$bridge" " ")
+ fi
+ fi
+ done <<<"$BRIDGES"
+
+ BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3)
+ if [[ -z "$BRG" ]]; then
+ exit_script
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
+ fi
+
+ # IPv4 methods: dhcp, static, none
+ while true; do
+ IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "IPv4 Address Management" \
+ --menu "Select IPv4 Address Assignment Method:" 12 60 2 \
+ "dhcp" "Automatic (DHCP, recommended)" \
+ "static" "Static (manual entry)" \
+ 3>&1 1>&2 2>&3)
+
+ exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ exit_script
+ fi
+
+ case "$IPV4_METHOD" in
+ dhcp)
+ NET="dhcp"
+ GATE=""
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}"
+ break
+ ;;
+ static)
+ # Static: call and validate CIDR address
+ while true; do
+ NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \
+ --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3)
+ if [ -z "$NET" ]; then
+ whiptail --msgbox "IPv4 address must not be empty." 8 58
+ continue
+ elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}"
+ break
+ else
+ whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58
+ fi
+ done
+
+ # call and validate Gateway
+ while true; do
+ GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \
+ --title "Gateway IP" 3>&1 1>&2 2>&3)
+ if [ -z "$GATE1" ]; then
+ whiptail --msgbox "Gateway IP address cannot be empty." 8 58
+ elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
+ whiptail --msgbox "Invalid Gateway IP address format." 8 58
+ else
+ GATE=",gw=$GATE1"
+ echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
+ break
+ fi
+ done
+ break
+ ;;
+ esac
+ done
+
+ # IPv6 Address Management selection
+ while true; do
+ IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \
+ "Select IPv6 Address Management Type:" 15 58 4 \
+ "auto" "SLAAC/AUTO (recommended, default)" \
+ "dhcp" "DHCPv6" \
+ "static" "Static (manual entry)" \
+ "none" "Disabled" \
+ --default-item "auto" 3>&1 1>&2 2>&3)
+ [ $? -ne 0 ] && exit_script
+
+ case "$IPV6_METHOD" in
+ auto)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}"
+ IPV6_ADDR=""
+ IPV6_GATE=""
+ break
+ ;;
+ dhcp)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}"
+ IPV6_ADDR="dhcp"
+ IPV6_GATE=""
+ break
+ ;;
+ static)
+ # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64)
+ while true; do
+ IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \
+ --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script
+ if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58
+ fi
+ done
+ # Optional: ask for IPv6 gateway for static config
+ while true; do
+ IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3)
+ if [ -z "$IPV6_GATE" ]; then
+ IPV6_GATE=""
+ break
+ elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "Invalid IPv6 gateway format." 8 58
+ fi
+ done
+ break
+ ;;
+ none)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}"
+ IPV6_ADDR="none"
+ IPV6_GATE=""
+ break
+ ;;
+ *)
+ exit_script
+ ;;
+ esac
+ done
+
+ if [ "$var_os" == "alpine" ]; then
+ APT_CACHER=""
+ APT_CACHER_IP=""
+ else
+ if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
+ APT_CACHER="${APT_CACHER_IP:+yes}"
+ echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
+ else
+ exit_script
+ fi
+ fi
+
+ # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then
+ # DISABLEIP6="yes"
+ # else
+ # DISABLEIP6="no"
+ # fi
+ # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}"
+
+ if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
+ if [ -z "$MTU1" ]; then
+ MTU1="Default"
+ MTU=""
+ else
+ MTU=",mtu=$MTU1"
+ fi
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ exit_script
+ fi
+
+ if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
+ if [ -z "$SD" ]; then
+ SX=Host
+ SD=""
+ else
+ SX=$SD
+ SD="-searchdomain=$SD"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
+ else
+ exit_script
+ fi
+
+ if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
+ if [ -z "$NX" ]; then
+ NX=Host
+ NS=""
+ else
+ NS="-nameserver=$NX"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
+ else
+ exit_script
+ fi
+
+ if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then
+ UDHCPC_FIX="yes"
+ else
+ UDHCPC_FIX="no"
+ fi
+ export UDHCPC_FIX
+
+ if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
+ if [ -z "$MAC1" ]; then
+ MAC1="Default"
+ MAC=""
+ else
+ MAC=",hwaddr=$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
+ else
+ exit_script
+ fi
+
+ if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
+ if [ -z "$VLAN1" ]; then
+ VLAN1="Default"
+ VLAN=""
+ else
+ VLAN=",tag=$VLAN1"
+ fi
+ echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
+ else
+ exit_script
+ fi
+
+ if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
+ if [ -n "${ADV_TAGS}" ]; then
+ ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
+ TAGS="${ADV_TAGS}"
+ else
+ TAGS=";"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
+ else
+ exit_script
+ fi
+
+ configure_ssh_settings
+ export SSH_KEYS_FILE
+ echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
+ ENABLE_FUSE="yes"
+ else
+ ENABLE_FUSE="no"
+ fi
+ echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
+ VERBOSE="yes"
+ else
+ VERBOSE="no"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
+ echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
+ else
+ clear
+ header_info
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
+ advanced_settings
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# diagnostics_check()
+#
+# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
+# - Asks user whether to send anonymous diagnostic data
+# - Saves DIAGNOSTICS=yes/no in the config file
+# ------------------------------------------------------------------------------
+diagnostics_check() {
+ if ! [ -d "/usr/local/community-scripts" ]; then
+ mkdir -p /usr/local/community-scripts
+ fi
+
+ if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=yes
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="yes"
+ else
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=no
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="no"
+ fi
+ else
+ DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
+
+ fi
+
+}
+
+# ------------------------------------------------------------------------------
+# default_var_settings
+#
+# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
+# - Loads var_* values from default.vars (safe parser, no source/eval)
+# - Precedence: ENV var_* > default.vars > built-in defaults
+# - Maps var_verbose → VERBOSE
+# - Calls base_settings "$VERBOSE" and echo_default
+# ------------------------------------------------------------------------------
+default_var_settings() {
+ # Allowed var_* keys (alphabetically sorted)
+ local VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+
+ # Snapshot: environment variables (highest precedence)
+ declare -A _HARD_ENV=()
+ local _k
+ for _k in "${VAR_WHITELIST[@]}"; do
+ if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
+ done
+
+ # Find default.vars location
+ local _find_default_vars
+ _find_default_vars() {
+ local f
+ for f in \
+ /usr/local/community-scripts/default.vars \
+ "$HOME/.config/community-scripts/default.vars" \
+ "./default.vars"; do
+ [ -f "$f" ] && {
+ echo "$f"
+ return 0
+ }
+ done
+ return 1
+ }
+ # Allow override of storages via env (for non-interactive use cases)
+ [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
+ [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
+
+ # Create once, with storages already selected, no var_ctid/var_hostname lines
+ local _ensure_default_vars
+ _ensure_default_vars() {
+ _find_default_vars >/dev/null 2>&1 && return 0
+
+ local canonical="/usr/local/community-scripts/default.vars"
+ msg_info "No default.vars found. Creating ${canonical}"
+ mkdir -p /usr/local/community-scripts
+
+ # Pick storages before writing the file (always ask unless only one)
+ # Create a minimal temp file to write into
+ : >"$canonical"
+
+ # Base content (no var_ctid / var_hostname here)
+ cat >"$canonical" <<'EOF'
+# Community-Scripts defaults (var_* only). Lines starting with # are comments.
+# Precedence: ENV var_* > default.vars > built-ins.
+# Keep keys alphabetically sorted.
+
+# Container type
+var_unprivileged=1
+
+# Resources
+var_cpu=1
+var_disk=4
+var_ram=1024
+
+# Network
+var_brg=vmbr0
+var_net=dhcp
+var_ipv6_method=none
+# var_gateway=
+# var_ipv6_static=
+# var_vlan=
+# var_mtu=
+# var_mac=
+# var_ns=
+
+# SSH
+var_ssh=no
+# var_ssh_authorized_key=
+
+# APT cacher (optional)
+# var_apt_cacher=yes
+# var_apt_cacher_ip=192.168.1.10
+
+# Features/Tags/verbosity
+var_fuse=no
+var_tun=no
+var_tags=community-script
+var_verbose=no
+
+# Security (root PW) – empty => autologin
+# var_pw=
+EOF
+
+ # Now choose storages (always prompt unless just one exists)
+ choose_and_set_storage_for_file "$canonical" template
+ choose_and_set_storage_for_file "$canonical" container
+
+ chmod 0644 "$canonical"
+ msg_ok "Created ${canonical}"
+ }
+
+ # Whitelist check
+ local _is_whitelisted_key
+ _is_whitelisted_key() {
+ local k="$1"
+ local w
+ for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
+ return 1
+ }
+
+ # Safe parser for KEY=VALUE lines
+ local _load_vars_file
+ _load_vars_file() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ msg_info "Loading defaults from ${file}"
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [[ -z "$line" || "$line" == \#* ]] && continue
+ if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
+ local var_key="${BASH_REMATCH[1]}"
+ local var_val="${BASH_REMATCH[2]}"
+
+ [[ "$var_key" != var_* ]] && continue
+ _is_whitelisted_key "$var_key" || {
+ msg_debug "Ignore non-whitelisted ${var_key}"
+ continue
+ }
+
+ # Strip quotes
+ if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ fi
+
+ # Unsafe characters
+ case $var_val in
+ \"*\")
+ var_val=${var_val#\"}
+ var_val=${var_val%\"}
+ ;;
+ \'*\')
+ var_val=${var_val#\'}
+ var_val=${var_val%\'}
+ ;;
+ esac # Hard env wins
+ [[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue
+ # Set only if not already exported
+ [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
+ else
+ msg_warn "Malformed line in ${file}: ${line}"
+ fi
+ done <"$file"
+ msg_ok "Loaded ${file}"
+ }
+
+ # 1) Ensure file exists
+ _ensure_default_vars
+
+ # 2) Load file
+ local dv
+ dv="$(_find_default_vars)" || {
+ msg_error "default.vars not found after ensure step"
+ return 1
+ }
+ _load_vars_file "$dv"
+
+ # 3) Map var_verbose → VERBOSE
+ if [[ -n "${var_verbose:-}" ]]; then
+ case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
+ else
+ VERBOSE="no"
+ fi
+
+ # 4) Apply base settings and show summary
+ METHOD="mydefaults-global"
+ base_settings "$VERBOSE"
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
+ echo_default
+}
+
+# ------------------------------------------------------------------------------
+# get_app_defaults_path()
+#
+# - Returns full path for app-specific defaults file
+# - Example: /usr/local/community-scripts/defaults/.vars
+# ------------------------------------------------------------------------------
+
+get_app_defaults_path() {
+ local n="${NSAPP:-${APP,,}}"
+ echo "/usr/local/community-scripts/defaults/${n}.vars"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults
+#
+# - Called after advanced_settings returned with fully chosen values.
+# - If no .vars exists, offers to persist current advanced settings
+# into /usr/local/community-scripts/defaults/.vars
+# - Only writes whitelisted var_* keys.
+# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
+# ------------------------------------------------------------------------------
+if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
+ declare -ag VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+fi
+
+# Note: _is_whitelisted_key() is defined above in default_var_settings section
+
+_sanitize_value() {
+ # Disallow Command-Substitution / Shell-Meta
+ case "$1" in
+ *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
+ echo ""
+ return 0
+ ;;
+ esac
+ echo "$1"
+}
+
+# Map-Parser: read var_* from file into _VARS_IN associative array
+# Note: Main _load_vars_file() with full validation is defined in default_var_settings section
+# This simplified version is used specifically for diff operations via _VARS_IN array
+declare -A _VARS_IN
+_load_vars_file_to_map() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ _VARS_IN=() # Clear array
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [ -z "$line" ] && continue
+ case "$line" in
+ \#*) continue ;;
+ esac
+ key=$(printf "%s" "$line" | cut -d= -f1)
+ val=$(printf "%s" "$line" | cut -d= -f2-)
+ case "$key" in
+ var_*)
+ if _is_whitelisted_key "$key"; then
+ _VARS_IN["$key"]="$val"
+ fi
+ ;;
+ esac
+ done <"$file"
+}
+
+# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
+_build_vars_diff() {
+ local oldf="$1" newf="$2"
+ local k
+ local -A OLD=() NEW=()
+ _load_vars_file_to_map "$oldf"
+ for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
+ _load_vars_file_to_map "$newf"
+ for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
+
+ local out
+ out+="# Diff for ${APP} (${NSAPP})\n"
+ out+="# Old: ${oldf}\n# New: ${newf}\n\n"
+
+ local found_change=0
+
+ # Changed & Removed
+ for k in "${!OLD[@]}"; do
+ if [[ -v NEW["$k"] ]]; then
+ if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
+ out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ else
+ out+="- ${k}\n - old: ${OLD[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ # Added
+ for k in "${!NEW[@]}"; do
+ if [[ ! -v OLD["$k"] ]]; then
+ out+="+ ${k}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ if [[ $found_change -eq 0 ]]; then
+ out+="(No differences)\n"
+ fi
+
+ printf "%b" "$out"
+}
+
+# Build a temporary .vars file from current advanced settings
+_build_current_app_vars_tmp() {
+ tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
+
+ # NET/GW
+ _net="${NET:-}"
+ _gate=""
+ case "${GATE:-}" in
+ ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
+ esac
+
+ # IPv6
+ _ipv6_method="${IPV6_METHOD:-auto}"
+ _ipv6_static=""
+ _ipv6_gateway=""
+ if [ "$_ipv6_method" = "static" ]; then
+ _ipv6_static="${IPV6_ADDR:-}"
+ _ipv6_gateway="${IPV6_GATE:-}"
+ fi
+
+ # MTU/VLAN/MAC
+ _mtu=""
+ _vlan=""
+ _mac=""
+ case "${MTU:-}" in
+ ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
+ esac
+ case "${VLAN:-}" in
+ ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
+ esac
+ case "${MAC:-}" in
+ ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
+ esac
+
+ # DNS / Searchdomain
+ _ns=""
+ _searchdomain=""
+ case "${NS:-}" in
+ -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
+ esac
+ case "${SD:-}" in
+ -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
+ esac
+
+ # SSH / APT / Features
+ _ssh="${SSH:-no}"
+ _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
+ _apt_cacher="${APT_CACHER:-}"
+ _apt_cacher_ip="${APT_CACHER_IP:-}"
+ _fuse="${ENABLE_FUSE:-no}"
+ _tun="${ENABLE_TUN:-no}"
+ _tags="${TAGS:-}"
+ _verbose="${VERBOSE:-no}"
+
+ # Type / Resources / Identity
+ _unpriv="${CT_TYPE:-1}"
+ _cpu="${CORE_COUNT:-1}"
+ _ram="${RAM_SIZE:-1024}"
+ _disk="${DISK_SIZE:-4}"
+ _hostname="${HN:-$NSAPP}"
+
+ # Storage
+ _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
+ _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
+
+ {
+ echo "# App-specific defaults for ${APP} (${NSAPP})"
+ echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo
+
+ echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
+ echo "var_cpu=$(_sanitize_value "$_cpu")"
+ echo "var_ram=$(_sanitize_value "$_ram")"
+ echo "var_disk=$(_sanitize_value "$_disk")"
+
+ [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
+ [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
+ [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
+ [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
+ [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
+ [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
+ [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
+
+ [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
+ [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")"
+
+ [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
+ [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
+
+ [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
+ [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
+
+ [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
+ [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
+ [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
+ [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
+
+ [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
+ [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
+
+ [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
+ [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
+ } >"$tmpf"
+
+ echo "$tmpf"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults()
+#
+# - Called after advanced_settings()
+# - Offers to save current values as app defaults if not existing
+# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
+# ------------------------------------------------------------------------------
+maybe_offer_save_app_defaults() {
+ local app_vars_path
+ app_vars_path="$(get_app_defaults_path)"
+
+ # always build from current settings
+ local new_tmp diff_tmp
+ new_tmp="$(_build_current_app_vars_tmp)"
+ diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
+
+ # 1) if no file → offer to create
+ if [[ ! -f "$app_vars_path" ]]; then
+ if whiptail --backtitle "[dev] 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"
+ msg_ok "Saved app defaults: ${app_vars_path}"
+ fi
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 2) if file exists → build diff
+ _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
+
+ # if no differences → do nothing
+ if grep -q "^(No differences)$" "$diff_tmp"; then
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 3) if file exists → show menu with default selection "Update Defaults"
+ local app_vars_file
+ app_vars_file="$(basename "$app_vars_path")"
+
+ while true; do
+ local sel
+ sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "APP DEFAULTS – ${APP}" \
+ --menu "Differences detected. What do you want to do?" 20 78 10 \
+ "Update Defaults" "Write new values to ${app_vars_file}" \
+ "Keep Current" "Keep existing defaults (no changes)" \
+ "View Diff" "Show a detailed diff" \
+ "Cancel" "Abort without changes" \
+ --default-item "Update Defaults" \
+ 3>&1 1>&2 2>&3)" || { sel="Cancel"; }
+
+ case "$sel" in
+ "Update Defaults")
+ install -m 0644 "$new_tmp" "$app_vars_path"
+ msg_ok "Updated app defaults: ${app_vars_path}"
+ break
+ ;;
+ "Keep Current")
+ msg_info "Keeping current app defaults: ${app_vars_path}"
+ break
+ ;;
+ "View Diff")
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Diff – ${APP}" \
+ --scrolltext --textbox "$diff_tmp" 25 100
+ ;;
+ "Cancel" | *)
+ msg_info "Canceled. No changes to app defaults."
+ break
+ ;;
+ esac
+ done
+
+ rm -f "$new_tmp" "$diff_tmp"
+}
+
+ensure_storage_selection_for_vars_file() {
+ local vf="$1"
+
+ # 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-)
+
+ if [[ -n "$tpl" && -n "$ct" ]]; then
+ TEMPLATE_STORAGE="$tpl"
+ CONTAINER_STORAGE="$ct"
+ return 0
+ fi
+
+ choose_and_set_storage_for_file "$vf" template
+ choose_and_set_storage_for_file "$vf" container
+
+ msg_ok "Storage configuration saved to $(basename "$vf")"
+}
+
+diagnostics_menu() {
+ if [ "${DIAGNOSTICS:-no}" = "yes" ]; then
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "No" --no-button "Back"; then
+ DIAGNOSTICS="no"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ else
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "Yes" --no-button "Back"; then
+ DIAGNOSTICS="yes"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ fi
+}
+
+ensure_global_default_vars_file() {
+ local vars_path="/usr/local/community-scripts/default.vars"
+ if [[ ! -f "$vars_path" ]]; then
+ mkdir -p "$(dirname "$vars_path")"
+ touch "$vars_path"
+ fi
+ echo "$vars_path"
+}
+
+# ------------------------------------------------------------------------------
+# install_script()
+#
+# - Main entrypoint for installation mode
+# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
+# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
+# - Applies chosen settings and triggers container build
+# ------------------------------------------------------------------------------
+install_script() {
+ pve_check
+ shell_check
+ root_check
+ arch_check
+ ssh_check
+ maxkeys_check
+ diagnostics_check
+
+ if systemctl is-active -q ping-instances.service; then
+ systemctl -q stop ping-instances.service
+ fi
+
+ NEXTID=$(pvesh get /cluster/nextid)
+ timezone=$(cat /etc/timezone)
+
+ # Show APP Header
+ header_info
+
+ # --- Support CLI argument as direct preset (default, advanced, …) ---
+ CHOICE="${mode:-${1:-}}"
+
+ # If no CLI argument → show whiptail menu
+ # Build menu dynamically based on available options
+ local appdefaults_option=""
+ local settings_option=""
+ local menu_items=(
+ "1" "Default Install"
+ "2" "Advanced Install"
+ "3" "My Defaults"
+ )
+
+ if [ -f "$(get_app_defaults_path)" ]; then
+ appdefaults_option="4"
+ menu_items+=("4" "App Defaults for ${APP}")
+ settings_option="5"
+ menu_items+=("5" "Settings")
+ else
+ settings_option="4"
+ menu_items+=("4" "Settings")
+ fi
+
+ if [ -z "$CHOICE" ]; then
+
+ TMP_CHOICE=$(whiptail \
+ --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts Options" \
+ --ok-button "Select" --cancel-button "Exit Script" \
+ --notags \
+ --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
+ 20 60 9 \
+ "${menu_items[@]}" \
+ --default-item "1" \
+ 3>&1 1>&2 2>&3) || exit_script
+ CHOICE="$TMP_CHOICE"
+ fi
+
+ APPDEFAULTS_OPTION="$appdefaults_option"
+ SETTINGS_OPTION="$settings_option"
+
+ # --- Main case ---
+ local defaults_target=""
+ local run_maybe_offer="no"
+ case "$CHOICE" in
+ 1 | default | DEFAULT)
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
+ VERBOSE="no"
+ METHOD="default"
+ base_settings "$VERBOSE"
+ echo_default
+ defaults_target="$(ensure_global_default_vars_file)"
+ ;;
+ 2 | advanced | ADVANCED)
+ header_info
+
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ METHOD="advanced"
+ base_settings
+ advanced_settings
+ defaults_target="$(ensure_global_default_vars_file)"
+ run_maybe_offer="yes"
+ ;;
+ 3 | mydefaults | MYDEFAULTS)
+ default_var_settings || {
+ msg_error "Failed to apply default.vars"
+ exit 1
+ }
+ defaults_target="/usr/local/community-scripts/default.vars"
+ ;;
+ "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
+ METHOD="appdefaults"
+ base_settings
+ _load_vars_file "$(get_app_defaults_path)"
+ echo_default
+ defaults_target="$(get_app_defaults_path)"
+ else
+ msg_error "No App Defaults available for ${APP}"
+ exit 1
+ fi
+ ;;
+ "$SETTINGS_OPTION" | settings | SETTINGS)
+ settings_menu
+ defaults_target=""
+ ;;
+ *)
+ echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}"
+ exit 1
+ ;;
+ esac
+
+ if [[ -n "$defaults_target" ]]; then
+ ensure_storage_selection_for_vars_file "$defaults_target"
+ fi
+
+ if [[ "$run_maybe_offer" == "yes" ]]; then
+ maybe_offer_save_app_defaults
+ fi
+}
+
+edit_default_storage() {
+ local vf="/usr/local/community-scripts/default.vars"
+
+ # Ensure file exists
+ if [[ ! -f "$vf" ]]; then
+ mkdir -p "$(dirname "$vf")"
+ touch "$vf"
+ fi
+
+ # Let ensure_storage_selection_for_vars_file handle everything
+ ensure_storage_selection_for_vars_file "$vf"
+}
+
+settings_menu() {
+ while true; do
+ local settings_items=(
+ "1" "Manage API-Diagnostic Setting"
+ "2" "Edit Default.vars"
+ "3" "Edit Default Storage"
+ )
+ if [ -f "$(get_app_defaults_path)" ]; then
+ settings_items+=("4" "Edit App.vars for ${APP}")
+ settings_items+=("5" "Exit")
+ else
+ settings_items+=("4" "Exit")
+ fi
+
+ local choice
+ choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts SETTINGS Menu" \
+ --ok-button "OK" --cancel-button "Back" \
+ --menu "\n\nChoose a settings option:\n\nUse TAB or Arrow keys to navigate, ENTER to select." 20 60 9 \
+ "${settings_items[@]}" \
+ 3>&1 1>&2 2>&3) || break
+
+ case "$choice" in
+ 1) diagnostics_menu ;;
+ 2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
+ 3) edit_default_storage ;;
+ 4)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ ${EDITOR:-nano} "$(get_app_defaults_path)"
+ else
+ exit_script
+ fi
+ ;;
+ 5) exit_script ;;
+ esac
+ done
+}
+
+# ===== Unified storage selection & writing to vars files =====
+_write_storage_to_vars() {
+ # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
+ local vf="$1" key="$2" val="$3"
+ # remove uncommented and commented versions to avoid duplicates
+ sed -i "/^[#[:space:]]*${key}=/d" "$vf"
+ echo "${key}=${val}" >>"$vf"
+}
+
+choose_and_set_storage_for_file() {
+ # $1 = vars_file, $2 = class ('container'|'template')
+ local vf="$1" class="$2" key="" current=""
+ case "$class" in
+ container) key="var_container_storage" ;;
+ template) key="var_template_storage" ;;
+ *)
+ msg_error "Unknown storage class: $class"
+ return 1
+ ;;
+ esac
+
+ current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
+
+ # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
+ local content="rootdir"
+ [[ "$class" == "template" ]] && content="vztmpl"
+ local count
+ count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
+
+ if [[ "$count" -eq 1 ]]; then
+ STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
+ STORAGE_INFO=""
+ else
+ # If the current value is preselectable, we could show it, but per your requirement we always offer selection
+ select_storage "$class" || return 1
+ fi
+
+ _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
+
+ # Keep environment in sync for later steps (e.g. app-default save)
+ if [[ "$class" == "container" ]]; then
+ export var_container_storage="$STORAGE_RESULT"
+ export CONTAINER_STORAGE="$STORAGE_RESULT"
+ else
+ export var_template_storage="$STORAGE_RESULT"
+ export TEMPLATE_STORAGE="$STORAGE_RESULT"
+ fi
+
+ msg_ok "Updated ${key} → ${STORAGE_RESULT}"
+}
+
+# ------------------------------------------------------------------------------
+# check_container_resources()
+#
+# - Compares host RAM/CPU with required values
+# - Warns if under-provisioned and asks user to continue or abort
+# ------------------------------------------------------------------------------
+check_container_resources() {
+ current_ram=$(free -m | awk 'NR==2{print $2}')
+ current_cpu=$(nproc)
+
+ if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
+ echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
+ echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
+ echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
+ echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ else
+ echo -e ""
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# check_container_storage()
+#
+# - Checks /boot partition usage
+# - Warns if usage >80% and asks user confirmation before proceeding
+# ------------------------------------------------------------------------------
+check_container_storage() {
+ total_size=$(df /boot --output=size | tail -n 1)
+ local used_size=$(df /boot --output=used | tail -n 1)
+ usage=$((100 * used_size / total_size))
+ if ((usage > 80)); then
+ echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
+ echo -ne "Continue anyway? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
+ echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# ssh_extract_keys_from_file()
+#
+# - Extracts valid SSH public keys from given file
+# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
+# ------------------------------------------------------------------------------
+ssh_extract_keys_from_file() {
+ local f="$1"
+ [[ -r "$f" ]] || return 0
+ tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ # nackt: typ base64 [comment]
+ /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
+ # mit Optionen: finde ab erstem Key-Typ
+ {
+ match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
+ if (RSTART>0) { print substr($0, RSTART) }
+ }
+ '
+}
+
+# ------------------------------------------------------------------------------
+# ssh_build_choices_from_files()
+#
+# - Builds interactive whiptail checklist of available SSH keys
+# - Generates fingerprint, type and comment for each key
+# ------------------------------------------------------------------------------
+ssh_build_choices_from_files() {
+ local -a files=("$@")
+ CHOICES=()
+ COUNT=0
+ MAPFILE="$(mktemp)"
+ local id key typ fp cmt base ln=0
+
+ for f in "${files[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # map every key in file
+ while IFS= read -r key; do
+ [[ -n "$key" ]] || continue
+
+ typ=""
+ fp=""
+ cmt=""
+ # Only the pure key part (without options) is already included in ‘key’.
+ read -r _typ _b64 _cmt <<<"$key"
+ typ="${_typ:-key}"
+ cmt="${_cmt:-}"
+ # Fingerprint via ssh-keygen (if available)
+ if command -v ssh-keygen >/dev/null 2>&1; then
+ fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
+ fi
+ # Label shorten
+ [[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
+
+ ln=$((ln + 1))
+ COUNT=$((COUNT + 1))
+ id="K${COUNT}"
+ echo "${id}|${key}" >>"$MAPFILE"
+ CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
+ done < <(ssh_extract_keys_from_file "$f")
+ done
+}
+
+# ------------------------------------------------------------------------------
+# ssh_discover_default_files()
+#
+# - Scans standard paths for SSH keys
+# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc.
+# ------------------------------------------------------------------------------
+ssh_discover_default_files() {
+ local -a cand=()
+ shopt -s nullglob
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ shopt -u nullglob
+ printf '%s\0' "${cand[@]}"
+}
+
+configure_ssh_settings() {
+ SSH_KEYS_FILE="$(mktemp)"
+ : >"$SSH_KEYS_FILE"
+
+ IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
+ ssh_build_choices_from_files "${_def_files[@]}"
+ local default_key_count="$COUNT"
+
+ local ssh_key_mode
+ if [[ "$default_key_count" -gt 0 ]]; then
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "Provision SSH keys for root:" 14 72 4 \
+ "found" "Select from detected keys (${default_key_count})" \
+ "manual" "Paste a single public key" \
+ "folder" "Scan another folder (path or glob)" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ else
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "No host keys detected; choose manual/none:" 12 72 2 \
+ "manual" "Paste a single public key" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ fi
+
+ case "$ssh_key_mode" in
+ found)
+ local selection
+ selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \
+ --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ ;;
+ manual)
+ SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
+ [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
+ ;;
+ folder)
+ local glob_path
+ glob_path=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
+ if [[ -n "$glob_path" ]]; then
+ shopt -s nullglob
+ read -r -a _scan_files <<<"$glob_path"
+ shopt -u nullglob
+ if [[ "${#_scan_files[@]}" -gt 0 ]]; then
+ ssh_build_choices_from_files "${_scan_files[@]}"
+ if [[ "$COUNT" -gt 0 ]]; then
+ local folder_selection
+ folder_selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \
+ --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $folder_selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60
+ fi
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60
+ fi
+ fi
+ ;;
+ none)
+ :
+ ;;
+ esac
+
+ if [[ -s "$SSH_KEYS_FILE" ]]; then
+ sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
+ printf '\n' >>"$SSH_KEYS_FILE"
+ fi
+
+ if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
+ SSH="yes"
+ else
+ SSH="no"
+ fi
+ else
+ SSH="no"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# start()
+#
+# - Entry point of script
+# - On Proxmox host: calls install_script
+# - In silent mode: runs update_script
+# - Otherwise: shows update/setting menu
+# ------------------------------------------------------------------------------
+start() {
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func)
+ if command -v pveversion >/dev/null 2>&1; then
+ install_script || return 0
+ return 0
+ elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
+ VERBOSE="no"
+ set_std_mode
+ update_script
+ else
+ CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
+ "Support/Update functions for ${APP} LXC. Choose an option:" \
+ 12 60 3 \
+ "1" "YES (Silent Mode)" \
+ "2" "YES (Verbose Mode)" \
+ "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
+
+ case "$CHOICE" in
+ 1)
+ VERBOSE="no"
+ set_std_mode
+ ;;
+ 2)
+ VERBOSE="yes"
+ set_std_mode
+ ;;
+ 3)
+ clear
+ exit_script
+ exit
+ ;;
+ esac
+ update_script
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# build_container()
+#
+# - Creates and configures the LXC container
+# - Builds network string and applies features (FUSE, TUN, VAAPI passthrough)
+# - Starts container and waits for network connectivity
+# - Installs base packages, SSH keys, and runs -install.sh
+# ------------------------------------------------------------------------------
+build_container() {
+ # if [ "$VERBOSE" == "yes" ]; then set -x; fi
+
+ NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
+
+ # MAC
+ if [[ -n "$MAC" ]]; then
+ case "$MAC" in
+ ,hwaddr=*) NET_STRING+="$MAC" ;;
+ *) NET_STRING+=",hwaddr=$MAC" ;;
+ esac
+ fi
+
+ # IP (immer zwingend, Standard dhcp)
+ NET_STRING+=",ip=${NET:-dhcp}"
+
+ # Gateway
+ if [[ -n "$GATE" ]]; then
+ case "$GATE" in
+ ,gw=*) NET_STRING+="$GATE" ;;
+ *) NET_STRING+=",gw=$GATE" ;;
+ esac
+ fi
+
+ # VLAN
+ if [[ -n "$VLAN" ]]; then
+ case "$VLAN" in
+ ,tag=*) NET_STRING+="$VLAN" ;;
+ *) NET_STRING+=",tag=$VLAN" ;;
+ esac
+ fi
+
+ # MTU
+ if [[ -n "$MTU" ]]; then
+ case "$MTU" in
+ ,mtu=*) NET_STRING+="$MTU" ;;
+ *) NET_STRING+=",mtu=$MTU" ;;
+ esac
+ fi
+
+ # IPv6 Handling
+ case "$IPV6_METHOD" in
+ auto) NET_STRING="$NET_STRING,ip6=auto" ;;
+ dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
+ static)
+ NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
+ [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
+ ;;
+ none) ;;
+ esac
+
+ if [ "$CT_TYPE" == "1" ]; then
+ FEATURES="keyctl=1,nesting=1"
+ else
+ FEATURES="nesting=1"
+ fi
+
+ if [ "$ENABLE_FUSE" == "yes" ]; then
+ FEATURES="$FEATURES,fuse=1"
+ fi
+
+ TEMP_DIR=$(mktemp -d)
+ pushd "$TEMP_DIR" >/dev/null
+ if [ "$var_os" == "alpine" ]; then
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)"
+ else
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)"
+ fi
+ export DIAGNOSTICS="$DIAGNOSTICS"
+ export RANDOM_UUID="$RANDOM_UUID"
+ export CACHER="$APT_CACHER"
+ export CACHER_IP="$APT_CACHER_IP"
+ export tz="$timezone"
+ export APPLICATION="$APP"
+ export app="$NSAPP"
+ export PASSWORD="$PW"
+ export VERBOSE="$VERBOSE"
+ export SSH_ROOT="${SSH}"
+ export SSH_AUTHORIZED_KEY
+ export CTID="$CT_ID"
+ export CTTYPE="$CT_TYPE"
+ export ENABLE_FUSE="$ENABLE_FUSE"
+ export ENABLE_TUN="$ENABLE_TUN"
+ export PCT_OSTYPE="$var_os"
+ export PCT_OSVERSION="$var_version"
+ export PCT_DISK_SIZE="$DISK_SIZE"
+ export PCT_OPTIONS="
+ -features $FEATURES
+ -hostname $HN
+ -tags $TAGS
+ $SD
+ $NS
+ $NET_STRING
+ -onboot 1
+ -cores $CORE_COUNT
+ -memory $RAM_SIZE
+ -unprivileged $CT_TYPE
+ $PW
+"
+ export TEMPLATE_STORAGE="${var_template_storage:-}"
+ export CONTAINER_STORAGE="${var_container_storage:-}"
+ create_lxc_container || exit $?
+
+ LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
+
+ # ============================================================================
+ # GPU/USB PASSTHROUGH CONFIGURATION
+ # ============================================================================
+
+ # List of applications that benefit from GPU acceleration
+ GPU_APPS=(
+ "immich" "channels" "emby" "ersatztv" "frigate"
+ "jellyfin" "plex" "scrypted" "tdarr" "unmanic"
+ "ollama" "fileflows" "open-webui" "tunarr" "debian"
+ "handbrake" "sunshine" "moonlight" "kodi" "stremio"
+ "viseron"
+ )
+
+ # Check if app needs GPU
+ is_gpu_app() {
+ local app="${1,,}"
+ for gpu_app in "${GPU_APPS[@]}"; do
+ [[ "$app" == "${gpu_app,,}" ]] && return 0
+ done
+ return 1
+ }
+
+ # Detect all available GPU devices
+ detect_gpu_devices() {
+ INTEL_DEVICES=()
+ AMD_DEVICES=()
+ NVIDIA_DEVICES=()
+
+ # Store PCI info to avoid multiple calls
+ local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D")
+
+ # Check for Intel GPU - look for Intel vendor ID [8086]
+ if echo "$pci_vga_info" | grep -q "\[8086:"; then
+ msg_info "Detected Intel GPU"
+ if [[ -d /dev/dri ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && INTEL_DEVICES+=("$d")
+ done
+ fi
+ fi
+
+ # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
+ if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then
+ msg_info "Detected AMD GPU"
+ if [[ -d /dev/dri ]]; then
+ # Only add if not already claimed by Intel
+ if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && AMD_DEVICES+=("$d")
+ done
+ fi
+ fi
+ fi
+
+ # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
+ if echo "$pci_vga_info" | grep -q "\[10de:"; then
+ msg_info "Detected NVIDIA GPU"
+ if ! check_nvidia_host_setup; then
+ msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough."
+ msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually."
+ return 0
+ fi
+
+ for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do
+ [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
+ done
+
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found"
+ msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver"
+ else
+ if [[ "$CT_TYPE" == "0" ]]; then
+ cat <>"$LXC_CONFIG"
+ # NVIDIA GPU Passthrough (privileged)
+ lxc.cgroup2.devices.allow: c 195:* rwm
+ lxc.cgroup2.devices.allow: c 243:* rwm
+ lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file
+EOF
+
+ if [[ -e /dev/dri/renderD128 ]]; then
+ echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+
+ export GPU_TYPE="NVIDIA"
+ export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
+ msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})"
+ else
+ msg_warn "NVIDIA passthrough only supported for privileged containers"
+ return 0
+ fi
+ fi
+ fi
+
+ # Debug output
+ msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
+ msg_debug "AMD devices: ${AMD_DEVICES[*]}"
+ msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
+ }
+
+ # Configure USB passthrough for privileged containers
+ configure_usb_passthrough() {
+ if [[ "$CT_TYPE" != "0" ]]; then
+ return 0
+ fi
+
+ msg_info "Configuring automatic USB passthrough (privileged container)"
+ cat <>"$LXC_CONFIG"
+# Automatic USB passthrough (privileged container)
+lxc.cgroup2.devices.allow: a
+lxc.cap.drop:
+lxc.cgroup2.devices.allow: c 188:* rwm
+lxc.cgroup2.devices.allow: c 189:* rwm
+lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
+lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
+EOF
+ msg_ok "USB passthrough configured"
+ }
+
+ # Configure GPU passthrough
+ configure_gpu_passthrough() {
+ # Skip if not a GPU app and not privileged
+ if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then
+ return 0
+ fi
+
+ detect_gpu_devices
+
+ # Count available GPU types
+ local gpu_count=0
+ local available_gpus=()
+
+ if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("INTEL")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("AMD")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("NVIDIA")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ $gpu_count -eq 0 ]]; then
+ msg_info "No GPU devices found for passthrough"
+ return 0
+ fi
+
+ local selected_gpu=""
+
+ if [[ $gpu_count -eq 1 ]]; then
+ # Automatic selection for single GPU
+ selected_gpu="${available_gpus[0]}"
+ msg_info "Automatically configuring ${selected_gpu} GPU passthrough"
+ else
+ # Multiple GPUs - ask user
+ echo -e "\n${INFO} Multiple GPU types detected:"
+ for gpu in "${available_gpus[@]}"; do
+ echo " - $gpu"
+ done
+ read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
+ selected_gpu="${selected_gpu^^}"
+
+ # Validate selection
+ local valid=0
+ for gpu in "${available_gpus[@]}"; do
+ [[ "$selected_gpu" == "$gpu" ]] && valid=1
+ done
+
+ if [[ $valid -eq 0 ]]; then
+ msg_warn "Invalid selection. Skipping GPU passthrough."
+ return 0
+ fi
+ fi
+
+ # Apply passthrough configuration based on selection
+ local dev_idx=0
+
+ case "$selected_gpu" in
+ INTEL | AMD)
+ local devices=()
+ [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
+ [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
+
+ # For Proxmox WebUI visibility, add as dev0, dev1 etc.
+ for dev in "${devices[@]}"; do
+ if [[ "$CT_TYPE" == "0" ]]; then
+ # Privileged container - use dev entries for WebUI visibility
+ # Use initial GID 104 (render) for renderD*, 44 (video) for card*
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+
+ # Also add cgroup allows for privileged containers
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ else
+ # Unprivileged container
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+ fi
+ done
+
+ export GPU_TYPE="$selected_gpu"
+ msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)"
+ ;;
+
+ NVIDIA)
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver"
+ return 1
+ fi
+
+ for dev in "${NVIDIA_DEVICES[@]}"; do
+ # NVIDIA devices typically need different handling
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ dev_idx=$((dev_idx + 1))
+
+ if [[ "$CT_TYPE" == "0" ]]; then
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ fi
+ done
+
+ export GPU_TYPE="NVIDIA"
+ msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)"
+ ;;
+ esac
+ }
+
+ # Additional device passthrough
+ configure_additional_devices() {
+ # TUN device passthrough
+ if [ "$ENABLE_TUN" == "yes" ]; then
+ cat <>"$LXC_CONFIG"
+lxc.cgroup2.devices.allow: c 10:200 rwm
+lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
+EOF
+ fi
+
+ # Coral TPU passthrough
+ if [[ -e /dev/apex_0 ]]; then
+ msg_info "Detected Coral TPU - configuring passthrough"
+ echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+ }
+
+ # Execute pre-start configurations
+ configure_usb_passthrough
+ configure_gpu_passthrough
+ configure_additional_devices
+
+ # ============================================================================
+ # START CONTAINER AND INSTALL USERLAND
+ # ============================================================================
+
+ msg_info "Starting LXC Container"
+ pct start "$CTID"
+
+ # Wait for container to be running
+ for i in {1..10}; do
+ if pct status "$CTID" | grep -q "status: running"; then
+ msg_ok "Started LXC Container"
+ break
+ fi
+ sleep 1
+ if [ "$i" -eq 10 ]; then
+ msg_error "LXC Container did not reach running state"
+ exit 1
+ fi
+ done
+
+ # Wait for network (skip for Alpine initially)
+ if [ "$var_os" != "alpine" ]; then
+ msg_info "Waiting for network in LXC container"
+
+ # Wait for IP
+ for i in {1..20}; do
+ ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+ [ -n "$ip_in_lxc" ] && break
+ sleep 1
+ done
+
+ if [ -z "$ip_in_lxc" ]; then
+ msg_error "No IP assigned to CT $CTID after 20s"
+ exit 1
+ fi
+
+ # Try to reach gateway
+ gw_ok=0
+ for i in {1..10}; do
+ if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then
+ gw_ok=1
+ break
+ fi
+ sleep 1
+ done
+
+ if [ "$gw_ok" -eq 1 ]; then
+ msg_ok "Network in LXC is reachable (IP $ip_in_lxc)"
+ else
+ msg_warn "Network reachable but gateway check failed"
+ fi
+ fi
+ # Function to get correct GID inside container
+ get_container_gid() {
+ local group="$1"
+ local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
+ echo "${gid:-44}" # Default to 44 if not found
+ }
+
+ fix_gpu_gids
+
+ # Continue with standard container setup
+ msg_info "Customizing LXC Container"
+
+ # # Install GPU userland if configured
+ # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
+ # install_gpu_userland "VAAPI"
+ # fi
+
+ # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
+ # install_gpu_userland "NVIDIA"
+ # fi
+
+ # Continue with standard container setup
+ if [ "$var_os" == "alpine" ]; then
+ sleep 3
+ pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
+EOF'
+ pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
+ else
+ sleep 3
+ pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
+ pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
+ echo LANG=\$locale_line >/etc/default/locale && \
+ locale-gen >/dev/null && \
+ export LANG=\$locale_line"
+
+ if [[ -z "${tz:-}" ]]; then
+ tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
+ fi
+
+ if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
+ pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
+ else
+ msg_warn "Skipping timezone setup – zone '$tz' not found in container"
+ fi
+
+ pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || {
+ msg_error "apt-get base packages installation failed"
+ exit 1
+ }
+ fi
+
+ msg_ok "Customized LXC Container"
+
+ # Verify GPU access if enabled
+ if [[ "${ENABLE_VAAPI:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "vainfo >/dev/null 2>&1" &&
+ msg_ok "VAAPI verified working" ||
+ msg_warn "VAAPI verification failed - may need additional configuration"
+ fi
+
+ if [[ "${ENABLE_NVIDIA:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "nvidia-smi >/dev/null 2>&1" &&
+ msg_ok "NVIDIA verified working" ||
+ msg_warn "NVIDIA verification failed - may need additional configuration"
+ fi
+
+ # Install SSH keys
+ install_ssh_keys_into_ct
+
+ # Run application installer
+ if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then
+ exit $?
+ fi
+}
+
+destroy_lxc() {
+ if [[ -z "$CT_ID" ]]; then
+ msg_error "No CT_ID found. Nothing to remove."
+ return 1
+ fi
+
+ # Abbruch bei Ctrl-C / Ctrl-D / ESC
+ trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
+
+ local prompt
+ if ! read -rp "Remove this Container? " prompt; then
+ # read gibt != 0 zurück bei Ctrl-D/ESC
+ msg_error "Aborted input (Ctrl-D/ESC)"
+ return 130
+ fi
+
+ case "${prompt,,}" in
+ y | yes)
+ if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
+ msg_ok "Removed Container $CT_ID"
+ else
+ msg_error "Failed to remove Container $CT_ID"
+ return 1
+ fi
+ ;;
+ "" | n | no)
+ msg_info "Container was not removed."
+ ;;
+ *)
+ msg_warn "Invalid response. Container was not removed."
+ ;;
+ esac
+}
+
+# ------------------------------------------------------------------------------
+# Storage discovery / selection helpers
+# ------------------------------------------------------------------------------
+# ===== Storage discovery / selection helpers (ported from create_lxc.sh) =====
+resolve_storage_preselect() {
+ local class="$1" preselect="$2" required_content=""
+ case "$class" in
+ template) required_content="vztmpl" ;;
+ container) required_content="rootdir" ;;
+ *) return 1 ;;
+ 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
+ fi
+
+ local line total used free
+ line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
+ if [[ -z "$line" ]]; then
+ STORAGE_INFO="n/a"
+ else
+ total="$(awk '{print $4}' <<<"$line")"
+ used="$(awk '{print $5}' <<<"$line")"
+ free="$(awk '{print $6}' <<<"$line")"
+ local total_h used_h free_h
+ if command -v numfmt >/dev/null 2>&1; then
+ total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
+ used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
+ free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
+ STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
+ else
+ STORAGE_INFO="Free: ${free} Used: ${used}"
+ fi
+ fi
+ STORAGE_RESULT="$preselect"
+ return 0
+}
+
+fix_gpu_gids() {
+ if [[ -z "${GPU_TYPE:-}" ]]; then
+ return 0
+ fi
+
+ msg_info "Detecting and setting correct GPU group IDs"
+
+ # Ermittle die tatsächlichen GIDs aus dem Container
+ local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+
+ # Fallbacks wenn Gruppen nicht existieren
+ if [[ -z "$video_gid" ]]; then
+ # Versuche die video Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true"
+ video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback
+ fi
+
+ if [[ -z "$render_gid" ]]; then
+ # Versuche die render Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true"
+ render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+ [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback
+ fi
+
+ msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}"
+
+ # Prüfe ob die GIDs von den Defaults abweichen
+ local need_update=0
+ if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then
+ need_update=1
+ fi
+
+ if [[ $need_update -eq 1 ]]; then
+ msg_info "Updating device GIDs in container config"
+
+ # Stoppe Container für Config-Update
+ pct stop "$CTID" >/dev/null 2>&1
+
+ # Update die dev Einträge mit korrekten GIDs
+ # Backup der Config
+ cp "$LXC_CONFIG" "${LXC_CONFIG}.bak"
+
+ # Parse und update jeden dev Eintrag
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^dev[0-9]+: ]]; then
+ # Extract device path
+ local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/')
+ local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/')
+
+ if [[ "$device_path" =~ renderD ]]; then
+ # RenderD device - use render GID
+ echo "${dev_num}: ${device_path},gid=${render_gid}"
+ elif [[ "$device_path" =~ card ]]; then
+ # Card device - use video GID
+ echo "${dev_num}: ${device_path},gid=${video_gid}"
+ else
+ # Keep original line
+ echo "$line"
+ fi
+ else
+ # Keep non-dev lines
+ echo "$line"
+ fi
+ done <"$LXC_CONFIG" >"${LXC_CONFIG}.new"
+
+ mv "${LXC_CONFIG}.new" "$LXC_CONFIG"
+
+ # Starte Container wieder
+ pct start "$CTID" >/dev/null 2>&1
+ sleep 3
+
+ msg_ok "Device GIDs updated successfully"
+ else
+ msg_ok "Device GIDs are already correct"
+ fi
+ if [[ "$CT_TYPE" == "0" ]]; then
+ pct exec "$CTID" -- bash -c "
+ if [ -d /dev/dri ]; then
+ for dev in /dev/dri/*; do
+ if [ -e \"\$dev\" ]; then
+ if [[ \"\$dev\" =~ renderD ]]; then
+ chgrp ${render_gid} \"\$dev\" 2>/dev/null || true
+ else
+ chgrp ${video_gid} \"\$dev\" 2>/dev/null || true
+ fi
+ chmod 660 \"\$dev\" 2>/dev/null || true
+ fi
+ done
+ fi
+ " >/dev/null 2>&1
+ fi
+}
+
+# NVIDIA-spezific check on host
+check_nvidia_host_setup() {
+ if ! command -v nvidia-smi >/dev/null 2>&1; then
+ msg_warn "NVIDIA GPU detected but nvidia-smi not found on host"
+ msg_warn "Please install NVIDIA drivers on host first."
+ #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run"
+ #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms"
+ #echo " 3. Verify: nvidia-smi"
+ return 1
+ fi
+
+ # check if nvidia-smi works
+ if ! nvidia-smi >/dev/null 2>&1; then
+ msg_warn "nvidia-smi installed but not working. Driver issue?"
+ return 1
+ fi
+
+ return 0
+}
+
+check_storage_support() {
+ local CONTENT="$1" VALID=0
+ while IFS= read -r line; do
+ local STORAGE_NAME
+ STORAGE_NAME=$(awk '{print $1}' <<<"$line")
+ [[ -n "$STORAGE_NAME" ]] && VALID=1
+ done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
+ [[ $VALID -eq 1 ]]
+}
+
+select_storage() {
+ local CLASS=$1 CONTENT CONTENT_LABEL
+ case $CLASS in
+ container)
+ CONTENT='rootdir'
+ CONTENT_LABEL='Container'
+ ;;
+ template)
+ CONTENT='vztmpl'
+ CONTENT_LABEL='Container template'
+ ;;
+ iso)
+ CONTENT='iso'
+ CONTENT_LABEL='ISO image'
+ ;;
+ images)
+ CONTENT='images'
+ CONTENT_LABEL='VM Disk image'
+ ;;
+ backup)
+ CONTENT='backup'
+ CONTENT_LABEL='Backup'
+ ;;
+ snippets)
+ CONTENT='snippets'
+ CONTENT_LABEL='Snippets'
+ ;;
+ *)
+ msg_error "Invalid storage class '$CLASS'"
+ return 1
+ ;;
+ esac
+
+ declare -A STORAGE_MAP
+ local -a MENU=()
+ local COL_WIDTH=0
+
+ while read -r TAG TYPE _ TOTAL USED FREE _; do
+ [[ -n "$TAG" && -n "$TYPE" ]] || continue
+ local DISPLAY="${TAG} (${TYPE})"
+ local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
+ local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
+ local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
+ STORAGE_MAP["$DISPLAY"]="$TAG"
+ MENU+=("$DISPLAY" "$INFO" "OFF")
+ ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
+ done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
+
+ if [[ ${#MENU[@]} -eq 0 ]]; then
+ msg_error "No storage found for content type '$CONTENT'."
+ return 2
+ fi
+
+ if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
+ STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
+ STORAGE_INFO="${MENU[1]}"
+ return 0
+ fi
+
+ local WIDTH=$((COL_WIDTH + 42))
+ while true; do
+ local DISPLAY_SELECTED
+ DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Storage Pools" \
+ --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
+ 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }
+
+ DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
+ if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
+ whiptail --msgbox "No valid storage selected. Please try again." 8 58
+ continue
+ fi
+ STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
+ for ((i = 0; i < ${#MENU[@]}; i += 3)); do
+ if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
+ STORAGE_INFO="${MENU[$i + 1]}"
+ break
+ fi
+ done
+ return 0
+ done
+}
+
+create_lxc_container() {
+ # ------------------------------------------------------------------------------
+ # Optional verbose mode (debug tracing)
+ # ------------------------------------------------------------------------------
+ if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
+
+ # ------------------------------------------------------------------------------
+ # Helpers (dynamic versioning / template parsing)
+ # ------------------------------------------------------------------------------
+ pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
+ pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
+
+ ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
+ ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
+ ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
+
+ # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
+ parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
+
+ # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
+ # Returns:
+ # 0 = no upgrade needed
+ # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
+ # 2 = user declined
+ # 3 = upgrade attempted but failed OR retry failed
+ offer_lxc_stack_upgrade_and_maybe_retry() {
+ local do_retry="${1:-no}" # yes|no
+ local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
+
+ _pvec_i="$(pkg_ver pve-container)"
+ _lxcp_i="$(pkg_ver lxc-pve)"
+ _pvec_c="$(pkg_cand pve-container)"
+ _lxcp_c="$(pkg_cand lxc-pve)"
+
+ if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
+ ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
+ fi
+ if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
+ ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
+ fi
+ if [[ $need -eq 0 ]]; then
+ msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
+ return 0
+ fi
+
+ echo
+ echo "An update for the Proxmox LXC stack is available:"
+ echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
+ echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
+ echo
+ read -rp "Do you want to upgrade now? [y/N] " _ans
+ case "${_ans,,}" in
+ y | yes)
+ msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
+ if apt-get update -qq >/dev/null && apt-get install -y --only-upgrade pve-container lxc-pve >/dev/null; then
+ msg_ok "LXC stack upgraded."
+ if [[ "$do_retry" == "yes" ]]; then
+ msg_info "Retrying container creation after upgrade"
+ if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container created successfully after upgrade."
+ return 0
+ else
+ msg_error "pct create still failed after upgrade. See $LOGFILE"
+ return 3
+ fi
+ fi
+ return 1
+ else
+ msg_error "Upgrade failed. Please check APT output."
+ return 3
+ fi
+ ;;
+ *) return 2 ;;
+ esac
+ }
+
+ # ------------------------------------------------------------------------------
+ # Required input variables
+ # ------------------------------------------------------------------------------
+ [[ "${CTID:-}" ]] || {
+ msg_error "You need to set 'CTID' variable."
+ exit 203
+ }
+ [[ "${PCT_OSTYPE:-}" ]] || {
+ msg_error "You need to set 'PCT_OSTYPE' variable."
+ exit 204
+ }
+
+ msg_debug "CTID=$CTID"
+ msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
+ msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
+
+ # ID checks
+ [[ "$CTID" -ge 100 ]] || {
+ msg_error "ID cannot be less than 100."
+ exit 205
+ }
+ if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
+ echo -e "ID '$CTID' is already in use."
+ unset CTID
+ msg_error "Cannot use ID that is already in use."
+ exit 206
+ fi
+
+ # Storage capability check
+ check_storage_support "rootdir" || {
+ msg_error "No valid storage found for 'rootdir' [Container]"
+ exit 1
+ }
+ check_storage_support "vztmpl" || {
+ msg_error "No valid storage found for 'vztmpl' [Template]"
+ exit 1
+ }
+
+ # Template storage selection
+ if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ else
+ while true; do
+ if [[ -z "${var_template_storage:-}" ]]; then
+ if select_storage template; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ break
+ fi
+ fi
+ done
+ fi
+
+ # Container storage selection
+ if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ else
+ if [[ -z "${var_container_storage:-}" ]]; then
+ if select_storage container; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ fi
+ fi
+ fi
+
+ # Validate content types
+ msg_info "Validating content types of storage '$CONTAINER_STORAGE'"
+ STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT"
+ grep -qw "rootdir" <<<"$STORAGE_CONTENT" || {
+ msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC."
+ exit 217
+ }
+ $STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'"
+
+ msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'"
+ TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT"
+ if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then
+ msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail."
+ else
+ $STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'"
+ fi
+
+ # Free space check
+ STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
+ REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
+ [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || {
+ msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
+ exit 214
+ }
+
+ # Cluster quorum (if cluster)
+ if [[ -f /etc/pve/corosync.conf ]]; then
+ msg_info "Checking cluster quorum"
+ if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
+ msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
+ exit 210
+ fi
+ msg_ok "Cluster is quorate"
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Template discovery & validation
+ # ------------------------------------------------------------------------------
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ case "$PCT_OSTYPE" in
+ debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
+ alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
+ *) TEMPLATE_PATTERN="" ;;
+ esac
+
+ msg_info "Searching for template '$TEMPLATE_SEARCH'"
+
+ # Build regex patterns outside awk/grep for clarity
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}"
+
+ #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'"
+ #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'"
+ #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+
+ pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)."
+
+ #echo "[DEBUG] pveam available output (first 5 lines with .tar files):"
+ #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /'
+
+ set +u
+ mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
+ #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found"
+ set -u
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #echo "[DEBUG] Online templates:"
+ for tmpl in "${ONLINE_TEMPLATES[@]}"; do
+ echo " - $tmpl"
+ done
+ fi
+
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'"
+ #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates"
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #msg_debug "First 3 online templates:"
+ count=0
+ for idx in "${!ONLINE_TEMPLATES[@]}"; do
+ #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}"
+ ((count++))
+ [[ $count -ge 3 ]] && break
+ done
+ fi
+ #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ # If still no template, try to find alternatives
+ if [[ -z "$TEMPLATE" ]]; then
+ echo ""
+ echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."
+
+ # Get all available versions for this OS type
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" |
+ sort -u -V 2>/dev/null
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo ""
+ echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ #echo "[DEBUG] Retrying with version: $PCT_OSVERSION"
+
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="online"
+ #echo "[DEBUG] Found alternative: $TEMPLATE"
+ else
+ msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ exit 225
+ fi
+ else
+ msg_info "Installation cancelled"
+ exit 0
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available at all"
+ exit 225
+ fi
+ fi
+
+ #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+ #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"
+
+ # Get available versions
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
+ grep -E '^[0-9]+\.[0-9]+$' |
+ sort -u -V 2>/dev/null || sort -u
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo -e "\n${BL}Available versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ export PCT_OSVERSION="$var_version"
+ msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"
+
+ # Retry template search with new version
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ msg_error "Template still not found after version change"
+ exit 220
+ }
+ else
+ msg_info "Installation cancelled"
+ exit 1
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available"
+ exit 220
+ fi
+ fi
+ }
+
+ # Validate that we found a template
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ msg_info "Please check:"
+ msg_info " - Is pveam catalog available? (run: pveam available -section system)"
+ msg_info " - Does the template exist for your OS version?"
+ exit 225
+ fi
+
+ msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
+ msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
+
+ NEED_DOWNLOAD=0
+ if [[ ! -f "$TEMPLATE_PATH" ]]; then
+ msg_info "Template not present locally – will download."
+ NEED_DOWNLOAD=1
+ elif [[ ! -r "$TEMPLATE_PATH" ]]; then
+ msg_error "Template file exists but is not readable – check permissions."
+ exit 221
+ elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template file too small (<1MB) – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template looks too small, but no online version exists. Keeping local file."
+ fi
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
+ fi
+ else
+ $STD msg_ok "Template $TEMPLATE is present and valid."
+ fi
+
+ if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
+ if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
+ TEMPLATE="$ONLINE_TEMPLATE"
+ NEED_DOWNLOAD=1
+ else
+ msg_info "Continuing with local template $TEMPLATE"
+ fi
+ fi
+
+ if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
+ [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
+ for attempt in {1..3}; do
+ msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
+ if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
+ msg_ok "Template download successful."
+ break
+ fi
+ if [[ $attempt -eq 3 ]]; then
+ msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
+ exit 222
+ fi
+ sleep $((attempt * 5))
+ done
+ fi
+
+ if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
+ msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
+ exit 223
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
+ # ------------------------------------------------------------------------------
+ if [[ "$PCT_OSTYPE" == "debian" ]]; then
+ OSVER="$(parse_template_osver "$TEMPLATE")"
+ if [[ -n "$OSVER" ]]; then
+ # Proactive, aber ohne Abbruch – nur Angebot
+ offer_lxc_stack_upgrade_and_maybe_retry "no" || true
+ fi
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Create LXC Container
+ # ------------------------------------------------------------------------------
+ msg_info "Creating LXC container"
+
+ # Ensure subuid/subgid entries exist
+ grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
+ grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
+
+ # Assemble pct options
+ PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
+ [[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
+
+ # Lock by template file (avoid concurrent downloads/creates)
+ lockfile="/tmp/template.${TEMPLATE}.lock"
+ exec 9>"$lockfile" || {
+ msg_error "Failed to create lock file '$lockfile'."
+ exit 200
+ }
+ flock -w 60 9 || {
+ msg_error "Timeout while waiting for template lock."
+ exit 211
+ }
+
+ LOGFILE="/tmp/pct_create_${CTID}.log"
+ msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}"
+ msg_debug "Logfile: $LOGFILE"
+
+ # First attempt
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then
+ msg_error "Container creation failed on ${TEMPLATE_STORAGE}. Checking template..."
+
+ # Validate template file
+ if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ msg_warn "Template file too small or missing – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
+ fi
+ fi
+
+ # Retry after repair
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ # Fallback to local storage
+ if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
+ msg_warn "Retrying container creation with fallback to local storage..."
+ LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
+ msg_info "Downloading template to local..."
+ pveam download local "$TEMPLATE" >/dev/null 2>&1
+ fi
+ if pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container successfully created using local fallback."
+ else
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed even with local fallback. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ else
+ msg_error "Container creation failed on local storage. See $LOGFILE"
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ fi
+ fi
+
+ # Verify container exists
+ pct list | awk '{print $1}' | grep -qx "$CTID" || {
+ msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE"
+ exit 215
+ }
+
+ # Verify config rootfs
+ grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
+ msg_error "RootFS entry missing in container config. See $LOGFILE"
+ exit 216
+ }
+
+ msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
+}
+
+# ------------------------------------------------------------------------------
+# description()
+#
+# - Sets container description with HTML content (logo, links, badges)
+# - Restarts ping-instances.service if present
+# - Posts status "done" to API
+# ------------------------------------------------------------------------------
+description() {
+ IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+
+ # Generate LXC Description
+ DESCRIPTION=$(
+ cat <
+
+
+
+
+ ${APP} LXC
+
+
+
+
+
+
+
+
+
+ GitHub
+
+
+
+ Discussions
+
+
+
+ Issues
+
+
+EOF
+ )
+ pct set "$CTID" -description "$DESCRIPTION"
+
+ if [[ -f /etc/systemd/system/ping-instances.service ]]; then
+ systemctl start ping-instances.service
+ fi
+
+ post_update_to_api "done" "none"
+}
+
+# ------------------------------------------------------------------------------
+# api_exit_script()
+#
+# - Exit trap handler
+# - Reports exit codes to API with detailed reason
+# - Handles known codes (100–209) and maps them to errors
+# ------------------------------------------------------------------------------
+api_exit_script() {
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then
+ case $exit_code in
+ 100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;;
+ 101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;;
+ 200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;;
+ 201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;;
+ 202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;;
+ 203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;;
+ 204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;;
+ 205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;;
+ 206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;;
+ 207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;;
+ 208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;;
+ 209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;;
+ *) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;;
+ esac
+ fi
+}
+
+if command -v pveversion >/dev/null 2>&1; then
+ trap 'api_exit_script' EXIT
+fi
+trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
+trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
+trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
diff --git a/misc/deferred/build.func.backup-refactoring-20251029-125644 b/misc/deferred/build.func.backup-refactoring-20251029-125644
new file mode 100644
index 000000000..d452f4637
--- /dev/null
+++ b/misc/deferred/build.func.backup-refactoring-20251029-125644
@@ -0,0 +1,3517 @@
+#!/usr/bin/env bash
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# Revision: 1
+
+# ==============================================================================
+# SECTION 1: CORE INITIALIZATION & VARIABLES
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# variables()
+#
+# - Normalize application name (NSAPP = lowercase, no spaces)
+# - Build installer filename (var_install)
+# - Define regex for integer validation
+# - Fetch hostname of Proxmox node
+# - Set default values for diagnostics/method
+# - Generate random UUID for tracking
+# - Get Proxmox VE version and kernel version
+# ------------------------------------------------------------------------------
+variables() {
+ NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
+ var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
+ INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
+ PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
+ DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
+ METHOD="default" # sets the METHOD variable to "default", used for the API call.
+ RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
+ CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
+ #CT_TYPE=${var_unprivileged:-$CT_TYPE}
+
+ # Get Proxmox VE version and kernel version
+ if command -v pveversion >/dev/null 2>&1; then
+ PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1)
+ else
+ PVEVERSION="N/A"
+ fi
+ KERNEL_VERSION=$(uname -r)
+}
+
+# -----------------------------------------------------------------------------
+# Community-Scripts bootstrap loader
+# - Always sources build.func from remote
+# - Updates local core files only if build.func changed
+# - Local cache: /usr/local/community-scripts/core
+# -----------------------------------------------------------------------------
+
+# FUNC_DIR="/usr/local/community-scripts/core"
+# mkdir -p "$FUNC_DIR"
+
+# BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func"
+# BUILD_REV="$FUNC_DIR/build.rev"
+# DEVMODE="${DEVMODE:-no}"
+
+# # --- Step 1: fetch build.func content once, compute hash ---
+# build_content="$(curl -fsSL "$BUILD_URL")" || {
+# echo "❌ Failed to fetch build.func"
+# exit 1
+# }
+
+# newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}')
+# oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "")
+
+# # --- Step 2: if build.func changed, offer update for core files ---
+# if [ "$newhash" != "$oldhash" ]; then
+# echo "⚠️ build.func changed!"
+
+# while true; do
+# read -rp "Refresh local core files? [y/N/diff]: " ans
+# case "$ans" in
+# [Yy]*)
+# echo "$newhash" >"$BUILD_REV"
+
+# update_func_file() {
+# local file="$1"
+# local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file"
+# local local_path="$FUNC_DIR/$file"
+
+# echo "⬇️ Downloading $file ..."
+# curl -fsSL "$url" -o "$local_path" || {
+# echo "❌ Failed to fetch $file"
+# exit 1
+# }
+# echo "✔️ Updated $file"
+# }
+
+# update_func_file core.func
+# update_func_file error_handler.func
+# update_func_file tools.func
+# break
+# ;;
+# [Dd]*)
+# for file in core.func error_handler.func tools.func; do
+# local_path="$FUNC_DIR/$file"
+# url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file"
+# remote_tmp="$(mktemp)"
+
+# curl -fsSL "$url" -o "$remote_tmp" || continue
+
+# if [ -f "$local_path" ]; then
+# echo "🔍 Diff for $file:"
+# diff -u "$local_path" "$remote_tmp" || echo "(no differences)"
+# else
+# echo "📦 New file $file will be installed"
+# fi
+
+# rm -f "$remote_tmp"
+# done
+# ;;
+# *)
+# echo "❌ Skipped updating local core files"
+# break
+# ;;
+# esac
+# done
+# else
+# if [ "$DEVMODE" != "yes" ]; then
+# echo "✔️ build.func unchanged → using existing local core files"
+# fi
+# fi
+
+# if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then
+# return 0 2>/dev/null || exit 0
+# fi
+# _COMMUNITY_SCRIPTS_LOADER=1
+
+# # --- Step 3: always source local versions of the core files ---
+# source "$FUNC_DIR/core.func"
+# source "$FUNC_DIR/error_handler.func"
+# source "$FUNC_DIR/tools.func"
+
+# # --- Step 4: finally, source build.func directly from memory ---
+# # (no tmp file needed)
+# source <(printf "%s" "$build_content")
+
+# ------------------------------------------------------------------------------
+# Load core + error handler functions from community-scripts repo
+#
+# - Prefer curl if available, fallback to wget
+# - Load: core.func, error_handler.func, api.func
+# - Initialize error traps after loading
+# ------------------------------------------------------------------------------
+
+source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func)
+
+if command -v curl >/dev/null 2>&1; then
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via curl"
+elif command -v wget >/dev/null 2>&1; then
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
+ source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
+ load_functions
+ catch_errors
+ #echo "(build.func) Loaded core.func via wget"
+fi
+
+# ------------------------------------------------------------------------------
+# maxkeys_check()
+#
+# - Reads kernel keyring limits (maxkeys, maxbytes)
+# - Checks current usage for LXC user (UID 100000)
+# - Warns if usage is close to limits and suggests sysctl tuning
+# - Exits if thresholds are exceeded
+# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
+# ------------------------------------------------------------------------------
+
+maxkeys_check() {
+ # Read kernel parameters
+ per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
+ per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
+
+ # Exit if kernel parameters are unavailable
+ if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
+ echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
+ exit 1
+ fi
+
+ # Fetch key usage for user ID 100000 (typical for containers)
+ used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
+ used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
+
+ # Calculate thresholds and suggested new limits
+ threshold_keys=$((per_user_maxkeys - 100))
+ threshold_bytes=$((per_user_maxbytes - 1000))
+ new_limit_keys=$((per_user_maxkeys * 2))
+ new_limit_bytes=$((per_user_maxbytes * 2))
+
+ # Check if key or byte usage is near limits
+ failure=0
+ if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+ if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
+ echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
+ echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
+ failure=1
+ fi
+
+ # Provide next steps if issues are detected
+ if [[ "$failure" -eq 1 ]]; then
+ echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
+ exit 1
+ fi
+
+ echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
+}
+
+# ------------------------------------------------------------------------------
+# get_current_ip()
+#
+# - Returns current container IP depending on OS type
+# - Debian/Ubuntu: uses `hostname -I`
+# - Alpine: parses eth0 via `ip -4 addr`
+# ------------------------------------------------------------------------------
+get_current_ip() {
+ if [ -f /etc/os-release ]; then
+ # Check for Debian/Ubuntu (uses hostname -I)
+ if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
+ CURRENT_IP=$(hostname -I | awk '{print $1}')
+ # Check for Alpine (uses ip command)
+ elif grep -q 'ID=alpine' /etc/os-release; then
+ CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
+ else
+ CURRENT_IP="Unknown"
+ fi
+ fi
+ echo "$CURRENT_IP"
+}
+
+# ------------------------------------------------------------------------------
+# update_motd_ip()
+#
+# - Updates /etc/motd with current container IP
+# - Removes old IP entries to avoid duplicates
+# ------------------------------------------------------------------------------
+update_motd_ip() {
+ MOTD_FILE="/etc/motd"
+
+ if [ -f "$MOTD_FILE" ]; then
+ # Remove existing IP Address lines to prevent duplication
+ sed -i '/IP Address:/d' "$MOTD_FILE"
+
+ IP=$(get_current_ip)
+ # Add the new IP address
+ echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# install_ssh_keys_into_ct()
+#
+# - Installs SSH keys into container root account if SSH is enabled
+# - Uses pct push or direct input to authorized_keys
+# - Falls back to warning if no keys provided
+# ------------------------------------------------------------------------------
+install_ssh_keys_into_ct() {
+ [[ "$SSH" != "yes" ]] && return 0
+
+ if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
+ 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
+ }
+ 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
+ }
+ pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
+ msg_ok "Installed SSH keys into CT ${CTID}"
+ return 0
+ fi
+
+ # Fallback: nichts ausgewählt
+ msg_warn "No SSH keys to install (skipping)."
+ return 0
+}
+
+# ------------------------------------------------------------------------------
+# base_settings()
+#
+# - Defines all base/default variables for container creation
+# - Reads from environment variables (var_*)
+# - Provides fallback defaults for OS type/version
+# ------------------------------------------------------------------------------
+base_settings() {
+ # Default Settings
+ CT_TYPE=${var_unprivileged:-"1"}
+ DISK_SIZE=${var_disk:-"4"}
+ CORE_COUNT=${var_cpu:-"1"}
+ RAM_SIZE=${var_ram:-"1024"}
+ VERBOSE=${var_verbose:-"${1:-no}"}
+ PW=${var_pw:-""}
+ CT_ID=${var_ctid:-$NEXTID}
+ HN=${var_hostname:-$NSAPP}
+ BRG=${var_brg:-"vmbr0"}
+ NET=${var_net:-"dhcp"}
+ IPV6_METHOD=${var_ipv6_method:-"none"}
+ IPV6_STATIC=${var_ipv6_static:-""}
+ GATE=${var_gateway:-""}
+ APT_CACHER=${var_apt_cacher:-""}
+ APT_CACHER_IP=${var_apt_cacher_ip:-""}
+ MTU=${var_mtu:-""}
+ SD=${var_storage:-""}
+ NS=${var_ns:-""}
+ MAC=${var_mac:-""}
+ VLAN=${var_vlan:-""}
+ SSH=${var_ssh:-"no"}
+ SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
+ UDHCPC_FIX=${var_udhcpc_fix:-""}
+ TAGS="community-script,${var_tags:-}"
+ ENABLE_FUSE=${var_fuse:-"${1:-no}"}
+ ENABLE_TUN=${var_tun:-"${1:-no}"}
+
+ # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
+ if [ -z "$var_os" ]; then
+ var_os="debian"
+ fi
+ if [ -z "$var_version" ]; then
+ var_version="12"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# echo_default()
+#
+# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
+# - Uses icons and formatting for readability
+# - Convert CT_TYPE to description
+# ------------------------------------------------------------------------------
+echo_default() {
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ if [ "$VERBOSE" == "yes" ]; then
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
+ fi
+ echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
+ echo -e " "
+}
+
+# ------------------------------------------------------------------------------
+# exit_script()
+#
+# - Called when user cancels an action
+# - Clears screen and exits gracefully
+# ------------------------------------------------------------------------------
+exit_script() {
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
+}
+
+# ------------------------------------------------------------------------------
+# find_host_ssh_keys()
+#
+# - Scans system for available SSH keys
+# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
+# - Returns list of files containing valid SSH public keys
+# - Sets FOUND_HOST_KEY_COUNT to number of keys found
+# ------------------------------------------------------------------------------
+find_host_ssh_keys() {
+ local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
+ local -a files=() cand=()
+ local g="${var_ssh_import_glob:-}"
+ local total=0 f base c
+
+ shopt -s nullglob
+ if [[ -n "$g" ]]; then
+ for pat in $g; do cand+=($pat); done
+ else
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ fi
+ shopt -u nullglob
+
+ for f in "${cand[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # CRLF safe check for host keys
+ c=$(tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ {print}
+ ' | grep -E -c '"$re"' || true)
+
+ if ((c > 0)); then
+ files+=("$f")
+ total=$((total + c))
+ fi
+ done
+
+ # Fallback to /root/.ssh/authorized_keys
+ if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
+ if grep -E -q "$re" /root/.ssh/authorized_keys; then
+ files+=(/root/.ssh/authorized_keys)
+ total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
+ fi
+ fi
+
+ FOUND_HOST_KEY_COUNT="$total"
+ (
+ IFS=:
+ echo "${files[*]}"
+ )
+}
+
+# ------------------------------------------------------------------------------
+# advanced_settings()
+#
+# - Interactive whiptail menu for advanced configuration
+# - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM
+# - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode
+# - Ends with confirmation or re-entry if cancelled
+# ------------------------------------------------------------------------------
+advanced_settings() {
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
+ # Setting Default Tag for Advanced Settings
+ TAGS="community-script;${var_tags:-}"
+ CT_DEFAULT_TYPE="${CT_TYPE}"
+ CT_TYPE=""
+ while [ -z "$CT_TYPE" ]; do
+ if [ "$CT_DEFAULT_TYPE" == "1" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" ON \
+ "0" "Privileged" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os |${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ if [ "$CT_DEFAULT_TYPE" == "0" ]; then
+ if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
+ "1" "Unprivileged" OFF \
+ "0" "Privileged" ON \
+ 3>&1 1>&2 2>&3); then
+ if [ -n "$CT_TYPE" ]; then
+ CT_TYPE_DESC="Unprivileged"
+ if [ "$CT_TYPE" -eq 0 ]; then
+ CT_TYPE_DESC="Privileged"
+ fi
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
+ echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
+ fi
+ else
+ exit_script
+ fi
+ fi
+ done
+
+ while true; do
+ if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
+ # Empty = Autologin
+ if [[ -z "$PW1" ]]; then
+ PW=""
+ PW1="Automatic Login"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
+ break
+ fi
+
+ # Invalid: contains spaces
+ if [[ "$PW1" == *" "* ]]; then
+ whiptail --msgbox "Password cannot contain spaces." 8 58
+ continue
+ fi
+
+ # Invalid: too short
+ if ((${#PW1} < 5)); then
+ whiptail --msgbox "Password must be at least 5 characters." 8 58
+ continue
+ fi
+
+ # Confirm password
+ if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
+ if [[ "$PW1" == "$PW2" ]]; then
+ PW="-password $PW1"
+ echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
+ break
+ else
+ whiptail --msgbox "Passwords do not match. Please try again." 8 58
+ fi
+ else
+ exit_script
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_ID" ]; then
+ CT_ID="$NEXTID"
+ fi
+ else
+ exit_script
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
+
+ while true; do
+ if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
+ if [ -z "$CT_NAME" ]; then
+ HN="$NSAPP"
+ else
+ HN=$(echo "${CT_NAME,,}" | tr -d ' ')
+ fi
+ # Hostname validate (RFC 1123)
+ if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70
+ fi
+ else
+ exit_script
+ fi
+ done
+
+ while true; do
+ DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$DISK_SIZE" ]; then
+ DISK_SIZE="$var_disk"
+ fi
+
+ if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
+ break
+ else
+ whiptail --msgbox "Disk size must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$CORE_COUNT" ]; then
+ CORE_COUNT="$var_cpu"
+ fi
+
+ if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ break
+ else
+ whiptail --msgbox "CPU core count must be a positive integer!" 8 58
+ fi
+ done
+
+ while true; do
+ RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script
+
+ if [ -z "$RAM_SIZE" ]; then
+ RAM_SIZE="$var_ram"
+ fi
+
+ if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
+ break
+ else
+ whiptail --msgbox "RAM size must be a positive integer!" 8 58
+ fi
+ done
+
+ IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
+ BRIDGES=""
+ OLD_IFS=$IFS
+ IFS=$'\n'
+ for iface_filepath in ${IFACE_FILEPATH_LIST}; do
+
+ iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
+ (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
+
+ if [ -f "${iface_indexes_tmpfile}" ]; then
+
+ while read -r pair; do
+ start=$(echo "${pair}" | cut -d':' -f1)
+ end=$(echo "${pair}" | cut -d':' -f2)
+
+ if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
+ iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
+ BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
+ fi
+
+ done <"${iface_indexes_tmpfile}"
+ rm -f "${iface_indexes_tmpfile}"
+ fi
+
+ done
+ IFS=$OLD_IFS
+ BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
+ if [[ -z "$BRIDGES" ]]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ # Build bridge menu with descriptions
+ BRIDGE_MENU_OPTIONS=()
+ while IFS= read -r bridge; do
+ if [[ -n "$bridge" ]]; then
+ # Get description from Proxmox built-in method - find comment for this specific bridge
+ description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//')
+ if [[ -n "$description" ]]; then
+ BRIDGE_MENU_OPTIONS+=("$bridge" "${description}")
+ else
+ BRIDGE_MENU_OPTIONS+=("$bridge" " ")
+ fi
+ fi
+ done <<<"$BRIDGES"
+
+ BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3)
+ if [[ -z "$BRG" ]]; then
+ exit_script
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
+ fi
+
+ # IPv4 methods: dhcp, static, none
+ while true; do
+ IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "IPv4 Address Management" \
+ --menu "Select IPv4 Address Assignment Method:" 12 60 2 \
+ "dhcp" "Automatic (DHCP, recommended)" \
+ "static" "Static (manual entry)" \
+ 3>&1 1>&2 2>&3)
+
+ exit_status=$?
+ if [ $exit_status -ne 0 ]; then
+ exit_script
+ fi
+
+ case "$IPV4_METHOD" in
+ dhcp)
+ NET="dhcp"
+ GATE=""
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}"
+ break
+ ;;
+ static)
+ # Static: call and validate CIDR address
+ while true; do
+ NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \
+ --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3)
+ if [ -z "$NET" ]; then
+ whiptail --msgbox "IPv4 address must not be empty." 8 58
+ continue
+ elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}"
+ break
+ else
+ whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58
+ fi
+ done
+
+ # call and validate Gateway
+ while true; do
+ GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \
+ --title "Gateway IP" 3>&1 1>&2 2>&3)
+ if [ -z "$GATE1" ]; then
+ whiptail --msgbox "Gateway IP address cannot be empty." 8 58
+ elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
+ whiptail --msgbox "Invalid Gateway IP address format." 8 58
+ else
+ GATE=",gw=$GATE1"
+ echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
+ break
+ fi
+ done
+ break
+ ;;
+ esac
+ done
+
+ # IPv6 Address Management selection
+ while true; do
+ IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \
+ "Select IPv6 Address Management Type:" 15 58 4 \
+ "auto" "SLAAC/AUTO (recommended, default)" \
+ "dhcp" "DHCPv6" \
+ "static" "Static (manual entry)" \
+ "none" "Disabled" \
+ --default-item "auto" 3>&1 1>&2 2>&3)
+ [ $? -ne 0 ] && exit_script
+
+ case "$IPV6_METHOD" in
+ auto)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}"
+ IPV6_ADDR=""
+ IPV6_GATE=""
+ break
+ ;;
+ dhcp)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}"
+ IPV6_ADDR="dhcp"
+ IPV6_GATE=""
+ break
+ ;;
+ static)
+ # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64)
+ while true; do
+ IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \
+ --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script
+ if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}"
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58
+ fi
+ done
+ # Optional: ask for IPv6 gateway for static config
+ while true; do
+ IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
+ "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3)
+ if [ -z "$IPV6_GATE" ]; then
+ IPV6_GATE=""
+ break
+ elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then
+ break
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
+ "Invalid IPv6 gateway format." 8 58
+ fi
+ done
+ break
+ ;;
+ none)
+ echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}"
+ IPV6_ADDR="none"
+ IPV6_GATE=""
+ break
+ ;;
+ *)
+ exit_script
+ ;;
+ esac
+ done
+
+ if [ "$var_os" == "alpine" ]; then
+ APT_CACHER=""
+ APT_CACHER_IP=""
+ else
+ if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
+ APT_CACHER="${APT_CACHER_IP:+yes}"
+ echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
+ else
+ exit_script
+ fi
+ fi
+
+ # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then
+ # DISABLEIP6="yes"
+ # else
+ # DISABLEIP6="no"
+ # fi
+ # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}"
+
+ if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
+ if [ -z "$MTU1" ]; then
+ MTU1="Default"
+ MTU=""
+ else
+ MTU=",mtu=$MTU1"
+ fi
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ exit_script
+ fi
+
+ if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
+ if [ -z "$SD" ]; then
+ SX=Host
+ SD=""
+ else
+ SX=$SD
+ SD="-searchdomain=$SD"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
+ else
+ exit_script
+ fi
+
+ if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
+ if [ -z "$NX" ]; then
+ NX=Host
+ NS=""
+ else
+ NS="-nameserver=$NX"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
+ else
+ exit_script
+ fi
+
+ if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then
+ UDHCPC_FIX="yes"
+ else
+ UDHCPC_FIX="no"
+ fi
+ export UDHCPC_FIX
+
+ if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
+ if [ -z "$MAC1" ]; then
+ MAC1="Default"
+ MAC=""
+ else
+ MAC=",hwaddr=$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
+ else
+ exit_script
+ fi
+
+ if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
+ if [ -z "$VLAN1" ]; then
+ VLAN1="Default"
+ VLAN=""
+ else
+ VLAN=",tag=$VLAN1"
+ fi
+ echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
+ else
+ exit_script
+ fi
+
+ if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
+ if [ -n "${ADV_TAGS}" ]; then
+ ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
+ TAGS="${ADV_TAGS}"
+ else
+ TAGS=";"
+ fi
+ echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
+ else
+ exit_script
+ fi
+
+ configure_ssh_settings
+ export SSH_KEYS_FILE
+ echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
+ ENABLE_FUSE="yes"
+ else
+ ENABLE_FUSE="no"
+ fi
+ echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
+ VERBOSE="yes"
+ else
+ VERBOSE="no"
+ fi
+ echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
+
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
+ echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
+ else
+ clear
+ header_info
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
+ advanced_settings
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# diagnostics_check()
+#
+# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
+# - Asks user whether to send anonymous diagnostic data
+# - Saves DIAGNOSTICS=yes/no in the config file
+# ------------------------------------------------------------------------------
+diagnostics_check() {
+ if ! [ -d "/usr/local/community-scripts" ]; then
+ mkdir -p /usr/local/community-scripts
+ fi
+
+ if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=yes
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="yes"
+ else
+ cat </usr/local/community-scripts/diagnostics
+DIAGNOSTICS=no
+
+#This file is used to store the diagnostics settings for the Community-Scripts API.
+#https://github.com/community-scripts/ProxmoxVED/discussions/1836
+#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
+#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
+#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will disable the diagnostics feature.
+#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
+#This will enable the diagnostics feature.
+#The following information will be sent:
+#"disk_size"
+#"core_count"
+#"ram_size"
+#"os_type"
+#"os_version"
+#"nsapp"
+#"method"
+#"pve_version"
+#"status"
+#If you have any concerns, please review the source code at /misc/build.func
+EOF
+ DIAGNOSTICS="no"
+ fi
+ else
+ DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
+
+ fi
+
+}
+
+# ------------------------------------------------------------------------------
+# default_var_settings
+#
+# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
+# - Loads var_* values from default.vars (safe parser, no source/eval)
+# - Precedence: ENV var_* > default.vars > built-in defaults
+# - Maps var_verbose → VERBOSE
+# - Calls base_settings "$VERBOSE" and echo_default
+# ------------------------------------------------------------------------------
+default_var_settings() {
+ # Allowed var_* keys (alphabetically sorted)
+ local VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+
+ # Snapshot: environment variables (highest precedence)
+ declare -A _HARD_ENV=()
+ local _k
+ for _k in "${VAR_WHITELIST[@]}"; do
+ if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
+ done
+
+ # Find default.vars location
+ local _find_default_vars
+ _find_default_vars() {
+ local f
+ for f in \
+ /usr/local/community-scripts/default.vars \
+ "$HOME/.config/community-scripts/default.vars" \
+ "./default.vars"; do
+ [ -f "$f" ] && {
+ echo "$f"
+ return 0
+ }
+ done
+ return 1
+ }
+ # Allow override of storages via env (for non-interactive use cases)
+ [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
+ [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
+
+ # Create once, with storages already selected, no var_ctid/var_hostname lines
+ local _ensure_default_vars
+ _ensure_default_vars() {
+ _find_default_vars >/dev/null 2>&1 && return 0
+
+ local canonical="/usr/local/community-scripts/default.vars"
+ msg_info "No default.vars found. Creating ${canonical}"
+ mkdir -p /usr/local/community-scripts
+
+ # Pick storages before writing the file (always ask unless only one)
+ # Create a minimal temp file to write into
+ : >"$canonical"
+
+ # Base content (no var_ctid / var_hostname here)
+ cat >"$canonical" <<'EOF'
+# Community-Scripts defaults (var_* only). Lines starting with # are comments.
+# Precedence: ENV var_* > default.vars > built-ins.
+# Keep keys alphabetically sorted.
+
+# Container type
+var_unprivileged=1
+
+# Resources
+var_cpu=1
+var_disk=4
+var_ram=1024
+
+# Network
+var_brg=vmbr0
+var_net=dhcp
+var_ipv6_method=none
+# var_gateway=
+# var_ipv6_static=
+# var_vlan=
+# var_mtu=
+# var_mac=
+# var_ns=
+
+# SSH
+var_ssh=no
+# var_ssh_authorized_key=
+
+# APT cacher (optional)
+# var_apt_cacher=yes
+# var_apt_cacher_ip=192.168.1.10
+
+# Features/Tags/verbosity
+var_fuse=no
+var_tun=no
+var_tags=community-script
+var_verbose=no
+
+# Security (root PW) – empty => autologin
+# var_pw=
+EOF
+
+ # Now choose storages (always prompt unless just one exists)
+ choose_and_set_storage_for_file "$canonical" template
+ choose_and_set_storage_for_file "$canonical" container
+
+ chmod 0644 "$canonical"
+ msg_ok "Created ${canonical}"
+ }
+
+ # Whitelist check
+ local _is_whitelisted_key
+ _is_whitelisted_key() {
+ local k="$1"
+ local w
+ for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
+ return 1
+ }
+
+ # Safe parser for KEY=VALUE lines
+ local _load_vars_file
+ _load_vars_file() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ msg_info "Loading defaults from ${file}"
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [[ -z "$line" || "$line" == \#* ]] && continue
+ if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
+ local var_key="${BASH_REMATCH[1]}"
+ local var_val="${BASH_REMATCH[2]}"
+
+ [[ "$var_key" != var_* ]] && continue
+ _is_whitelisted_key "$var_key" || {
+ msg_debug "Ignore non-whitelisted ${var_key}"
+ continue
+ }
+
+ # Strip quotes
+ if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
+ var_val="${BASH_REMATCH[1]}"
+ fi
+
+ # Unsafe characters
+ case $var_val in
+ \"*\")
+ var_val=${var_val#\"}
+ var_val=${var_val%\"}
+ ;;
+ \'*\')
+ var_val=${var_val#\'}
+ var_val=${var_val%\'}
+ ;;
+ esac # Hard env wins
+ [[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue
+ # Set only if not already exported
+ [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
+ else
+ msg_warn "Malformed line in ${file}: ${line}"
+ fi
+ done <"$file"
+ msg_ok "Loaded ${file}"
+ }
+
+ # 1) Ensure file exists
+ _ensure_default_vars
+
+ # 2) Load file
+ local dv
+ dv="$(_find_default_vars)" || {
+ msg_error "default.vars not found after ensure step"
+ return 1
+ }
+ _load_vars_file "$dv"
+
+ # 3) Map var_verbose → VERBOSE
+ if [[ -n "${var_verbose:-}" ]]; then
+ case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
+ else
+ VERBOSE="no"
+ fi
+
+ # 4) Apply base settings and show summary
+ METHOD="mydefaults-global"
+ base_settings "$VERBOSE"
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
+ echo_default
+}
+
+# ------------------------------------------------------------------------------
+# get_app_defaults_path()
+#
+# - Returns full path for app-specific defaults file
+# - Example: /usr/local/community-scripts/defaults/.vars
+# ------------------------------------------------------------------------------
+
+get_app_defaults_path() {
+ local n="${NSAPP:-${APP,,}}"
+ echo "/usr/local/community-scripts/defaults/${n}.vars"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults
+#
+# - Called after advanced_settings returned with fully chosen values.
+# - If no .vars exists, offers to persist current advanced settings
+# into /usr/local/community-scripts/defaults/.vars
+# - Only writes whitelisted var_* keys.
+# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
+# ------------------------------------------------------------------------------
+if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
+ declare -ag VAR_WHITELIST=(
+ var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
+ var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
+ var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
+ var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
+ )
+fi
+
+# Note: _is_whitelisted_key() is defined above in default_var_settings section
+
+_sanitize_value() {
+ # Disallow Command-Substitution / Shell-Meta
+ case "$1" in
+ *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
+ echo ""
+ return 0
+ ;;
+ esac
+ echo "$1"
+}
+
+# Map-Parser: read var_* from file into _VARS_IN associative array
+# Note: Main _load_vars_file() with full validation is defined in default_var_settings section
+# This simplified version is used specifically for diff operations via _VARS_IN array
+declare -A _VARS_IN
+_load_vars_file_to_map() {
+ local file="$1"
+ [ -f "$file" ] || return 0
+ _VARS_IN=() # Clear array
+ local line key val
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line#"${line%%[![:space:]]*}"}"
+ line="${line%"${line##*[![:space:]]}"}"
+ [ -z "$line" ] && continue
+ case "$line" in
+ \#*) continue ;;
+ esac
+ key=$(printf "%s" "$line" | cut -d= -f1)
+ val=$(printf "%s" "$line" | cut -d= -f2-)
+ case "$key" in
+ var_*)
+ if _is_whitelisted_key "$key"; then
+ _VARS_IN["$key"]="$val"
+ fi
+ ;;
+ esac
+ done <"$file"
+}
+
+# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
+_build_vars_diff() {
+ local oldf="$1" newf="$2"
+ local k
+ local -A OLD=() NEW=()
+ _load_vars_file_to_map "$oldf"
+ for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
+ _load_vars_file_to_map "$newf"
+ for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
+
+ local out
+ out+="# Diff for ${APP} (${NSAPP})\n"
+ out+="# Old: ${oldf}\n# New: ${newf}\n\n"
+
+ local found_change=0
+
+ # Changed & Removed
+ for k in "${!OLD[@]}"; do
+ if [[ -v NEW["$k"] ]]; then
+ if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
+ out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ else
+ out+="- ${k}\n - old: ${OLD[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ # Added
+ for k in "${!NEW[@]}"; do
+ if [[ ! -v OLD["$k"] ]]; then
+ out+="+ ${k}\n + new: ${NEW[$k]}\n"
+ found_change=1
+ fi
+ done
+
+ if [[ $found_change -eq 0 ]]; then
+ out+="(No differences)\n"
+ fi
+
+ printf "%b" "$out"
+}
+
+# Build a temporary .vars file from current advanced settings
+_build_current_app_vars_tmp() {
+ tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
+
+ # NET/GW
+ _net="${NET:-}"
+ _gate=""
+ case "${GATE:-}" in
+ ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
+ esac
+
+ # IPv6
+ _ipv6_method="${IPV6_METHOD:-auto}"
+ _ipv6_static=""
+ _ipv6_gateway=""
+ if [ "$_ipv6_method" = "static" ]; then
+ _ipv6_static="${IPV6_ADDR:-}"
+ _ipv6_gateway="${IPV6_GATE:-}"
+ fi
+
+ # MTU/VLAN/MAC
+ _mtu=""
+ _vlan=""
+ _mac=""
+ case "${MTU:-}" in
+ ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
+ esac
+ case "${VLAN:-}" in
+ ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
+ esac
+ case "${MAC:-}" in
+ ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
+ esac
+
+ # DNS / Searchdomain
+ _ns=""
+ _searchdomain=""
+ case "${NS:-}" in
+ -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
+ esac
+ case "${SD:-}" in
+ -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
+ esac
+
+ # SSH / APT / Features
+ _ssh="${SSH:-no}"
+ _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
+ _apt_cacher="${APT_CACHER:-}"
+ _apt_cacher_ip="${APT_CACHER_IP:-}"
+ _fuse="${ENABLE_FUSE:-no}"
+ _tun="${ENABLE_TUN:-no}"
+ _tags="${TAGS:-}"
+ _verbose="${VERBOSE:-no}"
+
+ # Type / Resources / Identity
+ _unpriv="${CT_TYPE:-1}"
+ _cpu="${CORE_COUNT:-1}"
+ _ram="${RAM_SIZE:-1024}"
+ _disk="${DISK_SIZE:-4}"
+ _hostname="${HN:-$NSAPP}"
+
+ # Storage
+ _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
+ _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
+
+ {
+ echo "# App-specific defaults for ${APP} (${NSAPP})"
+ echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo
+
+ echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
+ echo "var_cpu=$(_sanitize_value "$_cpu")"
+ echo "var_ram=$(_sanitize_value "$_ram")"
+ echo "var_disk=$(_sanitize_value "$_disk")"
+
+ [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
+ [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
+ [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
+ [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
+ [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
+ [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
+ [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
+
+ [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
+ [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")"
+
+ [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
+ [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
+
+ [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
+ [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
+
+ [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
+ [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
+ [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
+ [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
+
+ [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
+ [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
+
+ [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
+ [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
+ } >"$tmpf"
+
+ echo "$tmpf"
+}
+
+# ------------------------------------------------------------------------------
+# maybe_offer_save_app_defaults()
+#
+# - Called after advanced_settings()
+# - Offers to save current values as app defaults if not existing
+# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
+# ------------------------------------------------------------------------------
+maybe_offer_save_app_defaults() {
+ local app_vars_path
+ app_vars_path="$(get_app_defaults_path)"
+
+ # always build from current settings
+ local new_tmp diff_tmp
+ new_tmp="$(_build_current_app_vars_tmp)"
+ diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
+
+ # 1) if no file → offer to create
+ if [[ ! -f "$app_vars_path" ]]; then
+ if whiptail --backtitle "[dev] 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"
+ msg_ok "Saved app defaults: ${app_vars_path}"
+ fi
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 2) if file exists → build diff
+ _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
+
+ # if no differences → do nothing
+ if grep -q "^(No differences)$" "$diff_tmp"; then
+ rm -f "$new_tmp" "$diff_tmp"
+ return 0
+ fi
+
+ # 3) if file exists → show menu with default selection "Update Defaults"
+ local app_vars_file
+ app_vars_file="$(basename "$app_vars_path")"
+
+ while true; do
+ local sel
+ sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "APP DEFAULTS – ${APP}" \
+ --menu "Differences detected. What do you want to do?" 20 78 10 \
+ "Update Defaults" "Write new values to ${app_vars_file}" \
+ "Keep Current" "Keep existing defaults (no changes)" \
+ "View Diff" "Show a detailed diff" \
+ "Cancel" "Abort without changes" \
+ --default-item "Update Defaults" \
+ 3>&1 1>&2 2>&3)" || { sel="Cancel"; }
+
+ case "$sel" in
+ "Update Defaults")
+ install -m 0644 "$new_tmp" "$app_vars_path"
+ msg_ok "Updated app defaults: ${app_vars_path}"
+ break
+ ;;
+ "Keep Current")
+ msg_info "Keeping current app defaults: ${app_vars_path}"
+ break
+ ;;
+ "View Diff")
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Diff – ${APP}" \
+ --scrolltext --textbox "$diff_tmp" 25 100
+ ;;
+ "Cancel" | *)
+ msg_info "Canceled. No changes to app defaults."
+ break
+ ;;
+ esac
+ done
+
+ rm -f "$new_tmp" "$diff_tmp"
+}
+
+ensure_storage_selection_for_vars_file() {
+ local vf="$1"
+
+ # 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-)
+
+ if [[ -n "$tpl" && -n "$ct" ]]; then
+ TEMPLATE_STORAGE="$tpl"
+ CONTAINER_STORAGE="$ct"
+ return 0
+ fi
+
+ choose_and_set_storage_for_file "$vf" template
+ choose_and_set_storage_for_file "$vf" container
+
+ msg_ok "Storage configuration saved to $(basename "$vf")"
+}
+
+diagnostics_menu() {
+ if [ "${DIAGNOSTICS:-no}" = "yes" ]; then
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "No" --no-button "Back"; then
+ DIAGNOSTICS="no"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ else
+ if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "DIAGNOSTIC SETTINGS" \
+ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
+ --yes-button "Yes" --no-button "Back"; then
+ DIAGNOSTICS="yes"
+ sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
+ whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
+ fi
+ fi
+}
+
+ensure_global_default_vars_file() {
+ local vars_path="/usr/local/community-scripts/default.vars"
+ if [[ ! -f "$vars_path" ]]; then
+ mkdir -p "$(dirname "$vars_path")"
+ touch "$vars_path"
+ fi
+ echo "$vars_path"
+}
+
+# ------------------------------------------------------------------------------
+# install_script()
+#
+# - Main entrypoint for installation mode
+# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
+# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
+# - Applies chosen settings and triggers container build
+# ------------------------------------------------------------------------------
+install_script() {
+ pve_check
+ shell_check
+ root_check
+ arch_check
+ ssh_check
+ maxkeys_check
+ diagnostics_check
+
+ if systemctl is-active -q ping-instances.service; then
+ systemctl -q stop ping-instances.service
+ fi
+
+ NEXTID=$(pvesh get /cluster/nextid)
+ timezone=$(cat /etc/timezone)
+
+ # Show APP Header
+ header_info
+
+ # --- Support CLI argument as direct preset (default, advanced, …) ---
+ CHOICE="${mode:-${1:-}}"
+
+ # If no CLI argument → show whiptail menu
+ # Build menu dynamically based on available options
+ local appdefaults_option=""
+ local settings_option=""
+ local menu_items=(
+ "1" "Default Install"
+ "2" "Advanced Install"
+ "3" "My Defaults"
+ )
+
+ if [ -f "$(get_app_defaults_path)" ]; then
+ appdefaults_option="4"
+ menu_items+=("4" "App Defaults for ${APP}")
+ settings_option="5"
+ menu_items+=("5" "Settings")
+ else
+ settings_option="4"
+ menu_items+=("4" "Settings")
+ fi
+
+ if [ -z "$CHOICE" ]; then
+
+ TMP_CHOICE=$(whiptail \
+ --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts Options" \
+ --ok-button "Select" --cancel-button "Exit Script" \
+ --notags \
+ --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
+ 20 60 9 \
+ "${menu_items[@]}" \
+ --default-item "1" \
+ 3>&1 1>&2 2>&3) || exit_script
+ CHOICE="$TMP_CHOICE"
+ fi
+
+ APPDEFAULTS_OPTION="$appdefaults_option"
+ SETTINGS_OPTION="$settings_option"
+
+ # --- Main case ---
+ local defaults_target=""
+ local run_maybe_offer="no"
+ case "$CHOICE" in
+ 1 | default | DEFAULT)
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
+ VERBOSE="no"
+ METHOD="default"
+ base_settings "$VERBOSE"
+ echo_default
+ defaults_target="$(ensure_global_default_vars_file)"
+ ;;
+ 2 | advanced | ADVANCED)
+ header_info
+
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
+ echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
+ METHOD="advanced"
+ base_settings
+ advanced_settings
+ defaults_target="$(ensure_global_default_vars_file)"
+ run_maybe_offer="yes"
+ ;;
+ 3 | mydefaults | MYDEFAULTS)
+ default_var_settings || {
+ msg_error "Failed to apply default.vars"
+ exit 1
+ }
+ defaults_target="/usr/local/community-scripts/default.vars"
+ ;;
+ "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
+ METHOD="appdefaults"
+ base_settings
+ _load_vars_file "$(get_app_defaults_path)"
+ echo_default
+ defaults_target="$(get_app_defaults_path)"
+ else
+ msg_error "No App Defaults available for ${APP}"
+ exit 1
+ fi
+ ;;
+ "$SETTINGS_OPTION" | settings | SETTINGS)
+ settings_menu
+ defaults_target=""
+ ;;
+ *)
+ echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}"
+ exit 1
+ ;;
+ esac
+
+ if [[ -n "$defaults_target" ]]; then
+ ensure_storage_selection_for_vars_file "$defaults_target"
+ fi
+
+ if [[ "$run_maybe_offer" == "yes" ]]; then
+ maybe_offer_save_app_defaults
+ fi
+}
+
+edit_default_storage() {
+ local vf="/usr/local/community-scripts/default.vars"
+
+ # Ensure file exists
+ if [[ ! -f "$vf" ]]; then
+ mkdir -p "$(dirname "$vf")"
+ touch "$vf"
+ fi
+
+ # Let ensure_storage_selection_for_vars_file handle everything
+ ensure_storage_selection_for_vars_file "$vf"
+}
+
+settings_menu() {
+ while true; do
+ local settings_items=(
+ "1" "Manage API-Diagnostic Setting"
+ "2" "Edit Default.vars"
+ "3" "Edit Default Storage"
+ )
+ if [ -f "$(get_app_defaults_path)" ]; then
+ settings_items+=("4" "Edit App.vars for ${APP}")
+ settings_items+=("5" "Exit")
+ else
+ settings_items+=("4" "Exit")
+ fi
+
+ local choice
+ choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "Community-Scripts SETTINGS Menu" \
+ --ok-button "OK" --cancel-button "Back" \
+ --menu "\n\nChoose a settings option:\n\nUse TAB or Arrow keys to navigate, ENTER to select." 20 60 9 \
+ "${settings_items[@]}" \
+ 3>&1 1>&2 2>&3) || break
+
+ case "$choice" in
+ 1) diagnostics_menu ;;
+ 2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
+ 3) edit_default_storage ;;
+ 4)
+ if [ -f "$(get_app_defaults_path)" ]; then
+ ${EDITOR:-nano} "$(get_app_defaults_path)"
+ else
+ exit_script
+ fi
+ ;;
+ 5) exit_script ;;
+ esac
+ done
+}
+
+# ===== Unified storage selection & writing to vars files =====
+_write_storage_to_vars() {
+ # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
+ local vf="$1" key="$2" val="$3"
+ # remove uncommented and commented versions to avoid duplicates
+ sed -i "/^[#[:space:]]*${key}=/d" "$vf"
+ echo "${key}=${val}" >>"$vf"
+}
+
+choose_and_set_storage_for_file() {
+ # $1 = vars_file, $2 = class ('container'|'template')
+ local vf="$1" class="$2" key="" current=""
+ case "$class" in
+ container) key="var_container_storage" ;;
+ template) key="var_template_storage" ;;
+ *)
+ msg_error "Unknown storage class: $class"
+ return 1
+ ;;
+ esac
+
+ current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
+
+ # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
+ local content="rootdir"
+ [[ "$class" == "template" ]] && content="vztmpl"
+ local count
+ count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
+
+ if [[ "$count" -eq 1 ]]; then
+ STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
+ STORAGE_INFO=""
+ else
+ # If the current value is preselectable, we could show it, but per your requirement we always offer selection
+ select_storage "$class" || return 1
+ fi
+
+ _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
+
+ # Keep environment in sync for later steps (e.g. app-default save)
+ if [[ "$class" == "container" ]]; then
+ export var_container_storage="$STORAGE_RESULT"
+ export CONTAINER_STORAGE="$STORAGE_RESULT"
+ else
+ export var_template_storage="$STORAGE_RESULT"
+ export TEMPLATE_STORAGE="$STORAGE_RESULT"
+ fi
+
+ msg_ok "Updated ${key} → ${STORAGE_RESULT}"
+}
+
+# ------------------------------------------------------------------------------
+# check_container_resources()
+#
+# - Compares host RAM/CPU with required values
+# - Warns if under-provisioned and asks user to continue or abort
+# ------------------------------------------------------------------------------
+check_container_resources() {
+ current_ram=$(free -m | awk 'NR==2{print $2}')
+ current_cpu=$(nproc)
+
+ if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
+ echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
+ echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
+ echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
+ echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ else
+ echo -e ""
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# check_container_storage()
+#
+# - Checks /boot partition usage
+# - Warns if usage >80% and asks user confirmation before proceeding
+# ------------------------------------------------------------------------------
+check_container_storage() {
+ total_size=$(df /boot --output=size | tail -n 1)
+ local used_size=$(df /boot --output=used | tail -n 1)
+ usage=$((100 * used_size / total_size))
+ if ((usage > 80)); then
+ echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
+ echo -ne "Continue anyway? "
+ read -r prompt
+ if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
+ echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
+ exit 1
+ fi
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# ssh_extract_keys_from_file()
+#
+# - Extracts valid SSH public keys from given file
+# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
+# ------------------------------------------------------------------------------
+ssh_extract_keys_from_file() {
+ local f="$1"
+ [[ -r "$f" ]] || return 0
+ tr -d '\r' <"$f" | awk '
+ /^[[:space:]]*#/ {next}
+ /^[[:space:]]*$/ {next}
+ # nackt: typ base64 [comment]
+ /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
+ # mit Optionen: finde ab erstem Key-Typ
+ {
+ match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
+ if (RSTART>0) { print substr($0, RSTART) }
+ }
+ '
+}
+
+# ------------------------------------------------------------------------------
+# ssh_build_choices_from_files()
+#
+# - Builds interactive whiptail checklist of available SSH keys
+# - Generates fingerprint, type and comment for each key
+# ------------------------------------------------------------------------------
+ssh_build_choices_from_files() {
+ local -a files=("$@")
+ CHOICES=()
+ COUNT=0
+ MAPFILE="$(mktemp)"
+ local id key typ fp cmt base ln=0
+
+ for f in "${files[@]}"; do
+ [[ -f "$f" && -r "$f" ]] || continue
+ base="$(basename -- "$f")"
+ case "$base" in
+ known_hosts | known_hosts.* | config) continue ;;
+ id_*) [[ "$f" != *.pub ]] && continue ;;
+ esac
+
+ # map every key in file
+ while IFS= read -r key; do
+ [[ -n "$key" ]] || continue
+
+ typ=""
+ fp=""
+ cmt=""
+ # Only the pure key part (without options) is already included in ‘key’.
+ read -r _typ _b64 _cmt <<<"$key"
+ typ="${_typ:-key}"
+ cmt="${_cmt:-}"
+ # Fingerprint via ssh-keygen (if available)
+ if command -v ssh-keygen >/dev/null 2>&1; then
+ fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
+ fi
+ # Label shorten
+ [[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
+
+ ln=$((ln + 1))
+ COUNT=$((COUNT + 1))
+ id="K${COUNT}"
+ echo "${id}|${key}" >>"$MAPFILE"
+ CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
+ done < <(ssh_extract_keys_from_file "$f")
+ done
+}
+
+# ------------------------------------------------------------------------------
+# ssh_discover_default_files()
+#
+# - Scans standard paths for SSH keys
+# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc.
+# ------------------------------------------------------------------------------
+ssh_discover_default_files() {
+ local -a cand=()
+ shopt -s nullglob
+ cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
+ cand+=(/root/.ssh/*.pub)
+ cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
+ shopt -u nullglob
+ printf '%s\0' "${cand[@]}"
+}
+
+configure_ssh_settings() {
+ SSH_KEYS_FILE="$(mktemp)"
+ : >"$SSH_KEYS_FILE"
+
+ IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
+ ssh_build_choices_from_files "${_def_files[@]}"
+ local default_key_count="$COUNT"
+
+ local ssh_key_mode
+ if [[ "$default_key_count" -gt 0 ]]; then
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "Provision SSH keys for root:" 14 72 4 \
+ "found" "Select from detected keys (${default_key_count})" \
+ "manual" "Paste a single public key" \
+ "folder" "Scan another folder (path or glob)" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ else
+ ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
+ "No host keys detected; choose manual/none:" 12 72 2 \
+ "manual" "Paste a single public key" \
+ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
+ fi
+
+ case "$ssh_key_mode" in
+ found)
+ local selection
+ selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \
+ --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ ;;
+ manual)
+ SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
+ [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
+ ;;
+ folder)
+ local glob_path
+ glob_path=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
+ if [[ -n "$glob_path" ]]; then
+ shopt -s nullglob
+ read -r -a _scan_files <<<"$glob_path"
+ shopt -u nullglob
+ if [[ "${#_scan_files[@]}" -gt 0 ]]; then
+ ssh_build_choices_from_files "${_scan_files[@]}"
+ if [[ "$COUNT" -gt 0 ]]; then
+ local folder_selection
+ folder_selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \
+ --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
+ for tag in $folder_selection; do
+ tag="${tag%\"}"
+ tag="${tag#\"}"
+ local line
+ line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
+ [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
+ done
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60
+ fi
+ else
+ whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60
+ fi
+ fi
+ ;;
+ none)
+ :
+ ;;
+ esac
+
+ if [[ -s "$SSH_KEYS_FILE" ]]; then
+ sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
+ printf '\n' >>"$SSH_KEYS_FILE"
+ fi
+
+ if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then
+ if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
+ SSH="yes"
+ else
+ SSH="no"
+ fi
+ else
+ SSH="no"
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# start()
+#
+# - Entry point of script
+# - On Proxmox host: calls install_script
+# - In silent mode: runs update_script
+# - Otherwise: shows update/setting menu
+# ------------------------------------------------------------------------------
+start() {
+ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func)
+ if command -v pveversion >/dev/null 2>&1; then
+ install_script || return 0
+ return 0
+ elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
+ VERBOSE="no"
+ set_std_mode
+ update_script
+ else
+ CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
+ "Support/Update functions for ${APP} LXC. Choose an option:" \
+ 12 60 3 \
+ "1" "YES (Silent Mode)" \
+ "2" "YES (Verbose Mode)" \
+ "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
+
+ case "$CHOICE" in
+ 1)
+ VERBOSE="no"
+ set_std_mode
+ ;;
+ 2)
+ VERBOSE="yes"
+ set_std_mode
+ ;;
+ 3)
+ clear
+ exit_script
+ exit
+ ;;
+ esac
+ update_script
+ fi
+}
+
+# ------------------------------------------------------------------------------
+# build_container()
+#
+# - Creates and configures the LXC container
+# - Builds network string and applies features (FUSE, TUN, VAAPI passthrough)
+# - Starts container and waits for network connectivity
+# - Installs base packages, SSH keys, and runs -install.sh
+# ------------------------------------------------------------------------------
+build_container() {
+ # if [ "$VERBOSE" == "yes" ]; then set -x; fi
+
+ NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
+
+ # MAC
+ if [[ -n "$MAC" ]]; then
+ case "$MAC" in
+ ,hwaddr=*) NET_STRING+="$MAC" ;;
+ *) NET_STRING+=",hwaddr=$MAC" ;;
+ esac
+ fi
+
+ # IP (immer zwingend, Standard dhcp)
+ NET_STRING+=",ip=${NET:-dhcp}"
+
+ # Gateway
+ if [[ -n "$GATE" ]]; then
+ case "$GATE" in
+ ,gw=*) NET_STRING+="$GATE" ;;
+ *) NET_STRING+=",gw=$GATE" ;;
+ esac
+ fi
+
+ # VLAN
+ if [[ -n "$VLAN" ]]; then
+ case "$VLAN" in
+ ,tag=*) NET_STRING+="$VLAN" ;;
+ *) NET_STRING+=",tag=$VLAN" ;;
+ esac
+ fi
+
+ # MTU
+ if [[ -n "$MTU" ]]; then
+ case "$MTU" in
+ ,mtu=*) NET_STRING+="$MTU" ;;
+ *) NET_STRING+=",mtu=$MTU" ;;
+ esac
+ fi
+
+ # IPv6 Handling
+ case "$IPV6_METHOD" in
+ auto) NET_STRING="$NET_STRING,ip6=auto" ;;
+ dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
+ static)
+ NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
+ [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
+ ;;
+ none) ;;
+ esac
+
+ if [ "$CT_TYPE" == "1" ]; then
+ FEATURES="keyctl=1,nesting=1"
+ else
+ FEATURES="nesting=1"
+ fi
+
+ if [ "$ENABLE_FUSE" == "yes" ]; then
+ FEATURES="$FEATURES,fuse=1"
+ fi
+
+ TEMP_DIR=$(mktemp -d)
+ pushd "$TEMP_DIR" >/dev/null
+ if [ "$var_os" == "alpine" ]; then
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)"
+ else
+ export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)"
+ fi
+ export DIAGNOSTICS="$DIAGNOSTICS"
+ export RANDOM_UUID="$RANDOM_UUID"
+ export CACHER="$APT_CACHER"
+ export CACHER_IP="$APT_CACHER_IP"
+ export tz="$timezone"
+ export APPLICATION="$APP"
+ export app="$NSAPP"
+ export PASSWORD="$PW"
+ export VERBOSE="$VERBOSE"
+ export SSH_ROOT="${SSH}"
+ export SSH_AUTHORIZED_KEY
+ export CTID="$CT_ID"
+ export CTTYPE="$CT_TYPE"
+ export ENABLE_FUSE="$ENABLE_FUSE"
+ export ENABLE_TUN="$ENABLE_TUN"
+ export PCT_OSTYPE="$var_os"
+ export PCT_OSVERSION="$var_version"
+ export PCT_DISK_SIZE="$DISK_SIZE"
+ export PCT_OPTIONS="
+ -features $FEATURES
+ -hostname $HN
+ -tags $TAGS
+ $SD
+ $NS
+ $NET_STRING
+ -onboot 1
+ -cores $CORE_COUNT
+ -memory $RAM_SIZE
+ -unprivileged $CT_TYPE
+ $PW
+"
+ export TEMPLATE_STORAGE="${var_template_storage:-}"
+ export CONTAINER_STORAGE="${var_container_storage:-}"
+ create_lxc_container || exit $?
+
+ LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
+
+ # ============================================================================
+ # GPU/USB PASSTHROUGH CONFIGURATION
+ # ============================================================================
+
+ # List of applications that benefit from GPU acceleration
+ GPU_APPS=(
+ "immich" "channels" "emby" "ersatztv" "frigate"
+ "jellyfin" "plex" "scrypted" "tdarr" "unmanic"
+ "ollama" "fileflows" "open-webui" "tunarr" "debian"
+ "handbrake" "sunshine" "moonlight" "kodi" "stremio"
+ "viseron"
+ )
+
+ # Check if app needs GPU
+ is_gpu_app() {
+ local app="${1,,}"
+ for gpu_app in "${GPU_APPS[@]}"; do
+ [[ "$app" == "${gpu_app,,}" ]] && return 0
+ done
+ return 1
+ }
+
+ # Detect all available GPU devices
+ detect_gpu_devices() {
+ INTEL_DEVICES=()
+ AMD_DEVICES=()
+ NVIDIA_DEVICES=()
+
+ # Store PCI info to avoid multiple calls
+ local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D")
+
+ # Check for Intel GPU - look for Intel vendor ID [8086]
+ if echo "$pci_vga_info" | grep -q "\[8086:"; then
+ msg_info "Detected Intel GPU"
+ if [[ -d /dev/dri ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && INTEL_DEVICES+=("$d")
+ done
+ fi
+ fi
+
+ # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
+ if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then
+ msg_info "Detected AMD GPU"
+ if [[ -d /dev/dri ]]; then
+ # Only add if not already claimed by Intel
+ if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
+ for d in /dev/dri/renderD* /dev/dri/card*; do
+ [[ -e "$d" ]] && AMD_DEVICES+=("$d")
+ done
+ fi
+ fi
+ fi
+
+ # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
+ if echo "$pci_vga_info" | grep -q "\[10de:"; then
+ msg_info "Detected NVIDIA GPU"
+ if ! check_nvidia_host_setup; then
+ msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough."
+ msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually."
+ return 0
+ fi
+
+ for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do
+ [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
+ done
+
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found"
+ msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver"
+ else
+ if [[ "$CT_TYPE" == "0" ]]; then
+ cat <>"$LXC_CONFIG"
+ # NVIDIA GPU Passthrough (privileged)
+ lxc.cgroup2.devices.allow: c 195:* rwm
+ lxc.cgroup2.devices.allow: c 243:* rwm
+ lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
+ lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file
+EOF
+
+ if [[ -e /dev/dri/renderD128 ]]; then
+ echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+
+ export GPU_TYPE="NVIDIA"
+ export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
+ msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})"
+ else
+ msg_warn "NVIDIA passthrough only supported for privileged containers"
+ return 0
+ fi
+ fi
+ fi
+
+ # Debug output
+ msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
+ msg_debug "AMD devices: ${AMD_DEVICES[*]}"
+ msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
+ }
+
+ # Configure USB passthrough for privileged containers
+ configure_usb_passthrough() {
+ if [[ "$CT_TYPE" != "0" ]]; then
+ return 0
+ fi
+
+ msg_info "Configuring automatic USB passthrough (privileged container)"
+ cat <>"$LXC_CONFIG"
+# Automatic USB passthrough (privileged container)
+lxc.cgroup2.devices.allow: a
+lxc.cap.drop:
+lxc.cgroup2.devices.allow: c 188:* rwm
+lxc.cgroup2.devices.allow: c 189:* rwm
+lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
+lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
+lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
+EOF
+ msg_ok "USB passthrough configured"
+ }
+
+ # Configure GPU passthrough
+ configure_gpu_passthrough() {
+ # Skip if not a GPU app and not privileged
+ if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then
+ return 0
+ fi
+
+ detect_gpu_devices
+
+ # Count available GPU types
+ local gpu_count=0
+ local available_gpus=()
+
+ if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("INTEL")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("AMD")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
+ available_gpus+=("NVIDIA")
+ gpu_count=$((gpu_count + 1))
+ fi
+
+ if [[ $gpu_count -eq 0 ]]; then
+ msg_info "No GPU devices found for passthrough"
+ return 0
+ fi
+
+ local selected_gpu=""
+
+ if [[ $gpu_count -eq 1 ]]; then
+ # Automatic selection for single GPU
+ selected_gpu="${available_gpus[0]}"
+ msg_info "Automatically configuring ${selected_gpu} GPU passthrough"
+ else
+ # Multiple GPUs - ask user
+ echo -e "\n${INFO} Multiple GPU types detected:"
+ for gpu in "${available_gpus[@]}"; do
+ echo " - $gpu"
+ done
+ read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
+ selected_gpu="${selected_gpu^^}"
+
+ # Validate selection
+ local valid=0
+ for gpu in "${available_gpus[@]}"; do
+ [[ "$selected_gpu" == "$gpu" ]] && valid=1
+ done
+
+ if [[ $valid -eq 0 ]]; then
+ msg_warn "Invalid selection. Skipping GPU passthrough."
+ return 0
+ fi
+ fi
+
+ # Apply passthrough configuration based on selection
+ local dev_idx=0
+
+ case "$selected_gpu" in
+ INTEL | AMD)
+ local devices=()
+ [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
+ [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
+
+ # For Proxmox WebUI visibility, add as dev0, dev1 etc.
+ for dev in "${devices[@]}"; do
+ if [[ "$CT_TYPE" == "0" ]]; then
+ # Privileged container - use dev entries for WebUI visibility
+ # Use initial GID 104 (render) for renderD*, 44 (video) for card*
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+
+ # Also add cgroup allows for privileged containers
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ else
+ # Unprivileged container
+ if [[ "$dev" =~ renderD ]]; then
+ echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
+ else
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ fi
+ dev_idx=$((dev_idx + 1))
+ fi
+ done
+
+ export GPU_TYPE="$selected_gpu"
+ msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)"
+ ;;
+
+ NVIDIA)
+ if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
+ msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver"
+ return 1
+ fi
+
+ for dev in "${NVIDIA_DEVICES[@]}"; do
+ # NVIDIA devices typically need different handling
+ echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
+ dev_idx=$((dev_idx + 1))
+
+ if [[ "$CT_TYPE" == "0" ]]; then
+ local major minor
+ major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
+ minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
+
+ if [[ "$major" != "0" && "$minor" != "0" ]]; then
+ echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
+ fi
+ fi
+ done
+
+ export GPU_TYPE="NVIDIA"
+ msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)"
+ ;;
+ esac
+ }
+
+ # Additional device passthrough
+ configure_additional_devices() {
+ # TUN device passthrough
+ if [ "$ENABLE_TUN" == "yes" ]; then
+ cat <>"$LXC_CONFIG"
+lxc.cgroup2.devices.allow: c 10:200 rwm
+lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
+EOF
+ fi
+
+ # Coral TPU passthrough
+ if [[ -e /dev/apex_0 ]]; then
+ msg_info "Detected Coral TPU - configuring passthrough"
+ echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
+ fi
+ }
+
+ # Execute pre-start configurations
+ configure_usb_passthrough
+ configure_gpu_passthrough
+ configure_additional_devices
+
+ # ============================================================================
+ # START CONTAINER AND INSTALL USERLAND
+ # ============================================================================
+
+ msg_info "Starting LXC Container"
+ pct start "$CTID"
+
+ # Wait for container to be running
+ for i in {1..10}; do
+ if pct status "$CTID" | grep -q "status: running"; then
+ msg_ok "Started LXC Container"
+ break
+ fi
+ sleep 1
+ if [ "$i" -eq 10 ]; then
+ msg_error "LXC Container did not reach running state"
+ exit 1
+ fi
+ done
+
+ # Wait for network (skip for Alpine initially)
+ if [ "$var_os" != "alpine" ]; then
+ msg_info "Waiting for network in LXC container"
+
+ # Wait for IP
+ for i in {1..20}; do
+ ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+ [ -n "$ip_in_lxc" ] && break
+ sleep 1
+ done
+
+ if [ -z "$ip_in_lxc" ]; then
+ msg_error "No IP assigned to CT $CTID after 20s"
+ exit 1
+ fi
+
+ # Try to reach gateway
+ gw_ok=0
+ for i in {1..10}; do
+ if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then
+ gw_ok=1
+ break
+ fi
+ sleep 1
+ done
+
+ if [ "$gw_ok" -eq 1 ]; then
+ msg_ok "Network in LXC is reachable (IP $ip_in_lxc)"
+ else
+ msg_warn "Network reachable but gateway check failed"
+ fi
+ fi
+ # Function to get correct GID inside container
+ get_container_gid() {
+ local group="$1"
+ local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
+ echo "${gid:-44}" # Default to 44 if not found
+ }
+
+ fix_gpu_gids
+
+ # Continue with standard container setup
+ msg_info "Customizing LXC Container"
+
+ # # Install GPU userland if configured
+ # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
+ # install_gpu_userland "VAAPI"
+ # fi
+
+ # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
+ # install_gpu_userland "NVIDIA"
+ # fi
+
+ # Continue with standard container setup
+ if [ "$var_os" == "alpine" ]; then
+ sleep 3
+ pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
+http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
+EOF'
+ pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
+ else
+ sleep 3
+ pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
+ pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
+ echo LANG=\$locale_line >/etc/default/locale && \
+ locale-gen >/dev/null && \
+ export LANG=\$locale_line"
+
+ if [[ -z "${tz:-}" ]]; then
+ tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
+ fi
+
+ if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
+ pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
+ else
+ msg_warn "Skipping timezone setup – zone '$tz' not found in container"
+ fi
+
+ pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || {
+ msg_error "apt-get base packages installation failed"
+ exit 1
+ }
+ fi
+
+ msg_ok "Customized LXC Container"
+
+ # Verify GPU access if enabled
+ if [[ "${ENABLE_VAAPI:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "vainfo >/dev/null 2>&1" &&
+ msg_ok "VAAPI verified working" ||
+ msg_warn "VAAPI verification failed - may need additional configuration"
+ fi
+
+ if [[ "${ENABLE_NVIDIA:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
+ pct exec "$CTID" -- bash -c "nvidia-smi >/dev/null 2>&1" &&
+ msg_ok "NVIDIA verified working" ||
+ msg_warn "NVIDIA verification failed - may need additional configuration"
+ fi
+
+ # Install SSH keys
+ install_ssh_keys_into_ct
+
+ # Run application installer
+ if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then
+ exit $?
+ fi
+}
+
+destroy_lxc() {
+ if [[ -z "$CT_ID" ]]; then
+ msg_error "No CT_ID found. Nothing to remove."
+ return 1
+ fi
+
+ # Abbruch bei Ctrl-C / Ctrl-D / ESC
+ trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
+
+ local prompt
+ if ! read -rp "Remove this Container? " prompt; then
+ # read gibt != 0 zurück bei Ctrl-D/ESC
+ msg_error "Aborted input (Ctrl-D/ESC)"
+ return 130
+ fi
+
+ case "${prompt,,}" in
+ y | yes)
+ if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
+ msg_ok "Removed Container $CT_ID"
+ else
+ msg_error "Failed to remove Container $CT_ID"
+ return 1
+ fi
+ ;;
+ "" | n | no)
+ msg_info "Container was not removed."
+ ;;
+ *)
+ msg_warn "Invalid response. Container was not removed."
+ ;;
+ esac
+}
+
+# ------------------------------------------------------------------------------
+# Storage discovery / selection helpers
+# ------------------------------------------------------------------------------
+# ===== Storage discovery / selection helpers (ported from create_lxc.sh) =====
+resolve_storage_preselect() {
+ local class="$1" preselect="$2" required_content=""
+ case "$class" in
+ template) required_content="vztmpl" ;;
+ container) required_content="rootdir" ;;
+ *) return 1 ;;
+ 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
+ fi
+
+ local line total used free
+ line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
+ if [[ -z "$line" ]]; then
+ STORAGE_INFO="n/a"
+ else
+ total="$(awk '{print $4}' <<<"$line")"
+ used="$(awk '{print $5}' <<<"$line")"
+ free="$(awk '{print $6}' <<<"$line")"
+ local total_h used_h free_h
+ if command -v numfmt >/dev/null 2>&1; then
+ total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
+ used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
+ free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
+ STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
+ else
+ STORAGE_INFO="Free: ${free} Used: ${used}"
+ fi
+ fi
+ STORAGE_RESULT="$preselect"
+ return 0
+}
+
+fix_gpu_gids() {
+ if [[ -z "${GPU_TYPE:-}" ]]; then
+ return 0
+ fi
+
+ msg_info "Detecting and setting correct GPU group IDs"
+
+ # Ermittle die tatsächlichen GIDs aus dem Container
+ local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+
+ # Fallbacks wenn Gruppen nicht existieren
+ if [[ -z "$video_gid" ]]; then
+ # Versuche die video Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true"
+ video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
+ [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback
+ fi
+
+ if [[ -z "$render_gid" ]]; then
+ # Versuche die render Gruppe zu erstellen
+ pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true"
+ render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
+ [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback
+ fi
+
+ msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}"
+
+ # Prüfe ob die GIDs von den Defaults abweichen
+ local need_update=0
+ if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then
+ need_update=1
+ fi
+
+ if [[ $need_update -eq 1 ]]; then
+ msg_info "Updating device GIDs in container config"
+
+ # Stoppe Container für Config-Update
+ pct stop "$CTID" >/dev/null 2>&1
+
+ # Update die dev Einträge mit korrekten GIDs
+ # Backup der Config
+ cp "$LXC_CONFIG" "${LXC_CONFIG}.bak"
+
+ # Parse und update jeden dev Eintrag
+ while IFS= read -r line; do
+ if [[ "$line" =~ ^dev[0-9]+: ]]; then
+ # Extract device path
+ local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/')
+ local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/')
+
+ if [[ "$device_path" =~ renderD ]]; then
+ # RenderD device - use render GID
+ echo "${dev_num}: ${device_path},gid=${render_gid}"
+ elif [[ "$device_path" =~ card ]]; then
+ # Card device - use video GID
+ echo "${dev_num}: ${device_path},gid=${video_gid}"
+ else
+ # Keep original line
+ echo "$line"
+ fi
+ else
+ # Keep non-dev lines
+ echo "$line"
+ fi
+ done <"$LXC_CONFIG" >"${LXC_CONFIG}.new"
+
+ mv "${LXC_CONFIG}.new" "$LXC_CONFIG"
+
+ # Starte Container wieder
+ pct start "$CTID" >/dev/null 2>&1
+ sleep 3
+
+ msg_ok "Device GIDs updated successfully"
+ else
+ msg_ok "Device GIDs are already correct"
+ fi
+ if [[ "$CT_TYPE" == "0" ]]; then
+ pct exec "$CTID" -- bash -c "
+ if [ -d /dev/dri ]; then
+ for dev in /dev/dri/*; do
+ if [ -e \"\$dev\" ]; then
+ if [[ \"\$dev\" =~ renderD ]]; then
+ chgrp ${render_gid} \"\$dev\" 2>/dev/null || true
+ else
+ chgrp ${video_gid} \"\$dev\" 2>/dev/null || true
+ fi
+ chmod 660 \"\$dev\" 2>/dev/null || true
+ fi
+ done
+ fi
+ " >/dev/null 2>&1
+ fi
+}
+
+# NVIDIA-spezific check on host
+check_nvidia_host_setup() {
+ if ! command -v nvidia-smi >/dev/null 2>&1; then
+ msg_warn "NVIDIA GPU detected but nvidia-smi not found on host"
+ msg_warn "Please install NVIDIA drivers on host first."
+ #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run"
+ #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms"
+ #echo " 3. Verify: nvidia-smi"
+ return 1
+ fi
+
+ # check if nvidia-smi works
+ if ! nvidia-smi >/dev/null 2>&1; then
+ msg_warn "nvidia-smi installed but not working. Driver issue?"
+ return 1
+ fi
+
+ return 0
+}
+
+check_storage_support() {
+ local CONTENT="$1" VALID=0
+ while IFS= read -r line; do
+ local STORAGE_NAME
+ STORAGE_NAME=$(awk '{print $1}' <<<"$line")
+ [[ -n "$STORAGE_NAME" ]] && VALID=1
+ done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
+ [[ $VALID -eq 1 ]]
+}
+
+select_storage() {
+ local CLASS=$1 CONTENT CONTENT_LABEL
+ case $CLASS in
+ container)
+ CONTENT='rootdir'
+ CONTENT_LABEL='Container'
+ ;;
+ template)
+ CONTENT='vztmpl'
+ CONTENT_LABEL='Container template'
+ ;;
+ iso)
+ CONTENT='iso'
+ CONTENT_LABEL='ISO image'
+ ;;
+ images)
+ CONTENT='images'
+ CONTENT_LABEL='VM Disk image'
+ ;;
+ backup)
+ CONTENT='backup'
+ CONTENT_LABEL='Backup'
+ ;;
+ snippets)
+ CONTENT='snippets'
+ CONTENT_LABEL='Snippets'
+ ;;
+ *)
+ msg_error "Invalid storage class '$CLASS'"
+ return 1
+ ;;
+ esac
+
+ declare -A STORAGE_MAP
+ local -a MENU=()
+ local COL_WIDTH=0
+
+ while read -r TAG TYPE _ TOTAL USED FREE _; do
+ [[ -n "$TAG" && -n "$TYPE" ]] || continue
+ local DISPLAY="${TAG} (${TYPE})"
+ local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
+ local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
+ local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
+ STORAGE_MAP["$DISPLAY"]="$TAG"
+ MENU+=("$DISPLAY" "$INFO" "OFF")
+ ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
+ done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
+
+ if [[ ${#MENU[@]} -eq 0 ]]; then
+ msg_error "No storage found for content type '$CONTENT'."
+ return 2
+ fi
+
+ if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
+ STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
+ STORAGE_INFO="${MENU[1]}"
+ return 0
+ fi
+
+ local WIDTH=$((COL_WIDTH + 42))
+ while true; do
+ local DISPLAY_SELECTED
+ DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
+ --title "Storage Pools" \
+ --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
+ 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }
+
+ DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
+ if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
+ whiptail --msgbox "No valid storage selected. Please try again." 8 58
+ continue
+ fi
+ STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
+ for ((i = 0; i < ${#MENU[@]}; i += 3)); do
+ if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
+ STORAGE_INFO="${MENU[$i + 1]}"
+ break
+ fi
+ done
+ return 0
+ done
+}
+
+create_lxc_container() {
+ # ------------------------------------------------------------------------------
+ # Optional verbose mode (debug tracing)
+ # ------------------------------------------------------------------------------
+ if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
+
+ # ------------------------------------------------------------------------------
+ # Helpers (dynamic versioning / template parsing)
+ # ------------------------------------------------------------------------------
+ pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
+ pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
+
+ ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
+ ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
+ ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
+
+ # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
+ parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
+
+ # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
+ # Returns:
+ # 0 = no upgrade needed
+ # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
+ # 2 = user declined
+ # 3 = upgrade attempted but failed OR retry failed
+ offer_lxc_stack_upgrade_and_maybe_retry() {
+ local do_retry="${1:-no}" # yes|no
+ local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
+
+ _pvec_i="$(pkg_ver pve-container)"
+ _lxcp_i="$(pkg_ver lxc-pve)"
+ _pvec_c="$(pkg_cand pve-container)"
+ _lxcp_c="$(pkg_cand lxc-pve)"
+
+ if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
+ ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
+ fi
+ if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
+ ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
+ fi
+ if [[ $need -eq 0 ]]; then
+ msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
+ return 0
+ fi
+
+ echo
+ echo "An update for the Proxmox LXC stack is available:"
+ echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
+ echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
+ echo
+ read -rp "Do you want to upgrade now? [y/N] " _ans
+ case "${_ans,,}" in
+ y | yes)
+ msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
+ if apt-get update -qq >/dev/null && apt-get install -y --only-upgrade pve-container lxc-pve >/dev/null; then
+ msg_ok "LXC stack upgraded."
+ if [[ "$do_retry" == "yes" ]]; then
+ msg_info "Retrying container creation after upgrade"
+ if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container created successfully after upgrade."
+ return 0
+ else
+ msg_error "pct create still failed after upgrade. See $LOGFILE"
+ return 3
+ fi
+ fi
+ return 1
+ else
+ msg_error "Upgrade failed. Please check APT output."
+ return 3
+ fi
+ ;;
+ *) return 2 ;;
+ esac
+ }
+
+ # ------------------------------------------------------------------------------
+ # Required input variables
+ # ------------------------------------------------------------------------------
+ [[ "${CTID:-}" ]] || {
+ msg_error "You need to set 'CTID' variable."
+ exit 203
+ }
+ [[ "${PCT_OSTYPE:-}" ]] || {
+ msg_error "You need to set 'PCT_OSTYPE' variable."
+ exit 204
+ }
+
+ msg_debug "CTID=$CTID"
+ msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
+ msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
+
+ # ID checks
+ [[ "$CTID" -ge 100 ]] || {
+ msg_error "ID cannot be less than 100."
+ exit 205
+ }
+ if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
+ echo -e "ID '$CTID' is already in use."
+ unset CTID
+ msg_error "Cannot use ID that is already in use."
+ exit 206
+ fi
+
+ # Storage capability check
+ check_storage_support "rootdir" || {
+ msg_error "No valid storage found for 'rootdir' [Container]"
+ exit 1
+ }
+ check_storage_support "vztmpl" || {
+ msg_error "No valid storage found for 'vztmpl' [Template]"
+ exit 1
+ }
+
+ # Template storage selection
+ if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ else
+ while true; do
+ if [[ -z "${var_template_storage:-}" ]]; then
+ if select_storage template; then
+ TEMPLATE_STORAGE="$STORAGE_RESULT"
+ TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
+ break
+ fi
+ fi
+ done
+ fi
+
+ # Container storage selection
+ if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ else
+ if [[ -z "${var_container_storage:-}" ]]; then
+ if select_storage container; then
+ CONTAINER_STORAGE="$STORAGE_RESULT"
+ CONTAINER_STORAGE_INFO="$STORAGE_INFO"
+ msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
+ fi
+ fi
+ fi
+
+ # Validate content types
+ msg_info "Validating content types of storage '$CONTAINER_STORAGE'"
+ STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT"
+ grep -qw "rootdir" <<<"$STORAGE_CONTENT" || {
+ msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC."
+ exit 217
+ }
+ $STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'"
+
+ msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'"
+ TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
+ msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT"
+ if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then
+ msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail."
+ else
+ $STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'"
+ fi
+
+ # Free space check
+ STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
+ REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
+ [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || {
+ msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
+ exit 214
+ }
+
+ # Cluster quorum (if cluster)
+ if [[ -f /etc/pve/corosync.conf ]]; then
+ msg_info "Checking cluster quorum"
+ if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
+ msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
+ exit 210
+ fi
+ msg_ok "Cluster is quorate"
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Template discovery & validation
+ # ------------------------------------------------------------------------------
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ case "$PCT_OSTYPE" in
+ debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
+ alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
+ *) TEMPLATE_PATTERN="" ;;
+ esac
+
+ msg_info "Searching for template '$TEMPLATE_SEARCH'"
+
+ # Build regex patterns outside awk/grep for clarity
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}"
+
+ #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'"
+ #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'"
+ #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+
+ pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)."
+
+ #echo "[DEBUG] pveam available output (first 5 lines with .tar files):"
+ #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /'
+
+ set +u
+ mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
+ #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found"
+ set -u
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #echo "[DEBUG] Online templates:"
+ for tmpl in "${ONLINE_TEMPLATES[@]}"; do
+ echo " - $tmpl"
+ done
+ fi
+
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'"
+ #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates"
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ #msg_debug "First 3 online templates:"
+ count=0
+ for idx in "${!ONLINE_TEMPLATES[@]}"; do
+ #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}"
+ ((count++))
+ [[ $count -ge 3 ]] && break
+ done
+ fi
+ #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ # If still no template, try to find alternatives
+ if [[ -z "$TEMPLATE" ]]; then
+ echo ""
+ echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."
+
+ # Get all available versions for this OS type
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" |
+ sort -u -V 2>/dev/null
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo ""
+ echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ #echo "[DEBUG] Retrying with version: $PCT_OSVERSION"
+
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+
+ if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="online"
+ #echo "[DEBUG] Found alternative: $TEMPLATE"
+ else
+ msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ exit 225
+ fi
+ else
+ msg_info "Installation cancelled"
+ exit 0
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available at all"
+ exit 225
+ fi
+ fi
+
+ #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+ #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'"
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"
+
+ # Get available versions
+ mapfile -t AVAILABLE_VERSIONS < <(
+ pveam available -section system 2>/dev/null |
+ grep "^${PCT_OSTYPE}-" |
+ sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
+ grep -E '^[0-9]+\.[0-9]+$' |
+ sort -u -V 2>/dev/null || sort -u
+ )
+
+ if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
+ echo -e "\n${BL}Available versions:${CL}"
+ for i in "${!AVAILABLE_VERSIONS[@]}"; do
+ echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
+ done
+
+ echo ""
+ read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice
+
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
+ export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
+ export PCT_OSVERSION="$var_version"
+ msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"
+
+ # Retry template search with new version
+ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
+ SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
+
+ mapfile -t LOCAL_TEMPLATES < <(
+ pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
+ awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
+ sed 's|.*/||' | sort -t - -k 2 -V
+ )
+ mapfile -t ONLINE_TEMPLATES < <(
+ pveam available -section system 2>/dev/null |
+ grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
+ awk -F'\t' '{print $1}' |
+ grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
+ sort -t - -k 2 -V 2>/dev/null || true
+ )
+ ONLINE_TEMPLATE=""
+ [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
+
+ if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
+ TEMPLATE="${LOCAL_TEMPLATES[-1]}"
+ TEMPLATE_SOURCE="local"
+ else
+ TEMPLATE="$ONLINE_TEMPLATE"
+ TEMPLATE_SOURCE="online"
+ fi
+
+ TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
+ if [[ -z "$TEMPLATE_PATH" ]]; then
+ TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
+ [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
+ fi
+
+ # If we still don't have a path but have a valid template name, construct it
+ if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
+ TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ fi
+
+ [[ -n "$TEMPLATE_PATH" ]] || {
+ msg_error "Template still not found after version change"
+ exit 220
+ }
+ else
+ msg_info "Installation cancelled"
+ exit 1
+ fi
+ else
+ msg_error "No ${PCT_OSTYPE} templates available"
+ exit 220
+ fi
+ fi
+ }
+
+ # Validate that we found a template
+ if [[ -z "$TEMPLATE" ]]; then
+ msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
+ msg_info "Please check:"
+ msg_info " - Is pveam catalog available? (run: pveam available -section system)"
+ msg_info " - Does the template exist for your OS version?"
+ exit 225
+ fi
+
+ msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
+ msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
+
+ NEED_DOWNLOAD=0
+ if [[ ! -f "$TEMPLATE_PATH" ]]; then
+ msg_info "Template not present locally – will download."
+ NEED_DOWNLOAD=1
+ elif [[ ! -r "$TEMPLATE_PATH" ]]; then
+ msg_error "Template file exists but is not readable – check permissions."
+ exit 221
+ elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template file too small (<1MB) – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template looks too small, but no online version exists. Keeping local file."
+ fi
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ NEED_DOWNLOAD=1
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
+ fi
+ else
+ $STD msg_ok "Template $TEMPLATE is present and valid."
+ fi
+
+ if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
+ if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
+ TEMPLATE="$ONLINE_TEMPLATE"
+ NEED_DOWNLOAD=1
+ else
+ msg_info "Continuing with local template $TEMPLATE"
+ fi
+ fi
+
+ if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
+ [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
+ for attempt in {1..3}; do
+ msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
+ if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
+ msg_ok "Template download successful."
+ break
+ fi
+ if [[ $attempt -eq 3 ]]; then
+ msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
+ exit 222
+ fi
+ sleep $((attempt * 5))
+ done
+ fi
+
+ if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
+ msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
+ exit 223
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
+ # ------------------------------------------------------------------------------
+ if [[ "$PCT_OSTYPE" == "debian" ]]; then
+ OSVER="$(parse_template_osver "$TEMPLATE")"
+ if [[ -n "$OSVER" ]]; then
+ # Proactive, aber ohne Abbruch – nur Angebot
+ offer_lxc_stack_upgrade_and_maybe_retry "no" || true
+ fi
+ fi
+
+ # ------------------------------------------------------------------------------
+ # Create LXC Container
+ # ------------------------------------------------------------------------------
+ msg_info "Creating LXC container"
+
+ # Ensure subuid/subgid entries exist
+ grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
+ grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
+
+ # Assemble pct options
+ PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
+ [[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
+
+ # Lock by template file (avoid concurrent downloads/creates)
+ lockfile="/tmp/template.${TEMPLATE}.lock"
+ exec 9>"$lockfile" || {
+ msg_error "Failed to create lock file '$lockfile'."
+ exit 200
+ }
+ flock -w 60 9 || {
+ msg_error "Timeout while waiting for template lock."
+ exit 211
+ }
+
+ LOGFILE="/tmp/pct_create_${CTID}.log"
+ msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}"
+ msg_debug "Logfile: $LOGFILE"
+
+ # First attempt
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then
+ msg_error "Container creation failed on ${TEMPLATE_STORAGE}. Checking template..."
+
+ # Validate template file
+ if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
+ msg_warn "Template file too small or missing – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
+ if [[ -n "$ONLINE_TEMPLATE" ]]; then
+ msg_warn "Template appears corrupted – re-downloading."
+ rm -f "$TEMPLATE_PATH"
+ pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
+ else
+ msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
+ fi
+ fi
+
+ # Retry after repair
+ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ # Fallback to local storage
+ if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
+ msg_warn "Retrying container creation with fallback to local storage..."
+ LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
+ if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
+ msg_info "Downloading template to local..."
+ pveam download local "$TEMPLATE" >/dev/null 2>&1
+ fi
+ if pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
+ msg_ok "Container successfully created using local fallback."
+ else
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed even with local fallback. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ else
+ msg_error "Container creation failed on local storage. See $LOGFILE"
+ # --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
+ if grep -qiE 'unsupported .* version' "$LOGFILE"; then
+ echo
+ echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
+ echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
+ offer_lxc_stack_upgrade_and_maybe_retry "yes"
+ rc=$?
+ case $rc in
+ 0) : ;; # success - container created, continue
+ 2)
+ echo "Upgrade was declined. Please update and re-run:
+ apt update && apt install --only-upgrade pve-container lxc-pve"
+ exit 231
+ ;;
+ 3)
+ echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
+ exit 231
+ ;;
+ esac
+ else
+ msg_error "Container creation failed. See $LOGFILE"
+ if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
+ set -x
+ bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
+ set +x
+ fi
+ exit 209
+ fi
+ fi
+ fi
+ fi
+
+ # Verify container exists
+ pct list | awk '{print $1}' | grep -qx "$CTID" || {
+ msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE"
+ exit 215
+ }
+
+ # Verify config rootfs
+ grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
+ msg_error "RootFS entry missing in container config. See $LOGFILE"
+ exit 216
+ }
+
+ msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
+}
+
+# ------------------------------------------------------------------------------
+# description()
+#
+# - Sets container description with HTML content (logo, links, badges)
+# - Restarts ping-instances.service if present
+# - Posts status "done" to API
+# ------------------------------------------------------------------------------
+description() {
+ IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
+
+ # Generate LXC Description
+ DESCRIPTION=$(
+ cat <
+
+
+
+
+ ${APP} LXC
+
+
+
+
+
+
+
+
+
+ GitHub
+
+
+
+ Discussions
+
+
+
+ Issues
+
+
+EOF
+ )
+ pct set "$CTID" -description "$DESCRIPTION"
+
+ if [[ -f /etc/systemd/system/ping-instances.service ]]; then
+ systemctl start ping-instances.service
+ fi
+
+ post_update_to_api "done" "none"
+}
+
+# ------------------------------------------------------------------------------
+# api_exit_script()
+#
+# - Exit trap handler
+# - Reports exit codes to API with detailed reason
+# - Handles known codes (100–209) and maps them to errors
+# ------------------------------------------------------------------------------
+api_exit_script() {
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then
+ case $exit_code in
+ 100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;;
+ 101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;;
+ 200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;;
+ 201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;;
+ 202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;;
+ 203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;;
+ 204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;;
+ 205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;;
+ 206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;;
+ 207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;;
+ 208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;;
+ 209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;;
+ *) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;;
+ esac
+ fi
+}
+
+if command -v pveversion >/dev/null 2>&1; then
+ trap 'api_exit_script' EXIT
+fi
+trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
+trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
+trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
diff --git a/misc/error_handler.func b/misc/error_handler.func
index d2f21d087..5418a490e 100644
--- a/misc/error_handler.func
+++ b/misc/error_handler.func
@@ -1,148 +1,342 @@
#!/usr/bin/env bash
# ------------------------------------------------------------------------------
-# Error & Signal Handling for ProxmoxVED Scripts
+# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
# ------------------------------------------------------------------------------
# Copyright (c) 2021-2025 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# ------------------------------------------------------------------------------
+#
+# Provides comprehensive error handling and signal management for all scripts.
+# Includes:
+# - Exit code explanations (shell, package managers, databases, custom codes)
+# - Error handler with detailed logging
+# - Signal handlers (EXIT, INT, TERM)
+# - Initialization function for trap setup
+#
+# Usage:
+# source <(curl -fsSL .../error_handler.func)
+# catch_errors
+#
+# ------------------------------------------------------------------------------
+# ==============================================================================
+# SECTION 1: EXIT CODE EXPLANATIONS
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# explain_exit_code()
+#
+# - Maps numeric exit codes to human-readable error descriptions
+# - Supports:
+# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143)
+# * Package manager errors (APT, DPKG: 100, 101, 255)
+# * Node.js/npm errors (243-249, 254)
+# * Python/pip/uv errors (210-212)
+# * PostgreSQL errors (231-234)
+# * MySQL/MariaDB errors (241-244)
+# * MongoDB errors (251-254)
+# * Proxmox custom codes (200-231)
+# - Returns description string for given exit code
+# ------------------------------------------------------------------------------
explain_exit_code() {
- local code="$1"
- case "$code" in
- # --- Generic / Shell ---
- 1) echo "General error / Operation not permitted" ;;
- 2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
- 126) echo "Command invoked cannot execute (permission problem?)" ;;
- 127) echo "Command not found" ;;
- 128) echo "Invalid argument to exit" ;;
- 130) echo "Terminated by Ctrl+C (SIGINT)" ;;
- 137) echo "Killed (SIGKILL / Out of memory?)" ;;
- 139) echo "Segmentation fault (core dumped)" ;;
- 143) echo "Terminated (SIGTERM)" ;;
+ local code="$1"
+ case "$code" in
+ # --- Generic / Shell ---
+ 1) echo "General error / Operation not permitted" ;;
+ 2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
+ 126) echo "Command invoked cannot execute (permission problem?)" ;;
+ 127) echo "Command not found" ;;
+ 128) echo "Invalid argument to exit" ;;
+ 130) echo "Terminated by Ctrl+C (SIGINT)" ;;
+ 137) echo "Killed (SIGKILL / Out of memory?)" ;;
+ 139) echo "Segmentation fault (core dumped)" ;;
+ 143) echo "Terminated (SIGTERM)" ;;
- # --- Package manager / APT / DPKG ---
- 100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
- 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
- 255) echo "DPKG: Fatal internal error" ;;
+ # --- Package manager / APT / DPKG ---
+ 100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
+ 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
+ 255) echo "DPKG: Fatal internal error" ;;
- # --- Node.js / npm / pnpm / yarn ---
- 243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
- 245) echo "Node.js: Invalid command-line option" ;;
- 246) echo "Node.js: Internal JavaScript Parse Error" ;;
- 247) echo "Node.js: Fatal internal error" ;;
- 248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
- 249) echo "Node.js: Inspector error" ;;
- 254) echo "npm/pnpm/yarn: Unknown fatal error" ;;
+ # --- Node.js / npm / pnpm / yarn ---
+ 243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
+ 245) echo "Node.js: Invalid command-line option" ;;
+ 246) echo "Node.js: Internal JavaScript Parse Error" ;;
+ 247) echo "Node.js: Fatal internal error" ;;
+ 248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
+ 249) echo "Node.js: Inspector error" ;;
+ 254) echo "npm/pnpm/yarn: Unknown fatal error" ;;
- # --- Python / pip / uv ---
- 210) echo "Python: Virtualenv / uv environment missing or broken" ;;
- 211) echo "Python: Dependency resolution failed" ;;
- 212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
+ # --- Python / pip / uv ---
+ 210) echo "Python: Virtualenv / uv environment missing or broken" ;;
+ 211) echo "Python: Dependency resolution failed" ;;
+ 212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
- # --- PostgreSQL ---
- 231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
- 232) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
- 233) echo "PostgreSQL: Database does not exist" ;;
- 234) echo "PostgreSQL: Fatal error in query / syntax" ;;
+ # --- PostgreSQL ---
+ 231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
+ 232) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
+ 233) echo "PostgreSQL: Database does not exist" ;;
+ 234) echo "PostgreSQL: Fatal error in query / syntax" ;;
- # --- MySQL / MariaDB ---
- 241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
- 242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
- 243) echo "MySQL/MariaDB: Database does not exist" ;;
- 244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
+ # --- MySQL / MariaDB ---
+ 241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
+ 242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
+ 243) echo "MySQL/MariaDB: Database does not exist" ;;
+ 244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
- # --- MongoDB ---
- 251) echo "MongoDB: Connection failed (server not running)" ;;
- 252) echo "MongoDB: Authentication failed (bad user/password)" ;;
- 253) echo "MongoDB: Database not found" ;;
- 254) echo "MongoDB: Fatal query error" ;;
+ # --- MongoDB ---
+ 251) echo "MongoDB: Connection failed (server not running)" ;;
+ 252) echo "MongoDB: Authentication failed (bad user/password)" ;;
+ 253) echo "MongoDB: Database not found" ;;
+ 254) echo "MongoDB: Fatal query error" ;;
- # --- Proxmox Custom Codes ---
- 200) echo "Custom: Failed to create lock file" ;;
- 203) echo "Custom: Missing CTID variable" ;;
- 204) echo "Custom: Missing PCT_OSTYPE variable" ;;
- 205) echo "Custom: Invalid CTID (<100)" ;;
- 209) echo "Custom: Container creation failed" ;;
- 210) echo "Custom: Cluster not quorate" ;;
- 214) echo "Custom: Not enough storage space" ;;
- 215) echo "Custom: Container ID not listed" ;;
- 216) echo "Custom: RootFS entry missing in config" ;;
- 217) echo "Custom: Storage does not support rootdir" ;;
- 220) echo "Custom: Unable to resolve template path" ;;
- 222) echo "Custom: Template download failed after 3 attempts" ;;
- 223) echo "Custom: Template not available after download" ;;
- 231) echo "Custom: LXC stack upgrade/retry failed" ;;
+ # --- Proxmox Custom Codes ---
+ 200) echo "Custom: Failed to create lock file" ;;
+ 203) echo "Custom: Missing CTID variable" ;;
+ 204) echo "Custom: Missing PCT_OSTYPE variable" ;;
+ 205) echo "Custom: Invalid CTID (<100)" ;;
+ 206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;;
+ # --- Proxmox Custom Codes ---
+ 200) echo "Custom: Failed to create lock file" ;;
+ 203) echo "Custom: Missing CTID variable" ;;
+ 204) echo "Custom: Missing PCT_OSTYPE variable" ;;
+ 205) echo "Custom: Invalid CTID (<100)" ;;
+ 206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;;
+ 207) echo "Custom: Password contains unescaped special characters (-, /, \\, *, etc.)" ;;
+ 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;;
+ 209) echo "Custom: Container creation failed (check logs for pct create output)" ;;
+ 210) echo "Custom: Cluster not quorate" ;;
+ 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;;
+ 214) echo "Custom: Not enough storage space" ;;
+ 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;;
+ 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;;
+ 217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;;
+ 218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;;
+ 220) echo "Custom: Unable to resolve template path" ;;
+ 221) echo "Custom: Template file exists but not readable (check file permissions)" ;;
+ 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;;
+ 223) echo "Custom: Template not available after download (storage sync issue)" ;;
+ 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;;
+ 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;;
- # --- Default ---
- *) echo "Unknown error" ;;
- esac
+ # --- Default ---
+ *) echo "Unknown error" ;;
+ 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;;
+ 209) echo "Custom: Container creation failed (check logs for pct create output)" ;;
+ 210) echo "Custom: Cluster not quorate" ;;
+ 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;;
+ 214) echo "Custom: Not enough storage space" ;;
+ 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;;
+ 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;;
+ 217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;;
+ 218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;;
+ 220) echo "Custom: Unable to resolve template path" ;;
+ 221) echo "Custom: Template file exists but not readable (check file permissions)" ;;
+ 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;;
+ 223) echo "Custom: Template not available after download (storage sync issue)" ;;
+ 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;;
+ 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;;
+
+ # --- Default ---
+ *) echo "Unknown error" ;;
+ esac
}
-# === Error handler ============================================================
+# ==============================================================================
+# SECTION 2: ERROR HANDLERS
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# error_handler()
+#
+# - Main error handler triggered by ERR trap
+# - Arguments: exit_code, command, line_number
+# - Behavior:
+# * Returns silently if exit_code is 0 (success)
+# * Sources explain_exit_code() for detailed error description
+# * Displays error message with:
+# - Line number where error occurred
+# - Exit code with explanation
+# - Command that failed
+# * Shows last 20 lines of SILENT_LOGFILE if available
+# * Copies log to container /root for later inspection
+# * Exits with original exit code
+# ------------------------------------------------------------------------------
error_handler() {
- local exit_code=${1:-$?}
- local command=${2:-${BASH_COMMAND:-unknown}}
- local line_number=${BASH_LINENO[0]:-unknown}
+ local exit_code=${1:-$?}
+ local command=${2:-${BASH_COMMAND:-unknown}}
+ local line_number=${BASH_LINENO[0]:-unknown}
- command="${command//\$STD/}"
+ command="${command//\$STD/}"
- if [[ "$exit_code" -eq 0 ]]; then
- return 0
- fi
+ if [[ "$exit_code" -eq 0 ]]; then
+ return 0
+ fi
- local explanation
- explanation="$(explain_exit_code "$exit_code")"
+ local explanation
+ explanation="$(explain_exit_code "$exit_code")"
- printf "\e[?25h"
+ printf "\e[?25h"
+
+ # Use msg_error if available, fallback to echo
+ if declare -f msg_error >/dev/null 2>&1; then
+ msg_error "in line ${line_number}: exit code ${exit_code} (${explanation}): while executing command ${command}"
+ else
echo -e "\n${RD}[ERROR]${CL} in line ${RD}${line_number}${CL}: exit code ${RD}${exit_code}${CL} (${explanation}): while executing command ${YWB}${command}${CL}\n"
+ fi
- if [[ -n "${DEBUG_LOGFILE:-}" ]]; then
- {
- echo "------ ERROR ------"
- echo "Timestamp : $(date '+%Y-%m-%d %H:%M:%S')"
- echo "Exit Code : $exit_code ($explanation)"
- echo "Line : $line_number"
- echo "Command : $command"
- echo "-------------------"
- } >>"$DEBUG_LOGFILE"
+ if [[ -n "${DEBUG_LOGFILE:-}" ]]; then
+ {
+ echo "------ ERROR ------"
+ echo "Timestamp : $(date '+%Y-%m-%d %H:%M:%S')"
+ echo "Exit Code : $exit_code ($explanation)"
+ echo "Line : $line_number"
+ echo "Command : $command"
+ echo "-------------------"
+ } >>"$DEBUG_LOGFILE"
+ fi
+
+ # Get active log file (BUILD_LOG or INSTALL_LOG)
+ local active_log=""
+ if declare -f get_active_logfile >/dev/null 2>&1; then
+ active_log="$(get_active_logfile)"
+ elif [[ -n "${SILENT_LOGFILE:-}" ]]; then
+ active_log="$SILENT_LOGFILE"
+ fi
+
+ if [[ -n "$active_log" && -s "$active_log" ]]; then
+ echo "--- Last 20 lines of silent log ---"
+ tail -n 20 "$active_log"
+ echo "-----------------------------------"
+
+ # Detect context: Container (INSTALL_LOG set + /root exists) vs Host (BUILD_LOG)
+ if [[ -n "${INSTALL_LOG:-}" && -d /root ]]; then
+ # CONTAINER CONTEXT: Copy log and create flag file for host
+ local container_log="/root/.install-${SESSION_ID:-error}.log"
+ cp "$active_log" "$container_log" 2>/dev/null || true
+
+ # Create error flag file with exit code for host detection
+ echo "$exit_code" >"/root/.install-${SESSION_ID:-error}.failed" 2>/dev/null || true
+
+ if declare -f msg_custom >/dev/null 2>&1; then
+ msg_custom "📋" "${YW}" "Log saved to: ${container_log}"
+ else
+ echo -e "${YW}Log saved to:${CL} ${BL}${container_log}${CL}"
+ fi
+ else
+ # HOST CONTEXT: Show local log path and offer container cleanup
+ if declare -f msg_custom >/dev/null 2>&1; then
+ msg_custom "📋" "${YW}" "Full log: ${active_log}"
+ else
+ echo -e "${YW}Full log:${CL} ${BL}${active_log}${CL}"
+ fi
+
+ # Offer to remove container if it exists (build errors after container creation)
+ if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then
+ echo ""
+ echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
+
+ if read -t 60 -r response; then
+ if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
+ echo -e "\n${YW}Removing container ${CTID}${CL}"
+ pct stop "$CTID" &>/dev/null || true
+ pct destroy "$CTID" &>/dev/null || true
+ echo -e "${GN}✔${CL} Container ${CTID} removed"
+ elif [[ "$response" =~ ^[Nn]$ ]]; then
+ echo -e "\n${YW}Container ${CTID} kept for debugging${CL}"
+ fi
+ else
+ # Timeout - auto-remove
+ echo -e "\n${YW}No response - auto-removing container${CL}"
+ pct stop "$CTID" &>/dev/null || true
+ pct destroy "$CTID" &>/dev/null || true
+ echo -e "${GN}✔${CL} Container ${CTID} removed"
+ fi
+ fi
fi
+ fi
- if [[ -n "${SILENT_LOGFILE:-}" && -s "$SILENT_LOGFILE" ]]; then
- echo "--- Last 20 lines of silent log ($SILENT_LOGFILE) ---"
- tail -n 20 "$SILENT_LOGFILE"
- echo "---------------------------------------------------"
- fi
-
- exit "$exit_code"
+ exit "$exit_code"
}
-# === Exit handler =============================================================
+# ==============================================================================
+# SECTION 3: SIGNAL HANDLERS
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# on_exit()
+#
+# - EXIT trap handler
+# - Cleans up lock files if lockfile variable is set
+# - Exits with captured exit code
+# - Always runs on script termination (success or failure)
+# ------------------------------------------------------------------------------
on_exit() {
- local exit_code=$?
- [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
- exit "$exit_code"
+ local exit_code=$?
+ [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
+ exit "$exit_code"
}
-# === Signal handlers ==========================================================
+# ------------------------------------------------------------------------------
+# on_interrupt()
+#
+# - SIGINT (Ctrl+C) trap handler
+# - Displays "Interrupted by user" message
+# - Exits with code 130 (128 + SIGINT=2)
+# ------------------------------------------------------------------------------
on_interrupt() {
+ if declare -f msg_error >/dev/null 2>&1; then
+ msg_error "Interrupted by user (SIGINT)"
+ else
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
- exit 130
+ fi
+ exit 130
}
+# ------------------------------------------------------------------------------
+# on_terminate()
+#
+# - SIGTERM trap handler
+# - Displays "Terminated by signal" message
+# - Exits with code 143 (128 + SIGTERM=15)
+# - Triggered by external process termination
+# ------------------------------------------------------------------------------
on_terminate() {
+ if declare -f msg_error >/dev/null 2>&1; then
+ msg_error "Terminated by signal (SIGTERM)"
+ else
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
- exit 143
+ fi
+ exit 143
}
-# === Init traps ===============================================================
+# ==============================================================================
+# SECTION 4: INITIALIZATION
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# catch_errors()
+#
+# - Initializes error handling and signal traps
+# - Enables strict error handling:
+# * set -Ee: Exit on error, inherit ERR trap in functions
+# * set -o pipefail: Pipeline fails if any command fails
+# * set -u: (optional) Exit on undefined variable (if STRICT_UNSET=1)
+# - Sets up traps:
+# * ERR → error_handler
+# * EXIT → on_exit
+# * INT → on_interrupt
+# * TERM → on_terminate
+# - Call this function early in every script
+# ------------------------------------------------------------------------------
catch_errors() {
- set -Ee -o pipefail
- if [ "${STRICT_UNSET:-0}" = "1" ]; then
- set -u
- fi
- trap 'error_handler' ERR
- trap on_exit EXIT
- trap on_interrupt INT
- trap on_terminate TERM
+ set -Ee -o pipefail
+ if [ "${STRICT_UNSET:-0}" = "1" ]; then
+ set -u
+ fi
+
+ trap 'error_handler' ERR
+ trap on_exit EXIT
+ trap on_interrupt INT
+ trap on_terminate TERM
}
diff --git a/misc/install.func b/misc/install.func
index f741b921d..97bd19b8c 100644
--- a/misc/install.func
+++ b/misc/install.func
@@ -4,6 +4,41 @@
# Co-Author: michelroegl-brunner
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
+# ==============================================================================
+# INSTALL.FUNC - CONTAINER INSTALLATION & SETUP
+# ==============================================================================
+#
+# This file provides installation functions executed inside LXC containers
+# after creation. Handles:
+#
+# - Network connectivity verification (IPv4/IPv6)
+# - OS updates and package installation
+# - DNS resolution checks
+# - MOTD and SSH configuration
+# - Container customization and auto-login
+#
+# Usage:
+# - Sourced by -install.sh scripts
+# - Executes via pct exec inside container
+# - Requires internet connectivity
+#
+# ==============================================================================
+
+# ==============================================================================
+# SECTION 1: INITIALIZATION
+# ==============================================================================
+
+# Ensure INSTALL_LOG is set (exported from build.func, but fallback if missing)
+if [[ -z "${INSTALL_LOG:-}" ]]; then
+ INSTALL_LOG="/root/.install-${SESSION_ID:-unknown}.log"
+fi
+
+# Dev mode: Persistent logs directory
+if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then
+ mkdir -p /var/log/community-scripts
+ INSTALL_LOG="/var/log/community-scripts/install-${SESSION_ID:-unknown}-$(date +%Y%m%d_%H%M%S).log"
+fi
+
if ! command -v curl >/dev/null 2>&1; then
printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2
apt-get update >/dev/null 2>&1
@@ -14,7 +49,20 @@ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxV
load_functions
catch_errors
-# This function enables IPv6 if it's not disabled and sets verbose mode
+# Re-parse dev_mode in container context (flags exported from host)
+parse_dev_mode
+
+# ==============================================================================
+# SECTION 2: NETWORK & CONNECTIVITY
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# verb_ip6()
+#
+# - Configures IPv6 based on DISABLEIPV6 variable
+# - If DISABLEIPV6=yes: disables IPv6 via sysctl
+# - Sets verbose mode via set_std_mode()
+# ------------------------------------------------------------------------------
verb_ip6() {
set_std_mode # Set STD mode based on VERBOSE
@@ -24,29 +72,15 @@ verb_ip6() {
fi
}
-# # This function sets error handling options and defines the error_handler function to handle errors
-# catch_errors() {
-# set -Eeuo pipefail
-# trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
-# }
-
-# # This function handles errors
-# error_handler() {
-# source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/api.func)
-# local exit_code="$1"
-# local line_number="$2"
-# local command="${3:-}"
-
-# if [[ "$exit_code" -eq 0 ]]; then
-# return 0
-# fi
-
-# printf "\e[?25h"
-# echo -e "\n${RD}[ERROR]${CL} in line ${RD}${line_number}${CL}: exit code ${RD}${exit_code}${CL}: while executing command ${YW}${command}${CL}\n"
-# exit "$exit_code"
-#}
-
-# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
+# ------------------------------------------------------------------------------
+# setting_up_container()
+#
+# - Verifies network connectivity via hostname -I
+# - Retries up to RETRY_NUM times with RETRY_EVERY seconds delay
+# - Removes Python EXTERNALLY-MANAGED restrictions
+# - Disables systemd-networkd-wait-online.service for faster boot
+# - Exits with error if network unavailable after retries
+# ------------------------------------------------------------------------------
setting_up_container() {
msg_info "Setting up Container OS"
for ((i = RETRY_NUM; i > 0; i--)); do
@@ -68,7 +102,17 @@ setting_up_container() {
msg_ok "Network Connected: ${BL}$(hostname -I)"
}
-# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
+# ------------------------------------------------------------------------------
+# network_check()
+#
+# - Comprehensive network connectivity check for IPv4 and IPv6
+# - Tests connectivity to multiple DNS servers:
+# * IPv4: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9)
+# * IPv6: 2606:4700:4700::1111, 2001:4860:4860::8888, 2620:fe::fe
+# - Verifies DNS resolution for GitHub and Community-Scripts domains
+# - Prompts user to continue if no internet detected
+# - Uses fatal() on DNS resolution failure for critical hosts
+# ------------------------------------------------------------------------------
network_check() {
set +e
trap - ERR
@@ -78,20 +122,23 @@ network_check() {
# Check IPv4 connectivity to Google, Cloudflare & Quad9 DNS servers.
if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then
- msg_ok "IPv4 Internet Connected"
ipv4_connected=true
+ ipv4_status="${GN}✔${CL} IPv4"
else
- msg_error "IPv4 Internet Not Connected"
+ ipv4_status="${RD}✖${CL} IPv4"
fi
# Check IPv6 connectivity to Google, Cloudflare & Quad9 DNS servers.
if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null || ping6 -c 1 -W 1 2620:fe::fe &>/dev/null; then
- msg_ok "IPv6 Internet Connected"
ipv6_connected=true
+ ipv6_status="${GN}✔${CL} IPv6"
else
- msg_error "IPv6 Internet Not Connected"
+ ipv6_status="${RD}✖${CL} IPv6"
fi
+ # Show combined status
+ msg_ok "Internet: ${ipv4_status} ${ipv6_status}"
+
# If both IPv4 and IPv6 checks fail, prompt the user
if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then
read -r -p "No Internet detected, would you like to continue anyway? " prompt
@@ -128,7 +175,19 @@ network_check() {
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
}
-# This function updates the Container OS by running apt-get update and upgrade
+# ==============================================================================
+# SECTION 3: OS UPDATE & PACKAGE MANAGEMENT
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# update_os()
+#
+# - Updates container OS via apt-get update and dist-upgrade
+# - Configures APT cacher proxy if CACHER=yes (accelerates package downloads)
+# - Removes Python EXTERNALLY-MANAGED restrictions for pip
+# - Sources tools.func for additional setup functions after update
+# - Uses $STD wrapper to suppress output unless VERBOSE=yes
+# ------------------------------------------------------------------------------
update_os() {
msg_info "Updating Container OS"
if [[ "$CACHER" == "yes" ]]; then
@@ -150,7 +209,24 @@ EOF
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func)
}
-# This function modifies the message of the day (motd) and SSH settings
+# ==============================================================================
+# SECTION 4: MOTD & SSH CONFIGURATION
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# motd_ssh()
+#
+# - Configures Message of the Day (MOTD) with container information
+# - Creates /etc/profile.d/00_lxc-details.sh with:
+# * Application name
+# * Warning banner (DEV repository)
+# * OS name and version
+# * Hostname and IP address
+# * GitHub repository link
+# - Disables executable flag on /etc/update-motd.d/* scripts
+# - Enables root SSH access if SSH_ROOT=yes
+# - Configures TERM environment variable for better terminal support
+# ------------------------------------------------------------------------------
motd_ssh() {
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
@@ -180,7 +256,19 @@ motd_ssh() {
fi
}
-# This function customizes the container by modifying the getty service and enabling auto-login for the root user
+# ==============================================================================
+# SECTION 5: CONTAINER CUSTOMIZATION
+# ==============================================================================
+
+# ------------------------------------------------------------------------------
+# customize()
+#
+# - Customizes container for passwordless root login if PASSWORD is empty
+# - Configures getty for auto-login via /etc/systemd/system/container-getty@1.service.d/override.conf
+# - Creates /usr/bin/update script for easy application updates
+# - Injects SSH authorized keys if SSH_AUTHORIZED_KEY variable is set
+# - Sets proper permissions on SSH directories and key files
+# ------------------------------------------------------------------------------
customize() {
if [[ "$PASSWORD" == "" ]]; then
msg_info "Customizing Container"
@@ -191,8 +279,7 @@ customize() {
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM
EOF
- systemctl daemon-reload
- systemctl restart $(basename $(dirname $GETTY_OVERRIDE) | sed 's/\.d//')
+ $STD systemctl daemon-reload || true
msg_ok "Customized Container"
fi
echo "bash -c \"\$(curl -fsSL https://github.com/community-scripts/ProxmoxVED/raw/main/ct/${app}.sh)\"" >/usr/bin/update
diff --git a/misc/optimize_build_func.py b/misc/optimize_build_func.py
new file mode 100644
index 000000000..92fe03000
--- /dev/null
+++ b/misc/optimize_build_func.py
@@ -0,0 +1,508 @@
+#!/usr/bin/env python3
+"""
+Build.func Optimizer
+====================
+Optimizes the build.func file by:
+- Removing duplicate functions
+- Sorting and grouping functions logically
+- Adding section headers
+- Improving readability
+"""
+
+import re
+import sys
+from pathlib import Path
+from datetime import datetime
+from typing import List, Tuple, Dict
+
+# ==============================================================================
+# CONFIGURATION
+# ==============================================================================
+
+# Define function groups in desired order
+FUNCTION_GROUPS = {
+ "CORE_INIT": {
+ "title": "CORE INITIALIZATION & VARIABLES",
+ "functions": [
+ "variables",
+ ]
+ },
+ "DEPENDENCIES": {
+ "title": "DEPENDENCY LOADING",
+ "functions": [
+ # Bootstrap loader section (commented code)
+ ]
+ },
+ "VALIDATION": {
+ "title": "SYSTEM VALIDATION & CHECKS",
+ "functions": [
+ "maxkeys_check",
+ "check_container_resources",
+ "check_container_storage",
+ "check_nvidia_host_setup",
+ "check_storage_support",
+ ]
+ },
+ "NETWORK": {
+ "title": "NETWORK & IP MANAGEMENT",
+ "functions": [
+ "get_current_ip",
+ "update_motd_ip",
+ ]
+ },
+ "SSH": {
+ "title": "SSH KEY MANAGEMENT",
+ "functions": [
+ "find_host_ssh_keys",
+ "ssh_discover_default_files",
+ "ssh_extract_keys_from_file",
+ "ssh_build_choices_from_files",
+ "configure_ssh_settings",
+ "install_ssh_keys_into_ct",
+ ]
+ },
+ "SETTINGS": {
+ "title": "SETTINGS & CONFIGURATION",
+ "functions": [
+ "base_settings",
+ "echo_default",
+ "exit_script",
+ "advanced_settings",
+ "diagnostics_check",
+ "diagnostics_menu",
+ "default_var_settings",
+ "ensure_global_default_vars_file",
+ "settings_menu",
+ "edit_default_storage",
+ ]
+ },
+ "DEFAULTS": {
+ "title": "DEFAULTS MANAGEMENT (VAR_* FILES)",
+ "functions": [
+ "get_app_defaults_path",
+ "_is_whitelisted_key",
+ "_sanitize_value",
+ "_load_vars_file",
+ "_load_vars_file_to_map",
+ "_build_vars_diff",
+ "_build_current_app_vars_tmp",
+ "maybe_offer_save_app_defaults",
+ "ensure_storage_selection_for_vars_file",
+ ]
+ },
+ "STORAGE": {
+ "title": "STORAGE DISCOVERY & SELECTION",
+ "functions": [
+ "resolve_storage_preselect",
+ "select_storage",
+ "choose_and_set_storage_for_file",
+ "_write_storage_to_vars",
+ ]
+ },
+ "GPU": {
+ "title": "GPU & HARDWARE PASSTHROUGH",
+ "functions": [
+ "is_gpu_app",
+ "detect_gpu_devices",
+ "configure_gpu_passthrough",
+ "configure_usb_passthrough",
+ "configure_additional_devices",
+ "fix_gpu_gids",
+ "get_container_gid",
+ ]
+ },
+ "CONTAINER": {
+ "title": "CONTAINER LIFECYCLE & CREATION",
+ "functions": [
+ "create_lxc_container",
+ "offer_lxc_stack_upgrade_and_maybe_retry",
+ "parse_template_osver",
+ "pkg_ver",
+ "pkg_cand",
+ "ver_ge",
+ "ver_gt",
+ "ver_lt",
+ "build_container",
+ "destroy_lxc",
+ "description",
+ ]
+ },
+ "MAIN": {
+ "title": "MAIN ENTRY POINTS & ERROR HANDLING",
+ "functions": [
+ "install_script",
+ "start",
+ "api_exit_script",
+ ]
+ },
+}
+
+# Functions to exclude from duplication check (intentionally similar)
+EXCLUDE_FROM_DEDUP = {
+ "_load_vars_file",
+ "_load_vars_file_to_map",
+}
+
+# ==============================================================================
+# HELPER FUNCTIONS
+# ==============================================================================
+
+def extract_functions(content: str) -> Dict[str, Tuple[str, int, int]]:
+ """
+ Extract all function definitions from the content.
+ Returns dict: {function_name: (full_code, start_line, end_line)}
+ """
+ functions = {}
+ lines = content.split('\n')
+
+ i = 0
+ while i < len(lines):
+ line = lines[i]
+
+ # Match function definition: function_name() {
+ match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\)\s*\{', line)
+ if match:
+ func_name = match.group(1)
+ start_line = i
+
+ # Find function end by counting braces
+ brace_count = 1
+ func_lines = [line]
+ i += 1
+
+ while i < len(lines) and brace_count > 0:
+ current_line = lines[i]
+ func_lines.append(current_line)
+
+ # Count braces (simple method, doesn't handle strings/comments perfectly)
+ brace_count += current_line.count('{') - current_line.count('}')
+ i += 1
+
+ end_line = i
+ functions[func_name] = ('\n'.join(func_lines), start_line, end_line)
+ continue
+
+ i += 1
+
+ return functions
+
+def extract_header_comments(content: str, func_name: str, func_code: str) -> str:
+ """Extract comment block before function if exists"""
+ lines = content.split('\n')
+
+ # Find function start in original content
+ for i, line in enumerate(lines):
+ if line.strip().startswith(f"{func_name}()"):
+ # Look backwards for comment block
+ comments = []
+ j = i - 1
+ while j >= 0:
+ prev_line = lines[j]
+ stripped = prev_line.strip()
+
+ # SKIP section headers and copyright - we add our own
+ if (stripped.startswith('# ===') or
+ stripped.startswith('#!/usr/bin/env') or
+ 'Copyright' in stripped or
+ 'Author:' in stripped or
+ 'License:' in stripped or
+ 'Revision:' in stripped or
+ 'SECTION' in stripped):
+ j -= 1
+ continue
+
+ # Include function-specific comment lines
+ if (stripped.startswith('# ---') or
+ stripped.startswith('#')):
+ comments.insert(0, prev_line)
+ j -= 1
+ elif stripped == '':
+ # Keep collecting through empty lines
+ comments.insert(0, prev_line)
+ j -= 1
+ else:
+ break
+
+ # Remove leading empty lines from comments
+ while comments and comments[0].strip() == '':
+ comments.pop(0)
+
+ # Remove trailing empty lines from comments
+ while comments and comments[-1].strip() == '':
+ comments.pop()
+
+ if comments:
+ return '\n'.join(comments) + '\n'
+
+ return ''
+
+def find_duplicate_functions(functions: Dict[str, Tuple[str, int, int]]) -> List[str]:
+ """Find duplicate function definitions"""
+ seen = {}
+ duplicates = []
+
+ for func_name, (code, start, end) in functions.items():
+ if func_name in EXCLUDE_FROM_DEDUP:
+ continue
+
+ # Normalize code for comparison (remove whitespace variations)
+ normalized = re.sub(r'\s+', ' ', code).strip()
+
+ if normalized in seen:
+ duplicates.append(func_name)
+ print(f" ⚠️ Duplicate found: {func_name} (also defined as {seen[normalized]})")
+ else:
+ seen[normalized] = func_name
+
+ return duplicates
+
+def create_section_header(title: str) -> str:
+ """Create a formatted section header"""
+ return f"""
+# ==============================================================================
+# {title}
+# ==============================================================================
+"""
+
+def get_function_group(func_name: str) -> str:
+ """Determine which group a function belongs to"""
+ for group_key, group_data in FUNCTION_GROUPS.items():
+ if func_name in group_data["functions"]:
+ return group_key
+ return "UNKNOWN"
+
+# ==============================================================================
+# MAIN OPTIMIZATION LOGIC
+# ==============================================================================
+
+def optimize_build_func(input_file: Path, output_file: Path):
+ """Main optimization function"""
+
+ print("=" * 80)
+ print("BUILD.FUNC OPTIMIZER")
+ print("=" * 80)
+ print()
+
+ # Read input file
+ print(f"📖 Reading: {input_file}")
+ content = input_file.read_text(encoding='utf-8')
+ original_lines = len(content.split('\n'))
+ print(f" Lines: {original_lines:,}")
+ print()
+
+ # Extract functions
+ print("🔍 Extracting functions...")
+ functions = extract_functions(content)
+ print(f" Found {len(functions)} functions")
+ print()
+
+ # Find duplicates
+ print("🔎 Checking for duplicates...")
+ duplicates = find_duplicate_functions(functions)
+ if duplicates:
+ print(f" Found {len(duplicates)} duplicate(s)")
+ else:
+ print(" ✓ No duplicates found")
+ print()
+
+ # Extract header (copyright, etc)
+ print("📝 Extracting file header...")
+ lines = content.split('\n')
+ header_lines = []
+
+ # Extract only the first copyright block
+ in_header = True
+ for i, line in enumerate(lines):
+ if in_header:
+ # Keep copyright and license lines
+ if (line.strip().startswith('#!') or
+ line.strip().startswith('# Copyright') or
+ line.strip().startswith('# Author:') or
+ line.strip().startswith('# License:') or
+ line.strip().startswith('# Revision:') or
+ line.strip() == ''):
+ header_lines.append(line)
+ else:
+ in_header = False
+ break
+
+ # Remove trailing empty lines
+ while header_lines and header_lines[-1].strip() == '':
+ header_lines.pop()
+
+ header = '\n'.join(header_lines)
+ print()
+
+ # Build optimized content
+ print("🔨 Building optimized structure...")
+
+ optimized_parts = [header]
+
+ # Group functions
+ grouped_functions = {key: [] for key in FUNCTION_GROUPS.keys()}
+ grouped_functions["UNKNOWN"] = []
+
+ for func_name, (func_code, start, end) in functions.items():
+ if func_name in duplicates:
+ continue # Skip duplicates
+
+ group = get_function_group(func_name)
+
+ # Extract comments before function
+ comments = extract_header_comments(content, func_name, func_code)
+
+ grouped_functions[group].append((func_name, comments + func_code))
+
+ # Add grouped sections
+ for group_key, group_data in FUNCTION_GROUPS.items():
+ if grouped_functions[group_key]:
+ optimized_parts.append(create_section_header(group_data["title"]))
+
+ for func_name, func_code in grouped_functions[group_key]:
+ optimized_parts.append(func_code)
+ optimized_parts.append('') # Empty line between functions
+
+ # Add unknown functions at the end
+ if grouped_functions["UNKNOWN"]:
+ optimized_parts.append(create_section_header("UNCATEGORIZED FUNCTIONS"))
+ print(f" ⚠️ {len(grouped_functions['UNKNOWN'])} uncategorized functions:")
+ for func_name, func_code in grouped_functions["UNKNOWN"]:
+ print(f" - {func_name}")
+ optimized_parts.append(func_code)
+ optimized_parts.append('')
+
+ # Add any remaining non-function code (bootstrap, source commands, traps, etc)
+ print("📌 Adding remaining code...")
+
+ # Extract bootstrap/source section
+ bootstrap_lines = []
+ trap_lines = []
+ other_lines = []
+
+ in_function = False
+ brace_count = 0
+ in_bootstrap_comment = False
+
+ for line in lines:
+ stripped = line.strip()
+
+ # Skip the header we already extracted
+ if (stripped.startswith('#!/usr/bin/env bash') or
+ stripped.startswith('# Copyright') or
+ stripped.startswith('# Author:') or
+ stripped.startswith('# License:') or
+ stripped.startswith('# Revision:')):
+ continue
+
+ # Check if we're in a function
+ if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)\s*\{', line):
+ in_function = True
+ brace_count = 1
+ elif in_function:
+ brace_count += line.count('{') - line.count('}')
+ if brace_count == 0:
+ in_function = False
+ elif not in_function:
+ # Collect non-function lines
+
+ # Bootstrap/loader section
+ if ('Community-Scripts bootstrap' in line or
+ 'Load core' in line or
+ in_bootstrap_comment):
+ bootstrap_lines.append(line)
+ if '# ---' in line or '# ===' in line:
+ in_bootstrap_comment = not in_bootstrap_comment
+ continue
+
+ # Source commands
+ if (stripped.startswith('source <(') or
+ stripped.startswith('if command -v curl') or
+ stripped.startswith('elif command -v wget') or
+ 'load_functions' in stripped or
+ 'catch_errors' in stripped):
+ bootstrap_lines.append(line)
+ continue
+
+ # Traps
+ if stripped.startswith('trap '):
+ trap_lines.append(line)
+ continue
+
+ # VAR_WHITELIST declaration
+ if 'declare -ag VAR_WHITELIST' in line or (other_lines and 'VAR_WHITELIST' in other_lines[-1]):
+ other_lines.append(line)
+ continue
+
+ # Empty lines between sections - keep some
+ if stripped == '' and (bootstrap_lines or trap_lines or other_lines):
+ if bootstrap_lines and bootstrap_lines[-1].strip() != '':
+ bootstrap_lines.append(line)
+ elif trap_lines and trap_lines[-1].strip() != '':
+ trap_lines.append(line)
+
+ # Add bootstrap section if exists
+ if bootstrap_lines:
+ optimized_parts.append(create_section_header("DEPENDENCY LOADING"))
+ optimized_parts.extend(bootstrap_lines)
+ optimized_parts.append('')
+
+ # Add other declarations
+ if other_lines:
+ optimized_parts.extend(other_lines)
+ optimized_parts.append('')
+
+ # Write output
+ optimized_content = '\n'.join(optimized_parts)
+ optimized_lines = len(optimized_content.split('\n'))
+
+ print()
+ print(f"💾 Writing optimized file: {output_file}")
+ output_file.write_text(optimized_content, encoding='utf-8')
+
+ print()
+ print("=" * 80)
+ print("✅ OPTIMIZATION COMPLETE")
+ print("=" * 80)
+ print(f"Original lines: {original_lines:,}")
+ print(f"Optimized lines: {optimized_lines:,}")
+ print(f"Difference: {original_lines - optimized_lines:+,}")
+ print(f"Functions: {len(functions) - len(duplicates)}")
+ print(f"Duplicates removed: {len(duplicates)}")
+ print()
+
+# ==============================================================================
+# ENTRY POINT
+# ==============================================================================
+
+def main():
+ """Main entry point"""
+
+ # Set paths
+ script_dir = Path(__file__).parent
+ input_file = script_dir / "build.func"
+
+ # Create backup first
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
+ backup_file = script_dir / f"build.func.backup-{timestamp}"
+
+ if not input_file.exists():
+ print(f"❌ Error: {input_file} not found!")
+ sys.exit(1)
+
+ print(f"📦 Creating backup: {backup_file.name}")
+ backup_file.write_text(input_file.read_text(encoding='utf-8'), encoding='utf-8')
+ print()
+
+ # Optimize
+ output_file = script_dir / "build.func.optimized"
+ optimize_build_func(input_file, output_file)
+
+ print("📋 Next steps:")
+ print(f" 1. Review: {output_file.name}")
+ print(f" 2. Test the optimized version")
+ print(f" 3. If OK: mv build.func.optimized build.func")
+ print(f" 4. Backup available at: {backup_file.name}")
+ print()
+
+if __name__ == "__main__":
+ main()
diff --git a/misc/tools.func b/misc/tools.func
index 1914bc894..0a37d9422 100644
--- a/misc/tools.func
+++ b/misc/tools.func
@@ -3,6 +3,32 @@
# ==============================================================================
# HELPER FUNCTIONS FOR PACKAGE MANAGEMENT
# ==============================================================================
+#
+# This file provides unified helper functions for robust package installation
+# and repository management across Debian/Ubuntu OS upgrades.
+#
+# Key Features:
+# - Automatic retry logic for transient APT/network failures
+# - Unified keyring cleanup from all 3 locations
+# - Legacy installation cleanup (nvm, rbenv, rustup)
+# - OS-upgrade-safe repository preparation
+# - Service pattern matching for multi-version tools
+#
+# Usage in install scripts:
+# source /dev/stdin <<< "$FUNCTIONS" # Load from build.func
+# prepare_repository_setup "mysql"
+# install_packages_with_retry "mysql-server" "mysql-client"
+#
+# Quick Reference (Core Helpers):
+# cleanup_tool_keyrings() - Remove keyrings from all 3 locations
+# stop_all_services() - Stop services by pattern (e.g. "php*-fpm")
+# verify_tool_version() - Validate installed version matches expected
+# cleanup_legacy_install() - Remove nvm, rbenv, rustup, etc.
+# prepare_repository_setup() - Cleanup repos + keyrings + validate APT
+# install_packages_with_retry() - Install with 3 retries and APT refresh
+# upgrade_packages_with_retry() - Upgrade with 3 retries and APT refresh
+#
+# ==============================================================================
# ------------------------------------------------------------------------------
# Cache installed version to avoid repeated checks
@@ -24,6 +50,176 @@ get_cached_version() {
return 0
}
+# ------------------------------------------------------------------------------
+# Clean up ALL keyring locations for a tool (unified helper)
+# Usage: cleanup_tool_keyrings "mariadb" "mysql" "postgresql"
+# ------------------------------------------------------------------------------
+cleanup_tool_keyrings() {
+ local tool_patterns=("$@")
+
+ for pattern in "${tool_patterns[@]}"; do
+ rm -f /usr/share/keyrings/${pattern}*.gpg \
+ /etc/apt/keyrings/${pattern}*.gpg \
+ /etc/apt/trusted.gpg.d/${pattern}*.gpg 2>/dev/null || true
+ done
+}
+
+# ------------------------------------------------------------------------------
+# Stop and disable all service instances matching a pattern
+# Usage: stop_all_services "php*-fpm" "mysql" "mariadb"
+# ------------------------------------------------------------------------------
+stop_all_services() {
+ local service_patterns=("$@")
+
+ for pattern in "${service_patterns[@]}"; do
+ # Find all matching services
+ systemctl list-units --type=service --all 2>/dev/null |
+ grep -oE "${pattern}[^ ]*\.service" |
+ sort -u |
+ while read -r service; do
+ $STD systemctl stop "$service" 2>/dev/null || true
+ $STD systemctl disable "$service" 2>/dev/null || true
+ done
+ done
+}
+
+# ------------------------------------------------------------------------------
+# Verify installed tool version matches expected version
+# Returns: 0 if match, 1 if mismatch (with warning)
+# Usage: verify_tool_version "nodejs" "22" "$(node -v | grep -oP '^v\K[0-9]+')"
+# ------------------------------------------------------------------------------
+verify_tool_version() {
+ local tool_name="$1"
+ local expected_version="$2"
+ local installed_version="$3"
+
+ # Extract major version for comparison
+ local expected_major="${expected_version%%.*}"
+ local installed_major="${installed_version%%.*}"
+
+ if [[ "$installed_major" != "$expected_major" ]]; then
+ msg_warn "$tool_name version mismatch: expected $expected_version, got $installed_version"
+ return 1
+ fi
+
+ return 0
+}
+
+# ------------------------------------------------------------------------------
+# Clean up legacy installation methods (nvm, rbenv, rustup, etc.)
+# Usage: cleanup_legacy_install "nodejs" -> removes nvm
+# ------------------------------------------------------------------------------
+cleanup_legacy_install() {
+ local tool_name="$1"
+
+ case "$tool_name" in
+ nodejs | node)
+ if [[ -d "$HOME/.nvm" ]]; then
+ msg_info "Removing legacy nvm installation"
+ rm -rf "$HOME/.nvm" "$HOME/.npm" "$HOME/.bower" "$HOME/.config/yarn" 2>/dev/null || true
+ sed -i '/NVM_DIR/d' "$HOME/.bashrc" "$HOME/.profile" 2>/dev/null || true
+ msg_ok "Legacy nvm installation removed"
+ fi
+ ;;
+ ruby)
+ if [[ -d "$HOME/.rbenv" ]]; then
+ msg_info "Removing legacy rbenv installation"
+ rm -rf "$HOME/.rbenv" 2>/dev/null || true
+ sed -i '/rbenv/d' "$HOME/.bashrc" "$HOME/.profile" 2>/dev/null || true
+ msg_ok "Legacy rbenv installation removed"
+ fi
+ ;;
+ rust)
+ if [[ -d "$HOME/.cargo" ]] || [[ -d "$HOME/.rustup" ]]; then
+ msg_info "Removing legacy rustup installation"
+ rm -rf "$HOME/.cargo" "$HOME/.rustup" 2>/dev/null || true
+ sed -i '/cargo/d' "$HOME/.bashrc" "$HOME/.profile" 2>/dev/null || true
+ msg_ok "Legacy rustup installation removed"
+ fi
+ ;;
+ go | golang)
+ if [[ -d "$HOME/go" ]]; then
+ msg_info "Removing legacy Go workspace"
+ # Keep user code, just remove GOPATH env
+ sed -i '/GOPATH/d' "$HOME/.bashrc" "$HOME/.profile" 2>/dev/null || true
+ msg_ok "Legacy Go workspace cleaned"
+ fi
+ ;;
+ esac
+}
+
+# ------------------------------------------------------------------------------
+# Unified repository preparation before setup
+# Cleans up old repos, keyrings, and ensures APT is working
+# Usage: prepare_repository_setup "mariadb" "mysql"
+# ------------------------------------------------------------------------------
+prepare_repository_setup() {
+ local repo_names=("$@")
+
+ # Clean up all old repository files
+ for repo in "${repo_names[@]}"; do
+ cleanup_old_repo_files "$repo"
+ done
+
+ # Clean up all keyrings
+ cleanup_tool_keyrings "${repo_names[@]}"
+
+ # Ensure APT is in working state
+ ensure_apt_working || return 1
+
+ return 0
+}
+
+# ------------------------------------------------------------------------------
+# Install packages with retry logic
+# Usage: install_packages_with_retry "mysql-server" "mysql-client"
+# ------------------------------------------------------------------------------
+install_packages_with_retry() {
+ local packages=("$@")
+ local max_retries=2
+ local retry=0
+
+ while [[ $retry -le $max_retries ]]; do
+ if $STD apt install -y "${packages[@]}" 2>/dev/null; then
+ return 0
+ fi
+
+ retry=$((retry + 1))
+ if [[ $retry -le $max_retries ]]; then
+ msg_warn "Package installation failed, retrying ($retry/$max_retries)..."
+ sleep 2
+ $STD apt update 2>/dev/null || true
+ fi
+ done
+
+ return 1
+}
+
+# ------------------------------------------------------------------------------
+# Upgrade specific packages with retry logic
+# Usage: upgrade_packages_with_retry "mariadb-server" "mariadb-client"
+# ------------------------------------------------------------------------------
+upgrade_packages_with_retry() {
+ local packages=("$@")
+ local max_retries=2
+ local retry=0
+
+ while [[ $retry -le $max_retries ]]; do
+ if $STD apt install --only-upgrade -y "${packages[@]}" 2>/dev/null; then
+ return 0
+ fi
+
+ retry=$((retry + 1))
+ if [[ $retry -le $max_retries ]]; then
+ msg_warn "Package upgrade failed, retrying ($retry/$max_retries)..."
+ sleep 2
+ $STD apt update 2>/dev/null || true
+ fi
+ done
+
+ return 1
+}
+
# ------------------------------------------------------------------------------
# Check if tool is already installed and optionally verify exact version
# Returns: 0 if installed (with optional version match), 1 if not installed
@@ -110,57 +306,70 @@ remove_old_tool_version() {
case "$tool_name" in
mariadb)
- $STD systemctl stop mariadb >/dev/null 2>&1 || true
+ stop_all_services "mariadb"
$STD apt purge -y 'mariadb*' >/dev/null 2>&1 || true
+ cleanup_tool_keyrings "mariadb"
;;
mysql)
- $STD systemctl stop mysql >/dev/null 2>&1 || true
+ stop_all_services "mysql"
$STD apt purge -y 'mysql*' >/dev/null 2>&1 || true
- rm -rf /var/lib/mysql >/dev/null 2>&1 || true
+ rm -rf /var/lib/mysql 2>/dev/null || true
+ cleanup_tool_keyrings "mysql"
;;
mongodb)
- $STD systemctl stop mongod >/dev/null 2>&1 || true
+ stop_all_services "mongod"
$STD apt purge -y 'mongodb*' >/dev/null 2>&1 || true
- rm -rf /var/lib/mongodb >/dev/null 2>&1 || true
+ rm -rf /var/lib/mongodb 2>/dev/null || true
+ cleanup_tool_keyrings "mongodb"
;;
node | nodejs)
$STD apt purge -y nodejs npm >/dev/null 2>&1 || true
- npm list -g 2>/dev/null | grep -oE '^ \S+' | awk '{print $1}' | while read -r module; do
- npm uninstall -g "$module" >/dev/null 2>&1 || true
- done
+ # Clean up npm global modules
+ if command -v npm >/dev/null 2>&1; then
+ npm list -g 2>/dev/null | grep -oE '^ \S+' | awk '{print $1}' | while read -r module; do
+ npm uninstall -g "$module" >/dev/null 2>&1 || true
+ done
+ fi
+ cleanup_legacy_install "nodejs"
+ cleanup_tool_keyrings "nodesource"
;;
php)
- # Disable PHP-FPM if running
- $STD systemctl disable php*-fpm >/dev/null 2>&1 || true
- $STD systemctl stop php*-fpm >/dev/null 2>&1 || true
+ stop_all_services "php.*-fpm"
$STD apt purge -y 'php*' >/dev/null 2>&1 || true
- rm -rf /etc/php >/dev/null 2>&1 || true
+ rm -rf /etc/php 2>/dev/null || true
+ cleanup_tool_keyrings "deb.sury.org-php" "php"
;;
postgresql)
- $STD systemctl stop postgresql >/dev/null 2>&1 || true
+ stop_all_services "postgresql"
$STD apt purge -y 'postgresql*' >/dev/null 2>&1 || true
- rm -rf /var/lib/postgresql >/dev/null 2>&1 || true
+ # Keep data directory for safety (can be removed manually if needed)
+ # rm -rf /var/lib/postgresql 2>/dev/null || true
+ cleanup_tool_keyrings "postgresql" "pgdg"
+ ;;
+ java)
+ $STD apt purge -y 'temurin*' 'adoptium*' 'openjdk*' >/dev/null 2>&1 || true
+ cleanup_tool_keyrings "adoptium"
;;
ruby)
- if [[ -d "$HOME/.rbenv" ]]; then
- rm -rf "$HOME/.rbenv"
- fi
+ cleanup_legacy_install "ruby"
$STD apt purge -y 'ruby*' >/dev/null 2>&1 || true
;;
rust)
- rm -rf "$HOME/.cargo" "$HOME/.rustup" >/dev/null 2>&1 || true
+ cleanup_legacy_install "rust"
;;
go | golang)
- rm -rf /usr/local/go >/dev/null 2>&1 || true
+ rm -rf /usr/local/go 2>/dev/null || true
+ cleanup_legacy_install "golang"
;;
clickhouse)
- $STD systemctl stop clickhouse-server >/dev/null 2>&1 || true
+ stop_all_services "clickhouse-server"
$STD apt purge -y 'clickhouse*' >/dev/null 2>&1 || true
- rm -rf /var/lib/clickhouse >/dev/null 2>&1 || true
+ rm -rf /var/lib/clickhouse 2>/dev/null || true
+ cleanup_tool_keyrings "clickhouse"
;;
esac
- # Clean up old repositories
+ # Clean up old repository files (both .list and .sources)
cleanup_old_repo_files "$repo_name"
return 0
@@ -218,7 +427,12 @@ manage_tool_repository() {
suite=$(get_fallback_suite "$distro_id" "$distro_codename" "$repo_url/$distro_id")
# Setup new repository using deb822 format
- setup_deb822_repo "mariadb" "$gpg_key_url" "$repo_url/$distro_id" "$suite" "main" "amd64 arm64" || return 1
+ setup_deb822_repo \
+ "mariadb" \
+ "$gpg_key_url" \
+ "$repo_url/$distro_id" \
+ "$suite" \
+ "main"
return 0
;;
@@ -241,7 +455,51 @@ manage_tool_repository() {
# Setup repository
local distro_codename
distro_codename=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)
- suite=$(get_fallback_suite "$distro_id" "$distro_codename" "$repo_url")
+
+ # Suite mapping with fallback for newer releases not yet supported by upstream
+ if [[ "$distro_id" == "debian" ]]; then
+ case "$distro_codename" in
+ trixie | forky | sid)
+ # Testing/unstable releases fallback to latest stable suite
+ suite="bookworm"
+ ;;
+ bookworm)
+ suite="bookworm"
+ ;;
+ bullseye)
+ suite="bullseye"
+ ;;
+ *)
+ # Unknown release: fallback to latest stable suite
+ msg_warn "Unknown Debian release '${distro_codename}', using bookworm"
+ suite="bookworm"
+ ;;
+ esac
+ elif [[ "$distro_id" == "ubuntu" ]]; then
+ case "$distro_codename" in
+ oracular | plucky)
+ # Newer releases fallback to latest LTS
+ suite="noble"
+ ;;
+ noble)
+ suite="noble"
+ ;;
+ jammy)
+ suite="jammy"
+ ;;
+ focal)
+ suite="focal"
+ ;;
+ *)
+ # Unknown release: fallback to latest LTS
+ msg_warn "Unknown Ubuntu release '${distro_codename}', using noble"
+ suite="noble"
+ ;;
+ esac
+ else
+ # For other distros, try generic fallback
+ suite=$(get_fallback_suite "$distro_id" "$distro_codename" "$repo_url")
+ fi
repo_component="main"
[[ "$distro_id" == "ubuntu" ]] && repo_component="multiverse"
@@ -251,7 +509,7 @@ Types: deb
URIs: ${repo_url}
Suites: ${suite}/mongodb-org/${version}
Components: ${repo_component}
-Architectures: amd64 arm64
+Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/mongodb-server-${version}.gpg
EOF
return 0
@@ -283,7 +541,7 @@ Types: deb
URIs: $repo_url
Suites: nodistro
Components: main
-Architectures: amd64 arm64
+Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/nodesource.gpg
EOF
return 0
@@ -317,7 +575,7 @@ Types: deb
URIs: https://packages.sury.org/php
Suites: $distro_codename
Components: main
-Architectures: amd64 arm64
+Architectures: $(dpkg --print-architecture)
Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg
EOF
return 0
@@ -348,7 +606,7 @@ Types: deb
URIs: http://apt.postgresql.org/pub/repos/apt
Suites: $distro_codename-pgdg
Components: main
-Architectures: amd64 arm64
+Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/postgresql.gpg
EOF
return 0
@@ -924,13 +1182,13 @@ ensure_apt_working() {
cleanup_orphaned_sources
# Try to update package lists
- if ! apt update -qq 2>/dev/null; then
+ if ! $STD apt update; then
# More aggressive cleanup
rm -f /etc/apt/sources.list.d/*.sources 2>/dev/null || true
cleanup_orphaned_sources
# Try again
- if ! apt update -qq 2>/dev/null; then
+ if ! $STD apt update; then
msg_error "Cannot update package lists - APT is critically broken"
return 1
fi
@@ -940,8 +1198,8 @@ ensure_apt_working() {
}
# ------------------------------------------------------------------------------
-# Standardized deb822 repository setup
-# Validates all parameters and fails safely if any are empty
+# Standardized deb822 repository setup (with optional Architectures)
+# Always runs apt update after repo creation to ensure package availability
# ------------------------------------------------------------------------------
setup_deb822_repo() {
local name="$1"
@@ -949,56 +1207,40 @@ setup_deb822_repo() {
local repo_url="$3"
local suite="$4"
local component="${5:-main}"
- local architectures="${6:-amd64 arm64}"
+ local architectures="${6-}" # optional
# Validate required parameters
if [[ -z "$name" || -z "$gpg_url" || -z "$repo_url" || -z "$suite" ]]; then
- msg_error "setup_deb822_repo: missing required parameters (name=$name, gpg=$gpg_url, repo=$repo_url, suite=$suite)"
+ msg_error "setup_deb822_repo: missing required parameters (name=$name repo=$repo_url suite=$suite)"
return 1
fi
- # Cleanup old configs for this app
+ # Cleanup
cleanup_old_repo_files "$name"
-
- # Cleanup any orphaned .sources files from other apps
cleanup_orphaned_sources
- # Ensure keyring directory exists
mkdir -p /etc/apt/keyrings || {
- msg_error "Failed to create /etc/apt/keyrings directory"
+ msg_error "Failed to create /etc/apt/keyrings"
return 1
}
- # Download GPG key (with --yes to avoid interactive prompts)
- curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" 2>/dev/null || {
- msg_error "Failed to download or import GPG key for ${name} from $gpg_url"
+ # Import GPG
+ curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" || {
+ msg_error "Failed to import GPG key for ${name}"
return 1
}
- # Create deb822 sources file
- cat </etc/apt/sources.list.d/${name}.sources
-Types: deb
-URIs: $repo_url
-Suites: $suite
-Components: $component
-Architectures: $architectures
-Signed-By: /etc/apt/keyrings/${name}.gpg
-EOF
+ # Write deb822
+ {
+ echo "Types: deb"
+ echo "URIs: $repo_url"
+ echo "Suites: $suite"
+ echo "Components: $component"
+ [[ -n "$architectures" ]] && echo "Architectures: $architectures"
+ echo "Signed-By: /etc/apt/keyrings/${name}.gpg"
+ } >/etc/apt/sources.list.d/${name}.sources
- # Use cached apt update
- local apt_cache_file="/var/cache/apt-update-timestamp"
- local current_time=$(date +%s)
- local last_update=0
-
- if [[ -f "$apt_cache_file" ]]; then
- last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0)
- fi
-
- # For repo changes, always update but respect short-term cache (30s)
- if ((current_time - last_update > 30)); then
- $STD apt update
- echo "$current_time" >"$apt_cache_file"
- fi
+ $STD apt update
}
# ------------------------------------------------------------------------------
@@ -1157,7 +1399,7 @@ verify_gpg_fingerprint() {
}
# ==============================================================================
-# EXISTING FUNCTIONS
+# INSTALL FUNCTIONS
# ==============================================================================
# ------------------------------------------------------------------------------
@@ -1259,7 +1501,7 @@ check_for_gh_release() {
return 0
fi
- msg_error "No update available: ${app} is not installed!"
+ msg_ok "No update available: ${app} is already on pinned version (${current})"
return 1
fi
@@ -1293,11 +1535,8 @@ create_self_signed_cert() {
return 0
fi
- $STD apt update || {
- msg_error "Failed to update package list"
- return 1
- }
- $STD apt install -y openssl || {
+ # Use ensure_dependencies for cleaner handling
+ ensure_dependencies openssl || {
msg_error "Failed to install OpenSSL"
return 1
}
@@ -2515,9 +2754,14 @@ function setup_java() {
DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)
local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk"
+ # Prepare repository (cleanup + validation)
+ prepare_repository_setup "adoptium" || {
+ msg_error "Failed to prepare Adoptium repository"
+ return 1
+ }
+
# Add repo if needed
if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then
- cleanup_old_repo_files "adoptium"
local SUITE
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb")
setup_deb822_repo \
@@ -2525,8 +2769,7 @@ function setup_java() {
"https://packages.adoptium.net/artifactory/api/gpg/key/public" \
"https://packages.adoptium.net/artifactory/deb" \
"$SUITE" \
- "main" \
- "amd64 arm64"
+ "main"
fi
# Get currently installed version
@@ -2536,7 +2779,9 @@ function setup_java() {
fi
# Validate INSTALLED_VERSION is not empty if matched
- if [[ -z "$INSTALLED_VERSION" && $(dpkg -l 2>/dev/null | grep -c "temurin-.*-jdk" || echo "0") -gt 0 ]]; then
+ local JDK_COUNT=0
+ JDK_COUNT=$(dpkg -l 2>/dev/null | grep -c "temurin-.*-jdk" || echo "0")
+ if [[ -z "$INSTALLED_VERSION" && "${JDK_COUNT:-0}" -gt 0 ]]; then
msg_warn "Found Temurin JDK but cannot determine version"
INSTALLED_VERSION="0"
fi
@@ -2544,11 +2789,8 @@ function setup_java() {
# Scenario 1: Already at correct version
if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then
msg_info "Update Temurin JDK $JAVA_VERSION"
- $STD apt update || {
- msg_error "APT update failed"
- return 1
- }
- $STD apt install --only-upgrade -y "$DESIRED_PACKAGE" || {
+ ensure_apt_working || return 1
+ upgrade_packages_with_retry "$DESIRED_PACKAGE" || {
msg_error "Failed to update Temurin JDK"
return 1
}
@@ -2565,11 +2807,10 @@ function setup_java() {
msg_info "Setup Temurin JDK $JAVA_VERSION"
fi
- $STD apt update || {
- msg_error "APT update failed"
- return 1
- }
- $STD apt install -y "$DESIRED_PACKAGE" || {
+ ensure_apt_working || return 1
+
+ # Install with retry logic
+ install_packages_with_retry "$DESIRED_PACKAGE" || {
msg_error "Failed to install Temurin JDK $JAVA_VERSION"
return 1
}
@@ -2605,11 +2846,7 @@ function setup_local_ip_helper() {
# Install networkd-dispatcher if not present
if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then
- $STD apt update || {
- msg_error "Failed to update package list"
- return 1
- }
- $STD apt install -y networkd-dispatcher || {
+ ensure_dependencies networkd-dispatcher || {
msg_error "Failed to install networkd-dispatcher"
return 1
}
@@ -2737,12 +2974,9 @@ setup_mariadb() {
fi
fi
- # Perform upgrade
- $STD apt update || {
- msg_error "Failed to update package list"
- return 1
- }
- $STD apt install --only-upgrade -y mariadb-server mariadb-client || {
+ # Perform upgrade with retry logic
+ ensure_apt_working || return 1
+ upgrade_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to upgrade MariaDB packages"
return 1
}
@@ -2760,8 +2994,11 @@ setup_mariadb() {
# Scenario 3: Fresh install or version change
msg_info "Setup MariaDB $MARIADB_VERSION"
- # Ensure APT is working before proceeding
- ensure_apt_working || return 1
+ # Prepare repository (cleanup + validation)
+ prepare_repository_setup "mariadb" || {
+ msg_error "Failed to prepare MariaDB repository"
+ return 1
+ }
# Install required dependencies first
local mariadb_deps=()
@@ -2789,24 +3026,105 @@ setup_mariadb() {
echo "mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false" | debconf-set-selections
fi
- # Install packages
- DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || {
+ # Install packages with retry logic
+ export DEBIAN_FRONTEND=noninteractive
+ if ! install_packages_with_retry "mariadb-server" "mariadb-client"; then
# Fallback: try without specific version
msg_warn "Failed to install MariaDB packages from upstream repo, trying distro fallback..."
cleanup_old_repo_files "mariadb"
$STD apt update || {
msg_warn "APT update also failed, continuing with cache"
}
- DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || {
+ install_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to install MariaDB packages (both upstream and distro)"
return 1
}
- }
+ fi
cache_installed_version "mariadb" "$MARIADB_VERSION"
msg_ok "Setup MariaDB $MARIADB_VERSION"
}
+# ------------------------------------------------------------------------------
+# Creates MariaDB database with user, charset and optional extra grants/modes
+#
+# Description:
+# - Generates password if empty
+# - Creates database with utf8mb4_unicode_ci
+# - Creates local user with password
+# - Grants full access to this DB
+# - Optional: apply extra GRANT statements (comma-separated)
+# - Optional: apply custom GLOBAL sql_mode
+# - Saves credentials to file
+# - Exports variables for use in calling script
+#
+# Usage:
+# MARIADB_DB_NAME="myapp_db" MARIADB_DB_USER="myapp_user" setup_mariadb_db
+# MARIADB_DB_NAME="domain_monitor" MARIADB_DB_USER="domainmonitor" setup_mariadb_db
+# MARIADB_DB_NAME="myapp" MARIADB_DB_USER="myapp" MARIADB_DB_EXTRA_GRANTS="GRANT SELECT ON \`mysql\`.\`time_zone_name\`" setup_mariadb_db
+# MARIADB_DB_NAME="ghostfolio" MARIADB_DB_USER="ghostfolio" MARIADB_DB_SQL_MODE="" setup_mariadb_db
+#
+# Variables:
+# MARIADB_DB_NAME - Database name (required)
+# MARIADB_DB_USER - Database user (required)
+# MARIADB_DB_PASS - User password (optional, auto-generated if empty)
+# MARIADB_DB_EXTRA_GRANTS - Comma-separated GRANT statements (optional)
+# Example: "GRANT SELECT ON \`mysql\`.\`time_zone_name\`"
+# MARIADB_DB_SQL_MODE - Optional global sql_mode override (e.g. "", "STRICT_TRANS_TABLES")
+# MARIADB_DB_CREDS_FILE - Credentials file path (optional, default: ~/${APPLICATION}.creds)
+#
+# Exports:
+# MARIADB_DB_NAME, MARIADB_DB_USER, MARIADB_DB_PASS
+# ------------------------------------------------------------------------------
+
+function setup_mariadb_db() {
+ if [[ -z "${MARIADB_DB_NAME:-}" || -z "${MARIADB_DB_USER:-}" ]]; then
+ msg_error "MARIADB_DB_NAME and MARIADB_DB_USER must be set before calling setup_mariadb_db"
+ return 1
+ fi
+
+ if [[ -z "${MARIADB_DB_PASS:-}" ]]; then
+ MARIADB_DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
+ fi
+
+ msg_info "Setting up MariaDB Database"
+
+ $STD mariadb -u root -e "CREATE DATABASE \`$MARIADB_DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
+ $STD mariadb -u root -e "CREATE USER '$MARIADB_DB_USER'@'localhost' IDENTIFIED BY '$MARIADB_DB_PASS';"
+ $STD mariadb -u root -e "GRANT ALL ON \`$MARIADB_DB_NAME\`.* TO '$MARIADB_DB_USER'@'localhost';"
+
+ # Optional extra grants
+ if [[ -n "${MARIADB_DB_EXTRA_GRANTS:-}" ]]; then
+ IFS=',' read -ra G_LIST <<<"${MARIADB_DB_EXTRA_GRANTS:-}"
+ for g in "${G_LIST[@]}"; do
+ g=$(echo "$g" | xargs)
+ $STD mariadb -u root -e "$g TO '$MARIADB_DB_USER'@'localhost';"
+ done
+ fi
+
+ # Optional sql_mode override
+ if [[ -n "${MARIADB_DB_SQL_MODE:-}" ]]; then
+ $STD mariadb -u root -e "SET GLOBAL sql_mode='${MARIADB_DB_SQL_MODE:-}';"
+ fi
+
+ $STD mariadb -u root -e "FLUSH PRIVILEGES;"
+
+ local app_name="${APPLICATION,,}"
+ local CREDS_FILE="${MARIADB_DB_CREDS_FILE:-${HOME}/${app_name}.creds}"
+ {
+ echo "MariaDB Credentials"
+ echo "Database: $MARIADB_DB_NAME"
+ echo "User: $MARIADB_DB_USER"
+ echo "Password: $MARIADB_DB_PASS"
+ } >>"$CREDS_FILE"
+
+ msg_ok "Set up MariaDB Database"
+
+ export MARIADB_DB_NAME
+ export MARIADB_DB_USER
+ export MARIADB_DB_PASS
+}
+
# ------------------------------------------------------------------------------
# Installs or updates MongoDB to specified major version.
#
@@ -2856,8 +3174,8 @@ function setup_mongodb() {
ensure_apt_working || return 1
- # Perform upgrade
- $STD apt install --only-upgrade -y mongodb-org || {
+ # Perform upgrade with retry logic
+ upgrade_packages_with_retry "mongodb-org" || {
msg_error "Failed to upgrade MongoDB"
return 1
}
@@ -2876,6 +3194,12 @@ function setup_mongodb() {
cleanup_orphaned_sources
+ # Prepare repository (cleanup + validation)
+ prepare_repository_setup "mongodb" || {
+ msg_error "Failed to prepare MongoDB repository"
+ return 1
+ }
+
# Setup repository
manage_tool_repository "mongodb" "$MONGO_VERSION" "$MONGO_BASE_URL" \
"https://www.mongodb.org/static/pgp/server-${MONGO_VERSION}.asc" || {
@@ -2889,12 +3213,18 @@ function setup_mongodb() {
return 1
}
- # Install MongoDB
- $STD apt install -y mongodb-org || {
+ # Install MongoDB with retry logic
+ install_packages_with_retry "mongodb-org" || {
msg_error "Failed to install MongoDB packages"
return 1
}
+ # Verify MongoDB was installed correctly
+ if ! command -v mongod >/dev/null 2>&1; then
+ msg_error "MongoDB binary not found after installation"
+ return 1
+ fi
+
mkdir -p /var/lib/mongodb
chown -R mongodb:mongodb /var/lib/mongodb
@@ -2902,8 +3232,13 @@ function setup_mongodb() {
msg_warn "Failed to enable mongod service"
}
safe_service_restart mongod
- cache_installed_version "mongodb" "$MONGO_VERSION"
+ # Verify MongoDB version
+ local INSTALLED_VERSION
+ INSTALLED_VERSION=$(mongod --version 2>/dev/null | grep -oP 'db version v\K[0-9]+\.[0-9]+' | head -n1 || echo "0.0")
+ verify_tool_version "MongoDB" "$MONGO_VERSION" "$INSTALLED_VERSION" || true
+
+ cache_installed_version "mongodb" "$MONGO_VERSION"
msg_ok "Setup MongoDB $MONGO_VERSION"
}
@@ -2936,7 +3271,8 @@ function setup_mysql() {
ensure_apt_working || return 1
- $STD apt install --only-upgrade -y mysql-server mysql-client || true
+ # Perform upgrade with retry logic (non-fatal if fails)
+ upgrade_packages_with_retry "mysql-server" "mysql-client" || true
cache_installed_version "mysql" "$MYSQL_VERSION"
msg_ok "Update MySQL $MYSQL_VERSION"
@@ -2951,23 +3287,27 @@ function setup_mysql() {
msg_info "Setup MySQL $MYSQL_VERSION"
fi
+ # Prepare repository (cleanup + validation)
+ prepare_repository_setup "mysql" || {
+ msg_error "Failed to prepare MySQL repository"
+ return 1
+ }
+
# Debian 13+ Fix: MySQL 8.0 incompatible with libaio1t64, use 8.4 LTS
if [[ "$DISTRO_ID" == "debian" && "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then
msg_info "Debian ${DISTRO_CODENAME} detected → using MySQL 8.4 LTS (libaio1t64 compatible)"
- cleanup_old_repo_files "mysql"
-
if ! curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | gpg --dearmor -o /etc/apt/keyrings/mysql.gpg 2>/dev/null; then
msg_error "Failed to import MySQL GPG key"
return 1
fi
- cat >/etc/apt/sources.list.d/mysql.sources <<'EOF'
+ cat >/etc/apt/sources.list.d/mysql.sources </dev/null | grep -q . &&
- $STD apt install -y mysql-server mysql-client 2>/dev/null; then
+ install_packages_with_retry "mysql-server" "mysql-client"; then
mysql_install_success=true
elif apt-cache search "^mysql-community-server$" 2>/dev/null | grep -q . &&
- $STD apt install -y mysql-community-server mysql-community-client 2>/dev/null; then
+ install_packages_with_retry "mysql-community-server" "mysql-community-client"; then
mysql_install_success=true
elif apt-cache search "^mysql$" 2>/dev/null | grep -q . &&
- $STD apt install -y mysql 2>/dev/null; then
+ install_packages_with_retry "mysql"; then
mysql_install_success=true
fi
@@ -3062,6 +3403,9 @@ function setup_nodejs() {
local NODE_VERSION="${NODE_VERSION:-22}"
local NODE_MODULE="${NODE_MODULE:-}"
+ # ALWAYS clean up legacy installations first (nvm, etc.) to prevent conflicts
+ cleanup_legacy_install "nodejs"
+
# Get currently installed version
local CURRENT_NODE_VERSION=""
CURRENT_NODE_VERSION=$(is_tool_installed "nodejs" 2>/dev/null) || true
@@ -3095,37 +3439,66 @@ function setup_nodejs() {
msg_info "Setup Node.js $NODE_VERSION"
fi
- ensure_dependencies curl ca-certificates gnupg
+ # Remove ALL Debian nodejs packages BEFORE adding NodeSource repo
+ if dpkg -l 2>/dev/null | grep -qE "^ii.*(nodejs|libnode|node-cjs|node-acorn|node-balanced|node-brace|node-minimatch|node-undici|node-xtend|node-corepack)"; then
+ msg_info "Removing Debian-packaged Node.js and dependencies"
+ $STD apt purge -y nodejs nodejs-doc libnode* node-* 2>/dev/null || true
+ $STD apt autoremove -y 2>/dev/null || true
+ $STD apt clean 2>/dev/null || true
+ fi
- # Setup repository
+ # Remove any APT pinning (not needed)
+ rm -f /etc/apt/preferences.d/nodesource 2>/dev/null || true
+
+ # Prepare repository (cleanup + validation)
+ prepare_repository_setup "nodesource" || {
+ msg_error "Failed to prepare Node.js repository"
+ return 1
+ }
+
+ # Setup NodeSource repository
manage_tool_repository "nodejs" "$NODE_VERSION" "https://deb.nodesource.com/node_${NODE_VERSION}.x" "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" || {
msg_error "Failed to setup Node.js repository"
return 1
}
- # Wait for repo to settle
- sleep 2
+ # CRITICAL: Force APT cache refresh AFTER repository setup
+ # This ensures NodeSource is the only nodejs source in APT cache
+ $STD apt update
- # Install Node.js
- if ! apt update >/dev/null 2>&1; then
- msg_warn "APT update failed – retrying in 5s"
- sleep 5
- if ! apt update >/dev/null 2>&1; then
- msg_error "Failed to update APT repositories after adding NodeSource"
- return 1
- fi
- fi
+ # Install dependencies (NodeSource is now the only nodejs source)
+ ensure_dependencies curl ca-certificates gnupg
- if ! apt install -y nodejs >/dev/null 2>&1; then
+ # Install Node.js from NodeSource
+ install_packages_with_retry "nodejs" || {
msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource"
return 1
+ }
+
+ # Verify Node.js was installed correctly
+ if ! command -v node >/dev/null 2>&1; then
+ msg_error "Node.js binary not found after installation"
+ return 1
fi
- # Update to latest npm
- $STD npm install -g npm@latest || {
- msg_error "Failed to update npm to latest version"
+ local INSTALLED_NODE_VERSION
+ INSTALLED_NODE_VERSION=$(node -v 2>/dev/null | grep -oP '^v\K[0-9]+' || echo "0")
+ verify_tool_version "Node.js" "$NODE_VERSION" "$INSTALLED_NODE_VERSION" || true
+
+ # Verify npm is available (should come with NodeSource nodejs)
+ if ! command -v npm >/dev/null 2>&1; then
+ msg_error "npm not found after Node.js installation - repository issue?"
return 1
- }
+ fi
+
+ # Update to latest npm (with version check to avoid incompatibility)
+ local NPM_VERSION
+ NPM_VERSION=$(npm -v 2>/dev/null || echo "0")
+ if [[ "$NPM_VERSION" != "0" ]]; then
+ $STD npm install -g npm@latest 2>/dev/null || {
+ msg_warn "Failed to update npm to latest version (continuing with bundled npm $NPM_VERSION)"
+ }
+ fi
cache_installed_version "nodejs" "$NODE_VERSION"
msg_ok "Setup Node.js $NODE_VERSION"
@@ -3163,8 +3536,8 @@ function setup_nodejs() {
fi
# Check if the module is already installed
- if npm list -g --depth=0 "$MODULE_NAME" >/dev/null 2>&1; then
- MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')"
+ if $STD npm list -g --depth=0 "$MODULE_NAME" 2>&1 | grep -q "$MODULE_NAME@"; then
+ MODULE_INSTALLED_VERSION="$($STD npm list -g --depth=0 "$MODULE_NAME" 2>&1 | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')"
if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then
msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION"
if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" 2>/dev/null; then
@@ -3261,8 +3634,8 @@ function setup_php() {
ensure_apt_working || return 1
- # Just update PHP packages
- $STD apt install --only-upgrade -y "php${PHP_VERSION}" || true
+ # Perform upgrade with retry logic (non-fatal if fails)
+ upgrade_packages_with_retry "php${PHP_VERSION}" || true
cache_installed_version "php" "$PHP_VERSION"
msg_ok "Update PHP $PHP_VERSION"
@@ -3270,14 +3643,19 @@ function setup_php() {
# Scenario 2: Different version installed - clean upgrade
if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then
msg_info "Upgrade PHP from $CURRENT_PHP to $PHP_VERSION"
- # Stop old PHP-FPM if running
- $STD systemctl stop "php${CURRENT_PHP}-fpm" >/dev/null 2>&1 || true
- $STD systemctl disable "php${CURRENT_PHP}-fpm" >/dev/null 2>&1 || true
+ # Stop and disable ALL PHP-FPM versions
+ stop_all_services "php.*-fpm"
remove_old_tool_version "php"
else
msg_info "Setup PHP $PHP_VERSION"
fi
+ # Prepare repository (cleanup + validation)
+ prepare_repository_setup "php" "deb.sury.org-php" || {
+ msg_error "Failed to prepare PHP repository"
+ return 1
+ }
+
# Setup Sury repository
manage_tool_repository "php" "$PHP_VERSION" "" "https://packages.sury.org/debsuryorg-archive-keyring.deb" || {
msg_error "Failed to setup PHP repository"
@@ -3302,15 +3680,15 @@ function setup_php() {
# install apache2 with PHP support if requested
if [[ "$PHP_APACHE" == "YES" ]]; then
if ! dpkg -l 2>/dev/null | grep -q "libapache2-mod-php${PHP_VERSION}"; then
- $STD apt install -y apache2 libapache2-mod-php${PHP_VERSION} || {
+ install_packages_with_retry "apache2" "libapache2-mod-php${PHP_VERSION}" || {
msg_error "Failed to install Apache with PHP module"
return 1
}
fi
fi
- # Install PHP packages
- $STD apt install -y $MODULE_LIST || {
+ # Install PHP packages with retry logic
+ install_packages_with_retry $MODULE_LIST || {
msg_error "Failed to install PHP packages"
return 1
}
@@ -3380,8 +3758,10 @@ function setup_postgresql() {
# Scenario 1: Already at correct version
if [[ "$CURRENT_PG_VERSION" == "$PG_VERSION" ]]; then
msg_info "Update PostgreSQL $PG_VERSION"
- $STD apt update
- $STD apt install --only-upgrade -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null || true
+ ensure_apt_working || return 1
+
+ # Perform upgrade with retry logic (non-fatal if fails)
+ upgrade_packages_with_retry "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null || true
cache_installed_version "postgresql" "$PG_VERSION"
msg_ok "Update PostgreSQL $PG_VERSION"
@@ -3410,7 +3790,10 @@ function setup_postgresql() {
fi
# Scenario 3: Fresh install or after removal - setup repo and install
- cleanup_old_repo_files "pgdg"
+ prepare_repository_setup "pgdg" "postgresql" || {
+ msg_error "Failed to prepare PostgreSQL repository"
+ return 1
+ }
local SUITE
case "$DISTRO_CODENAME" in
@@ -3432,8 +3815,7 @@ function setup_postgresql() {
"https://www.postgresql.org/media/keys/ACCC4CF8.asc" \
"https://apt.postgresql.org/pub/repos/apt" \
"$SUITE" \
- "main" \
- "amd64 arm64"
+ "main"
if ! $STD apt update; then
msg_error "APT update failed for PostgreSQL repository"
@@ -3445,11 +3827,11 @@ function setup_postgresql() {
$STD apt install -y ssl-cert 2>/dev/null || true
fi
- # Try multiple PostgreSQL package patterns
+ # Try multiple PostgreSQL package patterns with retry logic
local pg_install_success=false
if apt-cache search "^postgresql-${PG_VERSION}$" 2>/dev/null | grep -q . &&
- $STD apt install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" 2>/dev/null; then
+ install_packages_with_retry "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}"; then
pg_install_success=true
fi
@@ -3502,6 +3884,104 @@ function setup_postgresql() {
fi
}
+# ------------------------------------------------------------------------------
+# Creates PostgreSQL database with user and optional extensions
+#
+# Description:
+# - Creates PostgreSQL role with login and password
+# - Creates database with UTF8 encoding and template0
+# - Installs optional extensions (postgis, pgvector, etc.)
+# - Configures ALTER ROLE settings for Django/Rails compatibility
+# - Saves credentials to file
+# - Exports variables for use in calling script
+#
+# Usage:
+# PG_DB_NAME="myapp_db" PG_DB_USER="myapp_user" setup_postgresql_db
+# PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_EXTENSIONS="pgvector" setup_postgresql_db
+# PG_DB_NAME="ghostfolio" PG_DB_USER="ghostfolio" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db
+# PG_DB_NAME="adventurelog" PG_DB_USER="adventurelog" PG_DB_EXTENSIONS="postgis" setup_postgresql_db
+#
+# Variables:
+# PG_DB_NAME - Database name (required)
+# PG_DB_USER - Database user (required)
+# PG_DB_PASS - Database password (optional, auto-generated if empty)
+# PG_DB_EXTENSIONS - Comma-separated list of extensions (optional, e.g. "postgis,pgvector")
+# PG_DB_GRANT_SUPERUSER - Grant SUPERUSER privilege (optional, "true" to enable, security risk!)
+# PG_DB_SCHEMA_PERMS - Grant schema-level permissions (optional, "true" to enable)
+# PG_DB_SKIP_ALTER_ROLE - Skip ALTER ROLE settings (optional, "true" to skip)
+# PG_DB_CREDS_FILE - Credentials file path (optional, default: ~/${APPLICATION}.creds)
+#
+# Exports:
+# PG_DB_NAME, PG_DB_USER, PG_DB_PASS - For use in calling script
+# ------------------------------------------------------------------------------
+
+function setup_postgresql_db() {
+ # Validation
+ if [[ -z "${PG_DB_NAME:-}" || -z "${PG_DB_USER:-}" ]]; then
+ msg_error "PG_DB_NAME and PG_DB_USER must be set before calling setup_postgresql_db"
+ return 1
+ fi
+
+ # Generate password if not provided
+ if [[ -z "${PG_DB_PASS:-}" ]]; then
+ PG_DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)
+ fi
+
+ msg_info "Setting up PostgreSQL Database"
+ $STD sudo -u postgres psql -c "CREATE ROLE $PG_DB_USER WITH LOGIN PASSWORD '$PG_DB_PASS';"
+ $STD sudo -u postgres psql -c "CREATE DATABASE $PG_DB_NAME WITH OWNER $PG_DB_USER ENCODING 'UTF8' TEMPLATE template0;"
+
+ # Install extensions (comma-separated)
+ if [[ -n "${PG_DB_EXTENSIONS:-}" ]]; then
+ IFS=',' read -ra EXT_LIST <<<"${PG_DB_EXTENSIONS:-}"
+ for ext in "${EXT_LIST[@]}"; do
+ ext=$(echo "$ext" | xargs) # Trim whitespace
+ $STD sudo -u postgres psql -d "$PG_DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS $ext;"
+ done
+ fi
+
+ # ALTER ROLE settings for Django/Rails compatibility (unless skipped)
+ if [[ "${PG_DB_SKIP_ALTER_ROLE:-}" != "true" ]]; then
+ $STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET client_encoding TO 'utf8';"
+ $STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET default_transaction_isolation TO 'read committed';"
+ $STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET timezone TO 'UTC';"
+ fi
+
+ # Schema permissions (if requested)
+ if [[ "${PG_DB_SCHEMA_PERMS:-}" == "true" ]]; then
+ $STD sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $PG_DB_NAME TO $PG_DB_USER;"
+ $STD sudo -u postgres psql -c "ALTER USER $PG_DB_USER CREATEDB;"
+ $STD sudo -u postgres psql -d "$PG_DB_NAME" -c "GRANT ALL ON SCHEMA public TO $PG_DB_USER;"
+ $STD sudo -u postgres psql -d "$PG_DB_NAME" -c "GRANT CREATE ON SCHEMA public TO $PG_DB_USER;"
+ $STD sudo -u postgres psql -d "$PG_DB_NAME" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $PG_DB_USER;"
+ $STD sudo -u postgres psql -d "$PG_DB_NAME" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $PG_DB_USER;"
+ fi
+
+ # Superuser grant (if requested - WARNING!)
+ if [[ "${PG_DB_GRANT_SUPERUSER:-}" == "true" ]]; then
+ msg_warn "Granting SUPERUSER privilege (security risk!)"
+ $STD sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $PG_DB_NAME to $PG_DB_USER;"
+ $STD sudo -u postgres psql -c "ALTER USER $PG_DB_USER WITH SUPERUSER;"
+ fi
+
+ # Save credentials
+ local app_name="${APPLICATION,,}"
+ local CREDS_FILE="${PG_DB_CREDS_FILE:-${HOME}/${app_name}.creds}"
+ {
+ echo "PostgreSQL Credentials"
+ echo "Database: $PG_DB_NAME"
+ echo "User: $PG_DB_USER"
+ echo "Password: $PG_DB_PASS"
+ } >>"$CREDS_FILE"
+
+ msg_ok "Setup PostgreSQL Database"
+
+ # Export for use in calling script
+ export PG_DB_NAME
+ export PG_DB_USER
+ export PG_DB_PASS
+}
+
# ------------------------------------------------------------------------------
# Installs rbenv and ruby-build, installs Ruby and optionally Rails.
#
@@ -3751,7 +4231,9 @@ function setup_clickhouse() {
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then
msg_info "Update ClickHouse $CLICKHOUSE_VERSION"
ensure_apt_working || return 1
- $STD apt install --only-upgrade -y clickhouse-server clickhouse-client || true
+
+ # Perform upgrade with retry logic (non-fatal if fails)
+ upgrade_packages_with_retry "clickhouse-server" "clickhouse-client" || true
cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION"
msg_ok "Update ClickHouse $CLICKHOUSE_VERSION"
return 0
@@ -3760,7 +4242,7 @@ function setup_clickhouse() {
# Scenario 2: Different version - clean upgrade
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$CLICKHOUSE_VERSION" ]]; then
msg_info "Upgrade ClickHouse from $CURRENT_VERSION to $CLICKHOUSE_VERSION"
- $STD systemctl stop clickhouse-server >/dev/null 2>&1 || true
+ stop_all_services "clickhouse-server"
remove_old_tool_version "clickhouse"
else
msg_info "Setup ClickHouse $CLICKHOUSE_VERSION"
@@ -3768,23 +4250,28 @@ function setup_clickhouse() {
ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg
+ # Prepare repository (cleanup + validation)
+ prepare_repository_setup "clickhouse" || {
+ msg_error "Failed to prepare ClickHouse repository"
+ return 1
+ }
+
# Setup repository (ClickHouse uses 'stable' suite)
setup_deb822_repo \
"clickhouse" \
"https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \
"https://packages.clickhouse.com/deb" \
"stable" \
- "main" \
- "amd64 arm64"
+ "main"
- # Install packages
+ # Install packages with retry logic
export DEBIAN_FRONTEND=noninteractive
$STD apt update || {
msg_error "APT update failed for ClickHouse repository"
return 1
}
- $STD apt install -y clickhouse-server clickhouse-client || {
+ install_packages_with_retry "clickhouse-server" "clickhouse-client" || {
msg_error "Failed to install ClickHouse packages"
return 1
}
@@ -3906,65 +4393,79 @@ function setup_rust() {
function setup_uv() {
local UV_BIN="/usr/local/bin/uv"
+ local UVX_BIN="/usr/local/bin/uvx"
local TMP_DIR=$(mktemp -d)
local CACHED_VERSION
+
+ # trap for TMP Cleanup
+ trap "rm -rf '$TMP_DIR'" EXIT
+
CACHED_VERSION=$(get_cached_version "uv")
+ # Architecture Detection
local ARCH=$(uname -m)
- local UV_TAR
+ local OS_TYPE=""
+ local UV_TAR=""
+
+ if grep -qi "alpine" /etc/os-release; then
+ OS_TYPE="musl"
+ else
+ OS_TYPE="gnu"
+ fi
case "$ARCH" in
x86_64)
- if grep -qi "alpine" /etc/os-release; then
- UV_TAR="uv-x86_64-unknown-linux-musl.tar.gz"
- else
- UV_TAR="uv-x86_64-unknown-linux-gnu.tar.gz"
- fi
+ UV_TAR="uv-x86_64-unknown-linux-${OS_TYPE}.tar.gz"
;;
aarch64)
- if grep -qi "alpine" /etc/os-release; then
- UV_TAR="uv-aarch64-unknown-linux-musl.tar.gz"
- else
- UV_TAR="uv-aarch64-unknown-linux-gnu.tar.gz"
- fi
+ UV_TAR="uv-aarch64-unknown-linux-${OS_TYPE}.tar.gz"
+ ;;
+ i686)
+ UV_TAR="uv-i686-unknown-linux-${OS_TYPE}.tar.gz"
;;
*)
- msg_error "Unsupported architecture: $ARCH"
- rm -rf "$TMP_DIR"
+ msg_error "Unsupported architecture: $ARCH (supported: x86_64, aarch64, i686)"
return 1
;;
esac
ensure_dependencies jq
- local LATEST_VERSION
+ # Fetch latest version
local releases_json
- releases_json=$(curl -fsSL --max-time 15 https://api.github.com/repos/astral-sh/uv/releases/latest 2>/dev/null || echo "")
+ releases_json=$(curl -fsSL --max-time 15 \
+ "https://api.github.com/repos/astral-sh/uv/releases/latest" 2>/dev/null || echo "")
if [[ -z "$releases_json" ]]; then
msg_error "Could not fetch latest uv version from GitHub API"
- rm -rf "$TMP_DIR"
return 1
fi
- LATEST_VERSION=$(echo "$releases_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//' || echo "")
+ local LATEST_VERSION
+ LATEST_VERSION=$(echo "$releases_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//')
if [[ -z "$LATEST_VERSION" ]]; then
msg_error "Could not parse uv version from GitHub API response"
- rm -rf "$TMP_DIR"
return 1
fi
# Get currently installed version
local INSTALLED_VERSION=""
if [[ -x "$UV_BIN" ]]; then
- INSTALLED_VERSION=$($UV_BIN -V 2>/dev/null | awk '{print $2}')
+ INSTALLED_VERSION=$("$UV_BIN" --version 2>/dev/null | awk '{print $2}')
fi
# Scenario 1: Already at latest version
if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then
cache_installed_version "uv" "$LATEST_VERSION"
- rm -rf "$TMP_DIR"
+
+ # Check if uvx is needed and missing
+ if [[ "${USE_UVX:-NO}" == "YES" ]] && [[ ! -x "$UVX_BIN" ]]; then
+ msg_info "Installing uvx wrapper"
+ _install_uvx_wrapper || return 1
+ msg_ok "uvx wrapper installed"
+ fi
+
return 0
fi
@@ -3975,52 +4476,75 @@ function setup_uv() {
msg_info "Setup uv $LATEST_VERSION"
fi
- local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}"
- curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || {
- msg_error "Failed to download uv"
- rm -rf "$TMP_DIR"
+ local UV_URL="https://github.com/astral-sh/uv/releases/download/${LATEST_VERSION}/${UV_TAR}"
+
+ $STD curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || {
+ msg_error "Failed to download uv from $UV_URL"
return 1
}
- tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || {
+ # Extract
+ $STD tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || {
msg_error "Failed to extract uv"
- rm -rf "$TMP_DIR"
return 1
}
- install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || {
+ # Find and install uv binary (tarball extracts to uv-VERSION-ARCH/ directory)
+ local UV_BINARY=$(find "$TMP_DIR" -name "uv" -type f -executable | head -n1)
+ if [[ ! -f "$UV_BINARY" ]]; then
+ msg_error "Could not find uv binary in extracted tarball"
+ return 1
+ fi
+
+ $STD install -m 755 "$UV_BINARY" "$UV_BIN" || {
msg_error "Failed to install uv binary"
- rm -rf "$TMP_DIR"
return 1
}
- rm -rf "$TMP_DIR"
ensure_usr_local_bin_persist
export PATH="/usr/local/bin:$PATH"
- $STD uv python update-shell || true
+ # Optional: Install uvx wrapper
+ if [[ "${USE_UVX:-NO}" == "YES" ]]; then
+ msg_info "Installing uvx wrapper"
+ _install_uvx_wrapper || {
+ msg_error "Failed to install uvx wrapper"
+ return 1
+ }
+ msg_ok "uvx wrapper installed"
+ fi
+
+ # Optional: Generate shell completions
+ $STD uv generate-shell-completion bash >/etc/bash_completion.d/uv 2>/dev/null || true
+ $STD uv generate-shell-completion zsh >/usr/share/zsh/site-functions/_uv 2>/dev/null || true
+
+ # Optional: Install specific Python version if requested
+ if [[ -n "${PYTHON_VERSION:-}" ]]; then
+ msg_info "Installing Python $PYTHON_VERSION via uv"
+ $STD uv python install "$PYTHON_VERSION" || {
+ msg_error "Failed to install Python $PYTHON_VERSION"
+ return 1
+ }
+ msg_ok "Python $PYTHON_VERSION installed"
+ fi
+
cache_installed_version "uv" "$LATEST_VERSION"
msg_ok "Setup uv $LATEST_VERSION"
+}
- # Optional: Install specific Python version
- if [[ -n "${PYTHON_VERSION:-}" ]]; then
- local VERSION_MATCH
- VERSION_MATCH=$(uv python list --only-downloads 2>/dev/null |
- grep -E "^cpython-${PYTHON_VERSION//./\\.}\.[0-9]+-linux" |
- cut -d'-' -f2 | sort -V | tail -n1)
+# Helper function to install uvx wrapper
+_install_uvx_wrapper() {
+ local UVX_BIN="/usr/local/bin/uvx"
- if [[ -z "$VERSION_MATCH" ]]; then
- msg_error "No matching Python $PYTHON_VERSION.x version found"
- return 1
- fi
+ cat >"$UVX_BIN" <<'EOF'
+#!/bin/bash
+# uvx - Run Python applications from PyPI as command-line tools
+# Wrapper for: uv tool run
+exec /usr/local/bin/uv tool run "$@"
+EOF
- if ! uv python list 2>/dev/null | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then
- $STD uv python install "$VERSION_MATCH" || {
- msg_error "Failed to install Python $VERSION_MATCH"
- return 1
- }
- fi
- fi
+ chmod +x "$UVX_BIN"
+ return 0
}
# ------------------------------------------------------------------------------
@@ -4106,3 +4630,214 @@ function setup_yq() {
cache_installed_version "yq" "$FINAL_VERSION"
msg_ok "Setup yq $FINAL_VERSION"
}
+
+# ------------------------------------------------------------------------------
+# Docker Engine Installation and Management (All-In-One)
+#
+# Description:
+# - Detects and migrates old Docker installations
+# - Installs/Updates Docker Engine via official repository
+# - Optional: Installs/Updates Portainer CE
+# - Updates running containers interactively
+# - Cleans up legacy repository files
+#
+# Usage:
+# setup_docker
+# DOCKER_PORTAINER="true" setup_docker
+# DOCKER_LOG_DRIVER="json-file" setup_docker
+#
+# Variables:
+# DOCKER_PORTAINER - Install Portainer CE (optional, "true" to enable)
+# DOCKER_LOG_DRIVER - Log driver (optional, default: "journald")
+# DOCKER_SKIP_UPDATES - Skip container update check (optional, "true" to skip)
+#
+# Features:
+# - Migrates from get.docker.com to repository-based installation
+# - Updates Docker Engine if newer version available
+# - Interactive container update with multi-select
+# - Portainer installation and update support
+# ------------------------------------------------------------------------------
+function setup_docker() {
+ local docker_installed=false
+ local portainer_installed=false
+
+ # Check if Docker is already installed
+ if command -v docker &>/dev/null; then
+ docker_installed=true
+ DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
+ msg_info "Docker $DOCKER_CURRENT_VERSION detected"
+ fi
+
+ # Check if Portainer is running
+ if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^portainer$'; then
+ portainer_installed=true
+ msg_info "Portainer container detected"
+ fi
+
+ # Cleanup old repository configurations
+ if [ -f /etc/apt/sources.list.d/docker.list ]; then
+ msg_info "Migrating from old Docker repository format"
+ rm -f /etc/apt/sources.list.d/docker.list
+ rm -f /etc/apt/keyrings/docker.asc
+ fi
+
+ # Setup/Update Docker repository
+ msg_info "Setting up Docker Repository"
+ setup_deb822_repo \
+ "docker" \
+ "https://download.docker.com/linux/$(get_os_info id)/gpg" \
+ "https://download.docker.com/linux/$(get_os_info id)" \
+ "$(get_os_info codename)" \
+ "stable" \
+ "$(dpkg --print-architecture)"
+
+ # Install or upgrade Docker
+ if [ "$docker_installed" = true ]; then
+ msg_info "Checking for Docker updates"
+ DOCKER_LATEST_VERSION=$(apt-cache policy docker-ce | grep Candidate | awk '{print $2}' | cut -d':' -f2 | cut -d'-' -f1)
+
+ if [ "$DOCKER_CURRENT_VERSION" != "$DOCKER_LATEST_VERSION" ]; then
+ msg_info "Updating Docker $DOCKER_CURRENT_VERSION → $DOCKER_LATEST_VERSION"
+ $STD apt-get install -y --only-upgrade \
+ docker-ce \
+ docker-ce-cli \
+ containerd.io \
+ docker-buildx-plugin \
+ docker-compose-plugin
+ msg_ok "Updated Docker to $DOCKER_LATEST_VERSION"
+ else
+ msg_ok "Docker is up-to-date ($DOCKER_CURRENT_VERSION)"
+ fi
+ else
+ msg_info "Installing Docker"
+ $STD apt-get install -y \
+ docker-ce \
+ docker-ce-cli \
+ containerd.io \
+ docker-buildx-plugin \
+ docker-compose-plugin
+
+ DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
+ msg_ok "Installed Docker $DOCKER_CURRENT_VERSION"
+ fi
+
+ # Configure daemon.json
+ local log_driver="${DOCKER_LOG_DRIVER:-journald}"
+ mkdir -p /etc/docker
+ if [ ! -f /etc/docker/daemon.json ]; then
+ cat </etc/docker/daemon.json
+{
+ "log-driver": "$log_driver"
+}
+EOF
+ fi
+
+ # Enable and start Docker
+ systemctl enable -q --now docker
+
+ # Portainer Management
+ if [[ "${DOCKER_PORTAINER:-}" == "true" ]]; then
+ if [ "$portainer_installed" = true ]; then
+ msg_info "Checking for Portainer updates"
+ PORTAINER_CURRENT=$(docker inspect portainer --format='{{.Config.Image}}' 2>/dev/null | cut -d':' -f2)
+ PORTAINER_LATEST=$(curl -fsSL https://registry.hub.docker.com/v2/repositories/portainer/portainer-ce/tags?page_size=100 | grep -oP '"name":"\K[0-9]+\.[0-9]+\.[0-9]+"' | head -1 | tr -d '"')
+
+ if [ "$PORTAINER_CURRENT" != "$PORTAINER_LATEST" ]; then
+ read -r -p "${TAB3}Update Portainer $PORTAINER_CURRENT → $PORTAINER_LATEST? " prompt
+ if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
+ msg_info "Updating Portainer"
+ docker stop portainer
+ docker rm portainer
+ docker pull portainer/portainer-ce:latest
+ docker run -d \
+ -p 9000:9000 \
+ -p 9443:9443 \
+ --name=portainer \
+ --restart=always \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v portainer_data:/data \
+ portainer/portainer-ce:latest
+ msg_ok "Updated Portainer to $PORTAINER_LATEST"
+ fi
+ else
+ msg_ok "Portainer is up-to-date ($PORTAINER_CURRENT)"
+ fi
+ else
+ msg_info "Installing Portainer"
+ docker volume create portainer_data
+ docker run -d \
+ -p 9000:9000 \
+ -p 9443:9443 \
+ --name=portainer \
+ --restart=always \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v portainer_data:/data \
+ portainer/portainer-ce:latest
+
+ LOCAL_IP=$(hostname -I | awk '{print $1}')
+ msg_ok "Installed Portainer (http://${LOCAL_IP}:9000)"
+ fi
+ fi
+
+ # Interactive Container Update Check
+ if [[ "${DOCKER_SKIP_UPDATES:-}" != "true" ]] && [ "$docker_installed" = true ]; then
+ msg_info "Checking for container updates"
+
+ # Get list of running containers with update status
+ local containers_with_updates=()
+ local container_info=()
+ local index=1
+
+ while IFS= read -r container; do
+ local name=$(echo "$container" | awk '{print $1}')
+ local image=$(echo "$container" | awk '{print $2}')
+ local current_digest=$(docker inspect "$name" --format='{{.Image}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
+
+ # Pull latest image digest
+ docker pull "$image" >/dev/null 2>&1
+ local latest_digest=$(docker inspect "$image" --format='{{.Id}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
+
+ if [ "$current_digest" != "$latest_digest" ]; then
+ containers_with_updates+=("$name")
+ container_info+=("${index}) ${name} (${image})")
+ ((index++))
+ fi
+ done < <(docker ps --format '{{.Names}} {{.Image}}')
+
+ if [ ${#containers_with_updates[@]} -gt 0 ]; then
+ echo ""
+ echo "${TAB3}Container updates available:"
+ for info in "${container_info[@]}"; do
+ echo "${TAB3} $info"
+ done
+ echo ""
+ read -r -p "${TAB3}Select containers to update (e.g., 1,3,5 or 'all' or 'none'): " selection
+
+ if [[ ${selection,,} == "all" ]]; then
+ for container in "${containers_with_updates[@]}"; do
+ msg_info "Updating container: $container"
+ docker stop "$container"
+ docker rm "$container"
+ # Note: This requires the original docker run command - best to recreate via compose
+ msg_ok "Stopped and removed $container (please recreate with updated image)"
+ done
+ elif [[ ${selection,,} != "none" ]]; then
+ IFS=',' read -ra SELECTED <<<"$selection"
+ for num in "${SELECTED[@]}"; do
+ num=$(echo "$num" | xargs) # trim whitespace
+ if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le "${#containers_with_updates[@]}" ]; then
+ container="${containers_with_updates[$((num - 1))]}"
+ msg_info "Updating container: $container"
+ docker stop "$container"
+ docker rm "$container"
+ msg_ok "Stopped and removed $container (please recreate with updated image)"
+ fi
+ done
+ fi
+ else
+ msg_ok "All containers are up-to-date"
+ fi
+ fi
+
+ msg_ok "Docker setup completed"
+}
diff --git a/misc/vm-core.func b/misc/vm-core.func
index 89cc1d716..099999de7 100644
--- a/misc/vm-core.func
+++ b/misc/vm-core.func
@@ -394,9 +394,9 @@ check_root() {
}
pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
msg_error "This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
echo -e "Exiting..."
sleep 2
exit
diff --git a/tools/addon/netdata.sh b/tools/addon/netdata.sh
index 1f2d598fa..cab0c8081 100644
--- a/tools/addon/netdata.sh
+++ b/tools/addon/netdata.sh
@@ -51,29 +51,29 @@ pve_check() {
PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
# Proxmox VE 8.x: allow 8.0 – 8.9
- if [[ "$PVE_VER" =~ ^9\.([0-9]+)(\.[0-9]+)?$ ]]; then
+ if [[ "$PVE_VER" =~ ^8\.([0-9]+)(\.[0-9]+)?$ ]]; then
local MINOR="${BASH_REMATCH[1]}"
- if ((MINOR != 0)); then
+ if ((MINOR < 0 || MINOR > 9)); then
msg_error "Unsupported Proxmox VE version: $PVE_VER"
- msg_error "Supported versions: 8.0 – 8.9 or 9.0.x"
+ msg_error "Supported versions: 8.0 – 8.9 or 9.0 – 9.1"
exit 1
fi
return 0
fi
- # Proxmox VE 9.x: allow only 9.0
+ # Proxmox VE 9.x: allow 9.0 – 9.1
if [[ "$PVE_VER" =~ ^9\.([0-9]+)$ ]]; then
local MINOR="${BASH_REMATCH[1]}"
- if ((MINOR != 0)); then
+ if ((MINOR < 0 || MINOR > 1)); then
msg_error "Unsupported Proxmox VE version: $PVE_VER"
- msg_error "Supported versions: 8.0 – 8.9 or 9.0"
+ msg_error "Supported versions: 8.0 – 8.9 or 9.0 – 9.1"
exit 1
fi
return 0
fi
msg_error "Unsupported Proxmox VE version: $PVE_VER"
- msg_error "Supported versions: 8.0 – 8.9 or 9.0"
+ msg_error "Supported versions: 8.0 – 8.9 or 9.0 – 9.1"
exit 1
}
diff --git a/tools/addon/qbittorrent-exporter.sh b/tools/addon/qbittorrent-exporter.sh
new file mode 100644
index 000000000..29e1dff90
--- /dev/null
+++ b/tools/addon/qbittorrent-exporter.sh
@@ -0,0 +1,147 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: CrazyWolf13
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/core.func)
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/tools.func)
+source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func)
+
+VERBOSE=${var_verbose:-no}
+APP="qbittorrent-exporter"
+APP_TYPE="tools"
+INSTALL_PATH="/opt/qbittorrent-exporter/src/qbittorrent-exporter"
+CONFIG_PATH="/opt/qbittorrent-exporter.env"
+header_info
+ensure_usr_local_bin_persist
+get_current_ip &>/dev/null
+
+# OS Detection
+if [[ -f "/etc/alpine-release" ]]; then
+ OS="Alpine"
+ SERVICE_PATH="/etc/init.d/qbittorrent-exporter"
+elif grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
+ OS="Debian"
+ SERVICE_PATH="/etc/systemd/system/qbittorrent-exporter.service"
+else
+ echo -e "${CROSS} Unsupported OS detected. Exiting."
+ exit 1
+fi
+
+# Existing installation
+if [[ -f "$INSTALL_PATH" ]]; then
+ echo -e "${YW}⚠️ qbittorrent-exporter is already installed.${CL}"
+ echo -n "Uninstall ${APP}? (y/N): "
+ read -r uninstall_prompt
+ if [[ "${uninstall_prompt,,}" =~ ^(y|yes)$ ]]; then
+ msg_info "Uninstalling qbittorrent-exporter"
+ if [[ "$OS" == "Debian" ]]; then
+ systemctl disable --now qbittorrent-exporter.service &>/dev/null
+ rm -f "$SERVICE_PATH"
+ else
+ rc-service qbittorrent-exporter stop &>/dev/null
+ rc-update del qbittorrent-exporter &>/dev/null
+ rm -f "$SERVICE_PATH"
+ fi
+ rm -f "$INSTALL_PATH" "$CONFIG_PATH" ~/.qbittorrent-exporter
+ msg_ok "${APP} has been uninstalled."
+ exit 0
+ fi
+
+ echo -n "Update qbittorrent-exporter? (y/N): "
+ read -r update_prompt
+ if [[ "${update_prompt,,}" =~ ^(y|yes)$ ]]; then
+ if check_for_gh_release "qbittorrent-exporter" "martabal/qbittorrent-exporter"; then
+ fetch_and_deploy_gh_release "qbittorrent-exporter" "martabal/qbittorrent-exporter"
+ setup_go
+ msg_info "Updating qbittorrent-exporter"
+ cd /opt/qbittorrent-exporter/src
+ /usr/local/bin/go build -o ./qbittorrent-exporter
+ msg_ok "Updated Successfully!"
+ fi
+ exit 0
+ else
+ echo -e "${YW}⚠️ Update skipped. Exiting.${CL}"
+ exit 0
+ fi
+fi
+
+echo -e "${YW}⚠️ qbittorrent-exporter is not installed.${CL}"
+echo -n "Enter URL of qbittorrent example: (http://192.168.1.10:8080): "
+read -er QBITTORRENT_BASE_URL
+
+echo -n "Enter qbittorrent username: "
+read -er QBITTORRENT_USERNAME
+
+echo -n "Enter qbittorrent password: "
+read -rs QBITTORRENT_PASSWORD
+echo ""
+
+echo -n "Install qbittorrent-exporter? (y/n): "
+read -r install_prompt
+if ! [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then
+ echo -e "${YW}⚠️ Installation skipped. Exiting.${CL}"
+ exit 0
+fi
+
+fetch_and_deploy_gh_release "qbittorrent-exporter" "martabal/qbittorrent-exporter" "tarball" "v1.12.0"
+setup_go
+msg_info "Installing qbittorrent-exporter on ${OS}"
+cd /opt/qbittorrent-exporter/src
+/usr/local/bin/go build -o ./qbittorrent-exporter
+msg_ok "Installed qbittorrent-exporter"
+
+msg_info "Creating configuration"
+cat <"$CONFIG_PATH"
+QBITTORRENT_BASE_URL=${QBITTORRENT_BASE_URL}
+QBITTORRENT_USERNAME=${QBITTORRENT_USERNAME}
+QBITTORRENT_PASSWORD=${QBITTORRENT_PASSWORD}
+EOF
+msg_ok "Created configuration"
+
+msg_info "Creating service"
+if [[ "$OS" == "Debian" ]]; then
+ cat <"$SERVICE_PATH"
+[Unit]
+Description=qbittorrent-exporter
+After=network.target
+
+[Service]
+User=root
+WorkingDirectory=/opt/qbittorrent-exporter/src
+EnvironmentFile=$CONFIG_PATH
+ExecStart=/opt/qbittorrent-exporter/src/qbittorrent-exporter
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+EOF
+ systemctl enable --now qbittorrent-exporter &>/dev/null
+else
+ cat <"$SERVICE_PATH"
+#!/sbin/openrc-run
+
+command="$INSTALL_PATH"
+command_args=""
+command_background=true
+directory="/opt/qbittorrent-exporter/src"
+pidfile="/opt/qbittorrent-exporter/src/pidfile"
+
+depend() {
+ need net
+}
+
+start_pre() {
+ if [ -f "$CONFIG_PATH" ]; then
+ export \$(grep -v '^#' $CONFIG_PATH | xargs)
+ fi
+}
+EOF
+ chmod +x "$SERVICE_PATH"
+ rc-update add qbittorrent-exporter default &>/dev/null
+ rc-service qbittorrent-exporter start &>/dev/null
+fi
+msg_ok "Service created successfully"
+
+echo -e "${CM} ${GN}${APP} is reachable at: ${BL}http://$CURRENT_IP:8090/metrics${CL}"
diff --git a/tools/headers/qbittorrent-exporter b/tools/headers/qbittorrent-exporter
new file mode 100644
index 000000000..07677b261
--- /dev/null
+++ b/tools/headers/qbittorrent-exporter
@@ -0,0 +1,6 @@
+ __ _ __ __ __ __
+ ____ _/ /_ (_) /_/ /_____ _____________ ____ / /_ ___ _ ______ ____ _____/ /____ _____
+ / __ `/ __ \/ / __/ __/ __ \/ ___/ ___/ _ \/ __ \/ __/_____/ _ \| |/_/ __ \/ __ \/ ___/ __/ _ \/ ___/
+/ /_/ / /_/ / / /_/ /_/ /_/ / / / / / __/ / / / /_/_____/ __/> /_/ / /_/ / / / /_/ __/ /
+\__, /_.___/_/\__/\__/\____/_/ /_/ \___/_/ /_/\__/ \___/_/|_/ .___/\____/_/ \__/\___/_/
+ /_/ /_/
diff --git a/vm/allstarlink-vm.sh b/vm/allstarlink-vm.sh
index 20d3e4786..0dc1bf3c0 100644
--- a/vm/allstarlink-vm.sh
+++ b/vm/allstarlink-vm.sh
@@ -8,8 +8,8 @@
source /dev/stdin <<<$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
___ ____ ____ __ __ _ __ _ ____ ___
/ | / / / ___// /_____ ______/ / (_)___ / /__ | | / / |/ /
/ /| | / / /\__ \/ __/ __ `/ ___/ / / / __ \/ //_/ | | / / /|_/ /
@@ -48,306 +48,306 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "${command}"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "${command}"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "AllStarLink VM" --yesno "This will create a New AllStarLink VM. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "⚠ User exited script \n" && exit
+ header_info && echo -e "⚠ User exited script \n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne " ${HOLD} ${YW}${msg}..."
+ local msg="$1"
+ echo -ne " ${HOLD} ${YW}${msg}..."
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-3](\.[0-9]+)*"; then
- msg_error "This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-3]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.3 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- if [ "$(dpkg --print-architecture)" != "arm64" ]; then
- msg_error "This script will not work with your CPU Architekture \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ if [ "$(dpkg --print-architecture)" != "arm64" ]; then
+ msg_error "This script will not work with your CPU Architekture \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
fi
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
fi
+ fi
}
function exit_script() {
- clear
- echo -e "⚠ User exited script \n"
- exit
+ clear
+ echo -e "⚠ User exited script \n"
+ exit
}
function default_settings() {
- VMID="$NEXTID"
- FORMAT=",efitype=4m"
- MACHINE=""
- DISK_CACHE=""
- HN="allstarlink"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="2048"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="yes"
- METHOD="default"
- echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${DGN}Using Machine Type: ${BGN}i440fx${CL}"
- echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
- echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
- echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
- echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${DGN}Using Bridge: ${BGN}${BRG}${CL}"
- echo -e "${DGN}Using MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${DGN}Using VLAN: ${BGN}Default${CL}"
- echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${BL}Creating a AllStarLink VM using the above default settings${CL}"
+ VMID="$NEXTID"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ DISK_CACHE=""
+ HN="allstarlink"
+ CPU_TYPE=""
+ CORE_COUNT="2"
+ RAM_SIZE="2048"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+ echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${DGN}Using Machine Type: ${BGN}i440fx${CL}"
+ echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
+ echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
+ echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
+ echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${DGN}Using Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${DGN}Using MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${DGN}Using VLAN: ${BGN}Default${CL}"
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${BL}Creating a AllStarLink VM using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $NEXTID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID="$NEXTID"
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
- else
- exit_script
- fi
- done
-
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
- else
- echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
- fi
+ METHOD="advanced"
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $NEXTID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID="$NEXTID"
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
else
- exit_script
+ exit_script
fi
+ done
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
- else
- echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
- fi
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
else
- exit_script
+ echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
fi
+ else
+ exit_script
+ fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 allstarlink --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="allstarlink"
- echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
- else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
- fi
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
else
- exit_script
+ echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
fi
+ else
+ exit_script
+ fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $CPU_TYPE1 = "1" ]; then
- echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
- else
- echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
- fi
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 allstarlink --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="allstarlink"
+ echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
else
- exit_script
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
fi
+ else
+ exit_script
+ fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
- else
- echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
- fi
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
else
- exit_script
+ echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
fi
+ else
+ exit_script
+ fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="4096"
- echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
- else
- echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
- fi
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
else
- exit_script
+ echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
fi
+ else
+ exit_script
+ fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
- else
- echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
- fi
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="4096"
+ echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
else
- exit_script
+ echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
fi
+ else
+ exit_script
+ fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}"
- else
- MAC="$MAC1"
- echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}"
- fi
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
else
- exit_script
+ echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
fi
+ else
+ exit_script
+ fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
- else
- VLAN=",tag=$VLAN1"
- echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
- fi
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}"
else
- exit_script
+ MAC="$MAC1"
+ echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}"
fi
+ else
+ exit_script
+ fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
- else
- MTU=",mtu=$MTU1"
- echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
- fi
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
else
- exit_script
+ VLAN=",tag=$VLAN1"
+ echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
fi
+ else
+ exit_script
+ fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
else
- echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
+ MTU=",mtu=$MTU1"
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
fi
+ else
+ exit_script
+ fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a AllStarLink VM?" --no-button Do-Over 10 58); then
- echo -e "${RD}Creating a AllStarLink VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
+ else
+ echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
+ fi
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a AllStarLink VM?" --no-button Do-Over 10 58); then
+ echo -e "${RD}Creating a AllStarLink VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
@@ -359,29 +359,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- while [ -z "${STORAGE:+x}" ]; do
- STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
- "Which storage pool you would like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit
- done
+ while [ -z "${STORAGE:+x}" ]; do
+ STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
+ "Which storage pool you would like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -397,23 +397,23 @@ msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir)
- DISK_EXT=".qcow2"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format qcow2"
- THIN=""
- ;;
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Installing Pre-Requisite libguestfs-tools onto Host"
@@ -422,41 +422,41 @@ msg_ok "Installed libguestfs-tools successfully"
msg_info "Adding ASL Package Repository"
virt-customize -q -a "${FILE}" \
- --run-command "curl -fsSL https://repo.allstarlink.org/public/asl-apt-repos.deb12_all.deb -o /tmp/asl-apt-repos.deb12_all.deb" \
- --run-command "dpkg -i /tmp/asl-apt-repos.deb12_all.deb" \
- --update \
- --run-command "rm -f /tmp/asl-apt-repos.deb12_all.deb" >/dev/null
+ --run-command "curl -fsSL https://repo.allstarlink.org/public/asl-apt-repos.deb12_all.deb -o /tmp/asl-apt-repos.deb12_all.deb" \
+ --run-command "dpkg -i /tmp/asl-apt-repos.deb12_all.deb" \
+ --update \
+ --run-command "rm -f /tmp/asl-apt-repos.deb12_all.deb" >/dev/null
msg_ok "Added ASL Package Repository"
msg_info "Installing AllStarLink (patience)"
virt-customize -q -a "${FILE}" \
- --install asl3 \
- --run-command "sed -i \"/secret /s/= .*/= $(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)/\" /etc/asterisk/manager.conf" >/dev/null
+ --install asl3 \
+ --run-command "sed -i \"/secret /s/= .*/= $(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13)/\" /etc/asterisk/manager.conf" >/dev/null
msg_ok "Installed AllStarLink"
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Would you like to add Allmon3?" 10 58); then
- msg_info "Installing Allmon3"
- virt-customize -q -a "${FILE}" \
- --install allmon3 \
- --run-command "sed -i \"s/;pass=.*/;pass=\$(sed -ne 's/^secret = //p' /etc/asterisk/manager.conf)/\" /etc/allmon3/allmon3.ini" >/dev/null
- msg_ok "Installed Allmon3"
+ msg_info "Installing Allmon3"
+ virt-customize -q -a "${FILE}" \
+ --install allmon3 \
+ --run-command "sed -i \"s/;pass=.*/;pass=\$(sed -ne 's/^secret = //p' /etc/asterisk/manager.conf)/\" /etc/allmon3/allmon3.ini" >/dev/null
+ msg_ok "Installed Allmon3"
fi
msg_info "Creating a AllStarLink VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
- -name $HN -tags community-script,debian12,radio -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
+ -name $HN -tags community-script,debian12,radio -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 \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=2G \
- -boot order=scsi0 \
- -serial0 socket >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=2G \
+ -boot order=scsi0 \
+ -serial0 socket >/dev/null
qm resize $VMID scsi0 8G >/dev/null
qm set $VMID --agent enabled=1 >/dev/null
DESCRIPTION=$(
- cat <
@@ -489,9 +489,9 @@ qm set "$VMID" -description "$DESCRIPTION" >/dev/null
msg_ok "Created a AllStarLink VM ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting AllStarLink VM"
- qm start $VMID
- msg_ok "Started AllStarLink VM"
+ msg_info "Starting AllStarLink VM"
+ qm start $VMID
+ msg_ok "Started AllStarLink VM"
fi
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"
diff --git a/vm/archlinux-vm.sh b/vm/archlinux-vm.sh
index 28a2c256b..fa28e60b0 100644
--- a/vm/archlinux-vm.sh
+++ b/vm/archlinux-vm.sh
@@ -7,8 +7,8 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
___ __ __ _ _ ____ ___
/ | __________/ /_ / / (_)___ __ ___ __ | | / / |/ /
/ /| | / ___/ ___/ __ \ / / / / __ \/ / / / |/_/ | | / / /|_/ /
@@ -65,340 +65,340 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "${commad}"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "${commad}"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Arch Linux VM" --yesno "This will create a New Arch Linux VM. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR}${CM}${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
- echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "\n${CROSS}${RD}User exited script${CL}\n"
- exit
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=",efitype=4m"
- MACHINE=""
- DISK_SIZE="4G"
- DISK_CACHE=""
- HN="arch-linux"
- CPU_TYPE=""
- CORE_COUNT="1"
- RAM_SIZE="1024"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="yes"
- METHOD="default"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a Arch Linux VM using the above default settings${CL}"
+ VMID=$(get_valid_nextid)
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ DISK_SIZE="4G"
+ DISK_CACHE=""
+ HN="arch-linux"
+ CPU_TYPE=""
+ CORE_COUNT="1"
+ RAM_SIZE="1024"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Arch Linux VM using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit-script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit-script
+ fi
else
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
- if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
- DISK_SIZE="${DISK_SIZE}G"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
- elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
- exit-script
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 arch-linux --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="arch-linux"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit-script
fi
- else
- exit-script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 arch-linux --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="arch-linux"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $CPU_TYPE1 = "1" ]; then
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
else
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
+ exit-script
fi
- else
- exit-script
- fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ fi
else
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="2048"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
else
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
else
- MTU=",mtu=$MTU1"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
fi
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
-
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Arch Linux VM?" --no-button Do-Over 10 58); then
- echo -e "${CREATING}${BOLD}${DGN}Creating a Arch Linux VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Arch Linux VM?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Arch Linux VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
@@ -410,29 +410,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -448,38 +448,38 @@ msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir | cifs)
- DISK_EXT=".qcow2"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format qcow2"
- THIN=""
- ;;
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Creating a Arch Linux VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
- -name $HN -tags community-script -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 \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
- -ide2 ${STORAGE}:cloudinit \
- -boot order=scsi0 \
- -serial0 socket >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
+ -ide2 ${STORAGE}:cloudinit \
+ -boot order=scsi0 \
+ -serial0 socket >/dev/null
DESCRIPTION=$(
- cat <
@@ -510,18 +510,18 @@ EOF
)
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
if [ -n "$DISK_SIZE" ]; then
- msg_info "Resizing disk to $DISK_SIZE GB"
- qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
+ msg_info "Resizing disk to $DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
else
- msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
- qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
+ msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
fi
msg_ok "Created a Arch Linux VM ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting Arch Linux VM"
- qm start $VMID
- msg_ok "Started Arch Linux VM"
+ msg_info "Starting Arch Linux VM"
+ qm start $VMID
+ msg_ok "Started Arch Linux VM"
fi
post_update_to_api "done" "none"
diff --git a/vm/debian-13-vm.sh b/vm/debian-13-vm.sh
index e1efe022a..7a2272972 100644
--- a/vm/debian-13-vm.sh
+++ b/vm/debian-13-vm.sh
@@ -7,8 +7,8 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
____ __ _ ________
/ __ \___ / /_ (_)___ _____ < /__ /
/ / / / _ \/ __ \/ / __ `/ __ \ / / /_ <
@@ -66,374 +66,374 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- post_update_to_api "failed" "${command}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ post_update_to_api "failed" "${command}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- post_update_to_api "done" "none"
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ post_update_to_api "done" "none"
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Debian 13 VM" --yesno "This will create a New Debian 13 VM. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR}${CM}${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
-# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 (NOT 9.1+)
+# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 – 9.1
pve_check() {
- local PVE_VER
- PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
+ local PVE_VER
+ PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
- # Check for Proxmox VE 8.x: allow 8.0–8.9
- if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
- local MINOR="${BASH_REMATCH[1]}"
- if ((MINOR < 0 || MINOR > 9)); then
- msg_error "This version of Proxmox VE is not supported."
- msg_error "Supported: Proxmox VE version 8.0 – 8.9"
- exit 1
+ # Check for Proxmox VE 8.x: allow 8.0–8.9
+ if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
+ local MINOR="${BASH_REMATCH[1]}"
+ if ((MINOR < 0 || MINOR > 9)); then
+ msg_error "This version of Proxmox VE is not supported."
+ msg_error "Supported: Proxmox VE version 8.0 – 8.9"
+ exit 1
+ fi
+ return 0
fi
- return 0
- fi
- # Check for Proxmox VE 9.x: allow ONLY 9.0
- if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
- local MINOR="${BASH_REMATCH[1]}"
- if ((MINOR != 0)); then
- msg_error "This version of Proxmox VE is not yet supported."
- msg_error "Supported: Proxmox VE version 9.0"
- exit 1
+ # Check for Proxmox VE 9.x: allow 9.0–9.1
+ if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
+ local MINOR="${BASH_REMATCH[1]}"
+ if ((MINOR < 0 || MINOR > 1)); then
+ msg_error "This version of Proxmox VE is not yet supported."
+ msg_error "Supported: Proxmox VE version 9.0 – 9.1"
+ exit 1
+ fi
+ return 0
fi
- return 0
- fi
- # All other unsupported versions
- msg_error "This version of Proxmox VE is not supported."
- msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0"
- exit 1
+ # All other unsupported versions
+ msg_error "This version of Proxmox VE is not supported."
+ msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0 – 9.1"
+ exit 1
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
- echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "\n${CROSS}${RD}User exited script${CL}\n"
- exit
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=""
- MACHINE="q35"
- DISK_SIZE="30G"
- HN="debian"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="4096"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="yes"
- METHOD="default"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}q35${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a Debian 13 VM using the above default settings${CL}"
+ VMID=$(get_valid_nextid)
+ FORMAT=""
+ MACHINE="q35"
+ DISK_SIZE="30G"
+ HN="debian"
+ CPU_TYPE=""
+ CORE_COUNT="2"
+ RAM_SIZE="4096"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}q35${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Debian 13 VM using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Machine Type" 10 58 2 \
+ "q35" "Modern (PCIe, UEFI, default)" ON \
+ "i440fx" "Legacy (older compatibility)" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ "$MACH" = "q35" ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}q35${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit-script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Machine Type" 10 58 2 \
- "q35" "Modern (PCIe, UEFI, default)" ON \
- "i440fx" "Legacy (older compatibility)" OFF \
- 3>&1 1>&2 2>&3); then
- if [ "$MACH" = "q35" ]; then
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}q35${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit-script
+ fi
else
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
- if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
- DISK_SIZE="${DISK_SIZE}G"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
- elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None" OFF \
+ "1" "Write Through (Default)" ON \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
- exit-script
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None" OFF \
- "1" "Write Through (Default)" ON \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 unifi-os-server --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="unifi-os-server"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit-script
fi
- else
- exit-script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 unifi-os-server --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="unifi-os-server"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose CPU Model" --cancel-button Exit-Script 10 58 2 \
+ "KVM64" "Default – safe for migration/compatibility" ON \
+ "Host" "Use host CPU features (faster, no migration)" OFF \
+ 3>&1 1>&2 2>&3); then
+ case "$CPU_TYPE1" in
+ Host)
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ ;;
+ *)
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ ;;
+ esac
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose CPU Model" --cancel-button Exit-Script 10 58 2 \
- "KVM64" "Default – safe for migration/compatibility" ON \
- "Host" "Use host CPU features (faster, no migration)" OFF \
- 3>&1 1>&2 2>&3); then
- case "$CPU_TYPE1" in
- Host)
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
- ;;
- *)
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
- ;;
- esac
- else
- exit-script
- fi
-
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
else
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="2048"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ fi
else
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
else
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
else
- MTU=",mtu=$MTU1"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" --yesno "Configure the VM with Cloud-init?" --defaultno 10 58); then
- echo -e "${CLOUD}${BOLD}${DGN}Configure Cloud-init: ${BGN}yes${CL}"
- CLOUD_INIT="yes"
- else
- echo -e "${CLOUD}${BOLD}${DGN}Configure Cloud-init: ${BGN}no${CL}"
- CLOUD_INIT="no"
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" --yesno "Configure the VM with Cloud-init?" --defaultno 10 58); then
+ echo -e "${CLOUD}${BOLD}${DGN}Configure Cloud-init: ${BGN}yes${CL}"
+ CLOUD_INIT="yes"
+ else
+ echo -e "${CLOUD}${BOLD}${DGN}Configure Cloud-init: ${BGN}no${CL}"
+ CLOUD_INIT="no"
+ fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
+ else
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
+ fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Debian 13 VM?" --no-button Do-Over 10 58); then
- echo -e "${CREATING}${BOLD}${DGN}Creating a Debian 13 VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Debian 13 VM?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Debian 13 VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
arch_check
@@ -444,40 +444,40 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- while [ -z "${STORAGE:+x}" ]; do
- if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null; fi
- printf "\e[?25h"
- STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
- "Which storage pool would you like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ while [ -z "${STORAGE:+x}" ]; do
+ if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null; fi
+ printf "\e[?25h"
+ STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
+ "Which storage pool would you like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
msg_info "Retrieving the URL for the Debian 13 Qcow2 Disk Image"
if [ "$CLOUD_INIT" == "yes" ]; then
- URL=https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
+ URL=https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
else
- URL=https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2
+ URL=https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2
fi
CACHE_DIR="/var/lib/vz/template/cache"
CACHE_FILE="$CACHE_DIR/$(basename "$URL")"
@@ -486,49 +486,49 @@ mkdir -p "$CACHE_DIR" "$(dirname "$FILE_IMG")"
msg_ok "${CL}${BL}${URL}${CL}"
if [[ ! -s "$CACHE_FILE" ]]; then
- curl -f#SL -o "$CACHE_FILE" "$URL"
- msg_ok "Downloaded ${CL}${BL}$(basename "$CACHE_FILE")${CL}"
+ curl -f#SL -o "$CACHE_FILE" "$URL"
+ msg_ok "Downloaded ${CL}${BL}$(basename "$CACHE_FILE")${CL}"
else
- msg_ok "Using cached image ${CL}${BL}$(basename "$CACHE_FILE")${CL}"
+ msg_ok "Using cached image ${CL}${BL}$(basename "$CACHE_FILE")${CL}"
fi
set -o pipefail
msg_info "Creating Debian 13 VM shell"
qm create "$VMID" -machine q35 -bios ovmf -agent 1 -tablet 0 -localtime 1 ${CPU_TYPE} \
- -cores "$CORE_COUNT" -memory "$RAM_SIZE" -name "$HN" -tags community-script \
- -net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 -scsihw virtio-scsi-pci >/dev/null
+ -cores "$CORE_COUNT" -memory "$RAM_SIZE" -name "$HN" -tags community-script \
+ -net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 -scsihw virtio-scsi-pci >/dev/null
msg_ok "Created VM shell"
msg_info "Importing disk into storage ($STORAGE)"
if qm disk import --help >/dev/null 2>&1; then
- IMPORT_CMD=(qm disk import)
+ IMPORT_CMD=(qm disk import)
else
- IMPORT_CMD=(qm importdisk)
+ IMPORT_CMD=(qm importdisk)
fi
IMPORT_OUT="$("${IMPORT_CMD[@]}" "$VMID" "$CACHE_FILE" "$STORAGE" --format raw 2>&1 || true)"
DISK_REF="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p" | tr -d "\r\"'")"
[[ -z "$DISK_REF" ]] && DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$5 ~ ("vm-"id"-disk-") {print $1":"$5}' | sort | tail -n1)"
[[ -z "$DISK_REF" ]] && {
- msg_error "Unable to determine imported disk reference."
- echo "$IMPORT_OUT"
- exit 1
+ msg_error "Unable to determine imported disk reference."
+ echo "$IMPORT_OUT"
+ exit 1
}
msg_ok "Imported disk (${CL}${BL}${DISK_REF}${CL})"
msg_info "Attaching EFI and root disk"
if [ "$CLOUD_INIT" == "yes" ]; then
- qm set "$VMID" \
- --efidisk0 "${STORAGE}:0,efitype=4m" \
- --scsi0 "${DISK_REF},ssd=1,discard=on" \
- --scsi1 "${STORAGE}:cloudinit" \
- --boot order=scsi0 \
- --serial0 socket >/dev/null
+ qm set "$VMID" \
+ --efidisk0 "${STORAGE}:0,efitype=4m" \
+ --scsi0 "${DISK_REF},ssd=1,discard=on" \
+ --scsi1 "${STORAGE}:cloudinit" \
+ --boot order=scsi0 \
+ --serial0 socket >/dev/null
else
- qm set "$VMID" \
- --efidisk0 "${STORAGE}:0,efitype=4m" \
- --scsi0 "${DISK_REF},ssd=1,discard=on" \
- --boot order=scsi0 \
- --serial0 socket >/dev/null
+ qm set "$VMID" \
+ --efidisk0 "${STORAGE}:0,efitype=4m" \
+ --scsi0 "${DISK_REF},ssd=1,discard=on" \
+ --boot order=scsi0 \
+ --serial0 socket >/dev/null
fi
qm set "$VMID" --agent enabled=1 >/dev/null
msg_ok "Attached EFI and root disk"
@@ -538,7 +538,7 @@ qm resize "$VMID" scsi0 "${DISK_SIZE}" >/dev/null
msg_ok "Resized disk"
DESCRIPTION=$(
- cat <
@@ -571,9 +571,9 @@ qm set "$VMID" -description "$DESCRIPTION" >/dev/null
msg_ok "Created a Debian 13 VM ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting Debian 13 VM"
- qm start $VMID
- msg_ok "Started Debian 13 VM"
+ msg_info "Starting Debian 13 VM"
+ qm start $VMID
+ msg_ok "Started Debian 13 VM"
fi
msg_info "Installing resize tools in VM"
diff --git a/vm/docker-vm-debug.sh b/vm/docker-vm-debug.sh
new file mode 100644
index 000000000..6da5ec333
--- /dev/null
+++ b/vm/docker-vm-debug.sh
@@ -0,0 +1,917 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: thost96 (thost96) | Co-Author: michelroegl-brunner
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+
+source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/api.func)
+# Load Cloud-Init library for VM configuration
+source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/cloud-init.sh) || true
+
+function header_info() {
+ clear
+ cat <<"EOF"
+ ____ __ _ ____ ___
+ / __ \____ _____/ /_____ _____ | | / / |/ /
+ / / / / __ \/ ___/ //_/ _ \/ ___/ | | / / /|_/ /
+ / /_/ / /_/ / /__/ ,< / __/ / | |/ / / / /
+/_____/\____/\___/_/|_|\___/_/ |___/_/ /_/
+
+EOF
+}
+header_info
+echo -e "\n Loading..."
+GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//')
+RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
+METHOD=""
+NSAPP="docker-vm"
+var_os="debian"
+var_version="13"
+DISK_SIZE="10G"
+USE_CLOUD_INIT="no"
+INSTALL_PORTAINER="no"
+OS_TYPE=""
+OS_VERSION=""
+
+YW=$(echo "\033[33m")
+BL=$(echo "\033[36m")
+RD=$(echo "\033[01;31m")
+BGN=$(echo "\033[4;92m")
+GN=$(echo "\033[1;92m")
+DGN=$(echo "\033[32m")
+CL=$(echo "\033[m")
+
+CL=$(echo "\033[m")
+BOLD=$(echo "\033[1m")
+BFR="\\r\\033[K"
+HOLD=" "
+TAB=" "
+
+CM="${TAB}✔️${TAB}${CL}"
+CROSS="${TAB}✖️${TAB}${CL}"
+INFO="${TAB}💡${TAB}${CL}"
+OS="${TAB}🖥️${TAB}${CL}"
+CONTAINERTYPE="${TAB}📦${TAB}${CL}"
+DISKSIZE="${TAB}💾${TAB}${CL}"
+CPUCORE="${TAB}🧠${TAB}${CL}"
+RAMSIZE="${TAB}🛠️${TAB}${CL}"
+CONTAINERID="${TAB}🆔${TAB}${CL}"
+HOSTNAME="${TAB}🏠${TAB}${CL}"
+BRIDGE="${TAB}🌉${TAB}${CL}"
+GATEWAY="${TAB}🌐${TAB}${CL}"
+DEFAULT="${TAB}⚙️${TAB}${CL}"
+MACADDRESS="${TAB}🔗${TAB}${CL}"
+VLANTAG="${TAB}🏷️${TAB}${CL}"
+CREATING="${TAB}🚀${TAB}${CL}"
+ADVANCED="${TAB}🧩${TAB}${CL}"
+CLOUD="${TAB}☁️${TAB}${CL}"
+
+THIN="discard=on,ssd=1,"
+set -e
+trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
+trap cleanup EXIT
+trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
+trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
+function error_handler() {
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ post_update_to_api "failed" "${command}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
+}
+
+function get_valid_nextid() {
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
+}
+
+function cleanup_vmid() {
+ if qm status $VMID; then
+ qm stop $VMID
+ qm destroy $VMID
+ fi
+}
+
+function cleanup() {
+ popd
+ post_update_to_api "done" "none"
+ rm -rf $TEMP_DIR
+}
+
+TEMP_DIR=$(mktemp -d)
+pushd $TEMP_DIR
+if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Docker VM" --yesno "This will create a New Docker VM. Proceed?" 10 58; then
+ :
+else
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+fi
+
+function msg_info() {
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+}
+
+function msg_ok() {
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
+}
+
+function msg_error() {
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+}
+
+function spinner() {
+ local pid=$1
+ local msg="$2"
+ local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
+ local i=0
+
+ echo -ne "${TAB}${YW}${msg} "
+ while kill -0 $pid; do
+ i=$(((i + 1) % 10))
+ echo -ne "\b${spin:$i:1}"
+ sleep 0.1
+ done
+ echo -ne "\b"
+}
+
+function check_root() {
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
+}
+
+# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
+# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 – 9.1
+pve_check() {
+ local PVE_VER
+ PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
+
+ # Check for Proxmox VE 8.x: allow 8.0–8.9
+ if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
+ local MINOR="${BASH_REMATCH[1]}"
+ if ((MINOR < 0 || MINOR > 9)); then
+ msg_error "This version of Proxmox VE is not supported."
+ msg_error "Supported: Proxmox VE version 8.0 – 8.9"
+ exit 1
+ fi
+ PVE_MAJOR=8
+ return 0
+ fi
+
+ # Check for Proxmox VE 9.x: allow 9.0 – 9.1
+ if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
+ local MINOR="${BASH_REMATCH[1]}"
+ if ((MINOR < 0 || MINOR > 1)); then
+ msg_error "This version of Proxmox VE is not yet supported."
+ msg_error "Supported: Proxmox VE version 9.0 – 9.1"
+ exit 1
+ fi
+ PVE_MAJOR=9
+ return 0
+ fi
+
+ # All other unsupported versions
+ msg_error "This version of Proxmox VE is not supported."
+ msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0 – 9.1"
+ exit 1
+}
+
+function arch_check() {
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
+}
+
+function ssh_check() {
+ if command -v pveversion; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
+ fi
+}
+
+function exit-script() {
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
+}
+
+function select_os() {
+ if OS_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT OS" --radiolist \
+ "Choose Operating System for Docker VM" 14 68 4 \
+ "debian13" "Debian 13 (Trixie) - Latest" ON \
+ "debian12" "Debian 12 (Bookworm) - Stable" OFF \
+ "ubuntu2404" "Ubuntu 24.04 LTS (Noble)" OFF \
+ "ubuntu2204" "Ubuntu 22.04 LTS (Jammy)" OFF \
+ 3>&1 1>&2 2>&3); then
+ case $OS_CHOICE in
+ debian13)
+ OS_TYPE="debian"
+ OS_VERSION="13"
+ OS_CODENAME="trixie"
+ OS_DISPLAY="Debian 13 (Trixie)"
+ ;;
+ debian12)
+ OS_TYPE="debian"
+ OS_VERSION="12"
+ OS_CODENAME="bookworm"
+ OS_DISPLAY="Debian 12 (Bookworm)"
+ ;;
+ ubuntu2404)
+ OS_TYPE="ubuntu"
+ OS_VERSION="24.04"
+ OS_CODENAME="noble"
+ OS_DISPLAY="Ubuntu 24.04 LTS"
+ ;;
+ ubuntu2204)
+ OS_TYPE="ubuntu"
+ OS_VERSION="22.04"
+ OS_CODENAME="jammy"
+ OS_DISPLAY="Ubuntu 22.04 LTS"
+ ;;
+ esac
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}${OS_DISPLAY}${CL}"
+ else
+ exit-script
+ fi
+}
+
+function select_cloud_init() {
+ # Ubuntu only has cloudimg variant (always Cloud-Init), so no choice needed
+ if [ "$OS_TYPE" = "ubuntu" ]; then
+ USE_CLOUD_INIT="yes"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}yes (Ubuntu requires Cloud-Init)${CL}"
+ return
+ fi
+
+ # Debian has two image variants, so user can choose
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
+ --yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n• User accounts and passwords\n• SSH keys\n• Network settings (DHCP/Static)\n• DNS configuration\n\nYou can also configure these settings later in Proxmox UI.\n\nNote: Debian without Cloud-Init will use nocloud image with console auto-login." 18 68); then
+ USE_CLOUD_INIT="yes"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}yes${CL}"
+ else
+ USE_CLOUD_INIT="no"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}no${CL}"
+ fi
+}
+
+function select_portainer() {
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "PORTAINER" \
+ --yesno "Install Portainer for Docker management?\n\nPortainer is a lightweight management UI for Docker.\n\nAccess after installation:\n• HTTP: http://:9000\n• HTTPS: https://:9443" 14 68); then
+ INSTALL_PORTAINER="yes"
+ echo -e "${ADVANCED}${BOLD}${DGN}Portainer: ${BGN}yes${CL}"
+ else
+ INSTALL_PORTAINER="no"
+ echo -e "${ADVANCED}${BOLD}${DGN}Portainer: ${BGN}no${CL}"
+ fi
+}
+
+function get_image_url() {
+ local arch=$(dpkg --print-architecture)
+ case $OS_TYPE in
+ debian)
+ # Debian has two variants:
+ # - generic: For Cloud-Init enabled VMs
+ # - nocloud: For VMs without Cloud-Init (has console auto-login)
+ if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ echo "https://cloud.debian.org/images/cloud/${OS_CODENAME}/latest/debian-${OS_VERSION}-generic-${arch}.qcow2"
+ else
+ echo "https://cloud.debian.org/images/cloud/${OS_CODENAME}/latest/debian-${OS_VERSION}-nocloud-${arch}.qcow2"
+ fi
+ ;;
+ ubuntu)
+ # Ubuntu only has cloudimg variant (always with Cloud-Init support)
+ echo "https://cloud-images.ubuntu.com/${OS_CODENAME}/current/${OS_CODENAME}-server-cloudimg-${arch}.img"
+ ;;
+ esac
+}
+
+function default_settings() {
+ # OS Selection - ALWAYS ask
+ select_os
+
+ # Cloud-Init Selection - ALWAYS ask
+ select_cloud_init
+
+ # Portainer Selection - ALWAYS ask
+ select_portainer
+
+ # Set defaults for other settings
+ VMID=$(get_valid_nextid)
+ FORMAT=""
+ MACHINE=" -machine q35"
+ DISK_CACHE=""
+ DISK_SIZE="10G"
+ HN="docker"
+ CPU_TYPE=" -cpu host"
+ CORE_COUNT="2"
+ RAM_SIZE="4096"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+
+ # Display summary
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}Q35 (Modern)${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Docker VM using the above settings${CL}"
+}
+
+function advanced_settings() {
+ # OS Selection - ALWAYS ask (at the beginning)
+ select_os
+
+ # Cloud-Init Selection - ALWAYS ask (at the beginning)
+ select_cloud_init
+
+ # Portainer Selection - ALWAYS ask (at the beginning)
+ select_portainer
+
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" || qm status "$VMID"; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "q35" "Q35 (Modern, PCIe)" ON \
+ "i440fx" "i440fx (Legacy, PCI)" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}Q35 (Modern)${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx (Legacy)${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
+ else
+ exit-script
+ fi
+
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit-script
+ fi
+ else
+ exit-script
+ fi
+
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
+ else
+ exit-script
+ fi
+
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 docker --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="docker"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "1" "Host (Recommended)" ON \
+ "0" "KVM64" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
+ else
+ exit-script
+ fi
+
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
+ else
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
+ fi
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Docker VM?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Docker VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
+}
+
+function start_script() {
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
+}
+check_root
+arch_check
+pve_check
+ssh_check
+start_script
+post_to_api_vm
+
+msg_info "Validating Storage"
+while read -r line; do
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+done < <(pvesm status -content images | awk 'NR>1')
+VALID=$(pvesm status -content images | awk 'NR>1')
+if [ -z "$VALID" ]; then
+ msg_error "Unable to detect a valid storage location."
+ exit
+elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
+ STORAGE=${STORAGE_MENU[0]}
+else
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
+fi
+msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
+msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
+
+if ! command -v virt-customize; then
+ msg_info "Installing Pre-Requisite libguestfs-tools onto Host"
+ apt-get update
+ apt-get install libguestfs-tools lsb-release -y
+ # Workaround for Proxmox VE 9.0 libguestfs issue
+ apt-get install dhcpcd-base -y || true
+ msg_ok "Installed libguestfs-tools successfully"
+fi
+
+msg_info "Retrieving the URL for the ${OS_DISPLAY} Qcow2 Disk Image"
+URL=$(get_image_url)
+sleep 2
+msg_ok "${CL}${BL}${URL}${CL}"
+curl -f#SL -o "$(basename "$URL")" "$URL"
+echo -en "\e[1A\e[0K"
+FILE=$(basename $URL)
+msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
+
+STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
+case $STORAGE_TYPE in
+nfs | dir)
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ THIN=""
+ ;;
+btrfs)
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
+esac
+for i in {0,1}; do
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+done
+
+echo -e "${INFO}${BOLD}${GN}Preparing ${OS_DISPLAY} Qcow2 Disk Image${CL}"
+
+# Set DNS for libguestfs appliance environment (not the guest)
+export LIBGUESTFS_BACKEND_SETTINGS=dns=8.8.8.8,1.1.1.1
+
+# Always create first-boot installation script as fallback
+virt-customize -a "${FILE}" --run-command "cat > /root/install-docker.sh << 'INSTALLEOF'
+#!/bin/bash
+# Debug mode - output to stdout/stderr (no log file redirection)
+set -x
+echo \"[\\$(date)] Starting Docker installation on first boot\"
+
+# Check if Docker is already installed
+if command -v docker; then
+ echo \"[\\$(date)] Docker already installed, checking if running\"
+ systemctl start docker || true
+ if docker info; then
+ echo \"[\\$(date)] Docker is already working, exiting\"
+ exit 0
+ fi
+fi
+
+# Wait for network to be fully available
+for i in {1..30}; do
+ if ping -c 1 8.8.8.8; then
+ echo \"[\\$(date)] Network is available\"
+ break
+ fi
+ echo \"[\\$(date)] Waiting for network... attempt \\$i/30\"
+ sleep 2
+done
+
+# Configure DNS
+echo \"[\\$(date)] Configuring DNS\"
+mkdir -p /etc/systemd/resolved.conf.d
+cat > /etc/systemd/resolved.conf.d/dns.conf << DNSEOF
+[Resolve]
+DNS=8.8.8.8 1.1.1.1
+FallbackDNS=8.8.4.4 1.0.0.1
+DNSEOF
+systemctl restart systemd-resolved || true
+
+# Update package lists
+echo \"[\\$(date)] Updating package lists\"
+apt-get update
+
+# Install base packages if not already installed
+echo \"[\\$(date)] Installing base packages\"
+apt-get install -y qemu-guest-agent curl ca-certificates || true
+
+# Install Docker
+echo \"[\\$(date)] Installing Docker\"
+curl -fsSL https://get.docker.com | sh
+systemctl enable docker
+systemctl start docker
+
+# Wait for Docker to be ready
+for i in {1..10}; do
+ if docker info; then
+ echo \"[\\$(date)] Docker is ready\"
+ break
+ fi
+ sleep 1
+done
+
+# Install Portainer if requested
+INSTALL_PORTAINER_PLACEHOLDER
+
+# Create completion flag
+echo \"[\\$(date)] Docker installation completed successfully\"
+touch /root/.docker-installed
+INSTALLEOF"
+
+# Add Portainer installation script if requested
+if [ "$INSTALL_PORTAINER" = "yes" ]; then
+ virt-customize -a "${FILE}" --run-command "cat > /root/install-portainer.sh << 'PORTAINEREOF'
+#!/bin/bash
+# Debug mode - output to stdout/stderr
+set -x
+echo \"[\\$(date)] Installing Portainer\"
+docker volume create portainer_data
+docker run -d -p 9000:9000 -p 9443:9443 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest
+echo \"[\\$(date)] Portainer installed and started\"
+PORTAINEREOF"
+ virt-customize -a "${FILE}" --run-command "chmod +x /root/install-portainer.sh"
+ virt-customize -a "${FILE}" --run-command "sed -i 's|INSTALL_PORTAINER_PLACEHOLDER|/root/install-portainer.sh|' /root/install-docker.sh"
+else
+ virt-customize -a "${FILE}" --run-command "sed -i 's|INSTALL_PORTAINER_PLACEHOLDER|echo \"[\\\\\\$(date)] Skipping Portainer installation\"|' /root/install-docker.sh"
+fi
+
+virt-customize -a "${FILE}" --run-command "chmod +x /root/install-docker.sh"
+
+virt-customize -a "${FILE}" --run-command "cat > /etc/systemd/system/install-docker.service << 'SERVICEEOF'
+[Unit]
+Description=Install Docker on First Boot
+After=network-online.target
+Wants=network-online.target
+ConditionPathExists=!/root/.docker-installed
+
+[Service]
+Type=oneshot
+ExecStart=/root/install-docker.sh
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+SERVICEEOF"
+
+virt-customize -a "${FILE}" --run-command "systemctl enable install-docker.service"
+
+# Try to install packages and Docker during image customization
+DOCKER_INSTALLED_ON_FIRST_BOOT="yes" # Assume first-boot by default
+
+msg_info "Installing base packages (qemu-guest-agent, curl, ca-certificates)"
+if virt-customize -a "${FILE}" --install qemu-guest-agent,curl,ca-certificates; then
+ msg_ok "Installed base packages"
+
+ msg_info "Installing Docker via get.docker.com"
+ if virt-customize -a "${FILE}" --run-command "curl -fsSL https://get.docker.com | sh" &&
+ virt-customize -a "${FILE}" --run-command "systemctl enable docker"; then
+ msg_ok "Installed Docker"
+
+ # Optimize Docker daemon configuration
+ virt-customize -a "${FILE}" --run-command "mkdir -p /etc/docker"
+ virt-customize -a "${FILE}" --run-command "cat > /etc/docker/daemon.json << 'DOCKEREOF'
+{
+ \"storage-driver\": \"overlay2\",
+ \"log-driver\": \"json-file\",
+ \"log-opts\": {
+ \"max-size\": \"10m\",
+ \"max-file\": \"3\"
+ }
+}
+DOCKEREOF"
+
+ # Create completion flag to prevent first-boot script from running
+ virt-customize -a "${FILE}" --run-command "touch /root/.docker-installed"
+
+ DOCKER_INSTALLED_ON_FIRST_BOOT="no"
+ else
+ msg_ok "Docker will be installed on first boot (installation failed during image preparation)"
+ fi
+else
+ msg_ok "Packages will be installed on first boot (network not available during image preparation)"
+fi
+
+# Set hostname and clean machine-id
+virt-customize -a "${FILE}" --hostname "${HN}"
+virt-customize -a "${FILE}" --run-command "truncate -s 0 /etc/machine-id"
+virt-customize -a "${FILE}" --run-command "rm -f /var/lib/dbus/machine-id"
+
+# Configure SSH to allow root login with password when Cloud-Init is enabled
+# (Cloud-Init will set the password, but SSH needs to accept password authentication)
+if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ virt-customize -a "${FILE}" --run-command "sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config" || true
+ virt-customize -a "${FILE}" --run-command "sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config" || true
+fi
+
+msg_info "Expanding root partition to use full disk space"
+qemu-img create -f qcow2 expanded.qcow2 ${DISK_SIZE}
+virt-resize --expand /dev/sda1 ${FILE} expanded.qcow2
+mv expanded.qcow2 ${FILE}
+msg_ok "Expanded image to full size"
+
+msg_info "Creating a Docker VM"
+
+qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
+ -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
+qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-}
+qm set $VMID \
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
+ -boot order=scsi0 \
+ -serial0 socket
+qm set $VMID --agent enabled=1
+
+# Proxmox 9: Enable I/O Thread for better disk performance
+if [ "${PVE_MAJOR:-8}" = "9" ]; then
+ qm set $VMID -iothread 1 || true
+fi
+
+msg_ok "Created a Docker VM ${CL}${BL}(${HN})${CL}"
+
+# Add Cloud-Init drive if requested
+if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ msg_info "Configuring Cloud-Init"
+ setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
+ msg_ok "Cloud-Init configured"
+fi
+
+DESCRIPTION=$(
+ cat <
+
+
+
+
+ Docker VM
+
+
+
+
+
+
+
+
+
+ GitHub
+
+
+
+ Discussions
+
+
+
+ Issues
+
+
+EOF
+)
+qm set "$VMID" -description "$DESCRIPTION"
+
+if [ "$START_VM" == "yes" ]; then
+ msg_info "Starting Docker VM"
+ qm start $VMID
+ msg_ok "Started Docker VM"
+fi
+
+# Try to get VM IP address silently in background (max 10 seconds)
+VM_IP=""
+if [ "$START_VM" == "yes" ]; then
+ for i in {1..5}; do
+ VM_IP=$(qm guest cmd "$VMID" network-get-interfaces |
+ jq -r '.[] | select(.name != "lo") | ."ip-addresses"[]? | select(."ip-address-type" == "ipv4") | ."ip-address"' |
+ grep -v "^127\." | head -1)
+
+ if [ -n "$VM_IP" ]; then
+ break
+ fi
+ sleep 2
+ done
+fi
+
+# Display information about installed components
+echo -e "\n${INFO}${BOLD}${GN}VM Configuration Summary:${CL}"
+echo -e "${TAB}${DGN}VM ID: ${BGN}${VMID}${CL}"
+echo -e "${TAB}${DGN}Hostname: ${BGN}${HN}${CL}"
+echo -e "${TAB}${DGN}OS: ${BGN}${OS_DISPLAY}${CL}"
+
+if [ -n "$VM_IP" ]; then
+ echo -e "${TAB}${DGN}IP Address: ${BGN}${VM_IP}${CL}"
+fi
+
+if [ "$DOCKER_INSTALLED_ON_FIRST_BOOT" = "yes" ]; then
+ echo -e "${TAB}${DGN}Docker: ${BGN}Will be installed on first boot${CL}"
+ echo -e "${TAB}${YW}⚠️ Docker installation will happen automatically after VM starts${CL}"
+ echo -e "${TAB}${YW}⚠️ Wait 2-3 minutes after boot for installation to complete${CL}"
+ echo -e "${TAB}${YW}⚠️ Check installation progress: ${BL}cat /var/log/install-docker.log${CL}"
+else
+ echo -e "${TAB}${DGN}Docker: ${BGN}Latest (via get.docker.com)${CL}"
+fi
+
+if [ "$INSTALL_PORTAINER" = "yes" ]; then
+ if [ -n "$VM_IP" ]; then
+ echo -e "${TAB}${DGN}Portainer: ${BGN}https://${VM_IP}:9443${CL}"
+ else
+ echo -e "${TAB}${DGN}Portainer: ${BGN}Will be accessible at https://:9443${CL}"
+ echo -e "${TAB}${YW}⚠️ Wait 2-3 minutes after boot for installation to complete${CL}"
+ echo -e "${TAB}${YW}⚠️ Get IP with: ${BL}qm guest cmd ${VMID} network-get-interfaces${CL}"
+ fi
+fi
+if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ display_cloud_init_info "$VMID" "$HN"
+fi
+
+post_update_to_api "done" "none"
+msg_ok "Completed Successfully!\n"
diff --git a/vm/docker-vm.sh b/vm/docker-vm.sh
index 36f7177ff..c7f2b285c 100644
--- a/vm/docker-vm.sh
+++ b/vm/docker-vm.sh
@@ -1,36 +1,14 @@
#!/usr/bin/env bash
-# Docker VM (Debian/Ubuntu Cloud-Image) für Proxmox VE 8/9
-#
-# PVE 8: direct inject via virt-customize
-# PVE 9: Cloud-Init (user-data via local:snippets)
-#
+
# Copyright (c) 2021-2025 community-scripts ORG
# Author: thost96 (thost96) | Co-Author: michelroegl-brunner
-# Refactor (q35 + PVE9 cloud-init + Robustheit): MickLesk
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
-set -euo pipefail
+source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/api.func)
+# Load Cloud-Init library for VM configuration
+source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/cloud-init.sh) 2>/dev/null || true
-# ---- API-Funktionen laden ----------------------------------------------------
-source /dev/stdin <<<"$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/api.func)"
-
-# ---- UI / Farben -------------------------------------------------------------
-YW=$'\033[33m'; BL=$'\033[36m'; RD=$'\033[01;31m'; GN=$'\033[1;92m'; DGN=$'\033[32m'; CL=$'\033[m'
-BOLD=$'\033[1m'; BFR=$'\\r\\033[K'; TAB=" "
-CM="${TAB}✔️${TAB}${CL}"; CROSS="${TAB}✖️${TAB}${CL}"; INFO="${TAB}💡${TAB}${CL}"
-OSI="${TAB}🖥️${TAB}${CL}"; DISKSIZE="${TAB}💾${TAB}${CL}"; CPUCORE="${TAB}🧠${TAB}${CL}"
-RAMSIZE="${TAB}🛠️${TAB}${CL}"; CONTAINERID="${TAB}🆔${TAB}${CL}"; HOSTNAME="${TAB}🏠${TAB}${CL}"
-BRIDGE="${TAB}🌉${TAB}${CL}"; GATEWAY="${TAB}🌐${TAB}${CL}"; DEFAULT="${TAB}⚙️${TAB}${CL}"
-MACADDRESS="${TAB}🔗${TAB}${CL}"; VLANTAG="${TAB}🏷️${TAB}${CL}"; CREATING="${TAB}🚀${TAB}${CL}"
-ADVANCED="${TAB}🧩${TAB}${CL}"
-
-# ---- Spinner-/Msg-Funktionen (kompakt) ---------------------------------------
-msg_info() { echo -ne "${TAB}${YW}$1${CL}"; }
-msg_ok() { echo -e "${BFR}${CM}${GN}$1${CL}"; }
-msg_error() { echo -e "${BFR}${CROSS}${RD}$1${CL}"; }
-
-# ---- Header ------------------------------------------------------------------
-header_info() {
+function header_info() {
clear
cat <<"EOF"
____ __ _ ____ ___
@@ -41,568 +19,829 @@ header_info() {
EOF
}
-header_info; echo -e "\n Loading..."
+header_info
+echo -e "\n Loading..."
+GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//')
+RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
+METHOD=""
+NSAPP="docker-vm"
+var_os="debian"
+var_version="13"
+DISK_SIZE="10G"
+USE_CLOUD_INIT="no"
+INSTALL_PORTAINER="no"
+OS_TYPE=""
+OS_VERSION=""
+YW=$(echo "\033[33m")
+BL=$(echo "\033[36m")
+RD=$(echo "\033[01;31m")
+BGN=$(echo "\033[4;92m")
+GN=$(echo "\033[1;92m")
+DGN=$(echo "\033[32m")
+CL=$(echo "\033[m")
+
+CL=$(echo "\033[m")
+BOLD=$(echo "\033[1m")
+BFR="\\r\\033[K"
+HOLD=" "
+TAB=" "
+
+CM="${TAB}✔️${TAB}${CL}"
+CROSS="${TAB}✖️${TAB}${CL}"
+INFO="${TAB}💡${TAB}${CL}"
+OS="${TAB}🖥️${TAB}${CL}"
+CONTAINERTYPE="${TAB}📦${TAB}${CL}"
+DISKSIZE="${TAB}💾${TAB}${CL}"
+CPUCORE="${TAB}🧠${TAB}${CL}"
+RAMSIZE="${TAB}🛠️${TAB}${CL}"
+CONTAINERID="${TAB}🆔${TAB}${CL}"
+HOSTNAME="${TAB}🏠${TAB}${CL}"
+BRIDGE="${TAB}🌉${TAB}${CL}"
+GATEWAY="${TAB}🌐${TAB}${CL}"
+DEFAULT="${TAB}⚙️${TAB}${CL}"
+MACADDRESS="${TAB}🔗${TAB}${CL}"
+VLANTAG="${TAB}🏷️${TAB}${CL}"
+CREATING="${TAB}🚀${TAB}${CL}"
+ADVANCED="${TAB}🧩${TAB}${CL}"
+CLOUD="${TAB}☁️${TAB}${CL}"
+
+THIN="discard=on,ssd=1,"
+set -e
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
-trap 'cleanup' EXIT
+trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
-
-error_handler() {
- local ec=$? ln="$1" cmd="$2"
- msg_error "in line ${ln}: exit code ${ec}: while executing: ${YW}${cmd}${CL}"
- post_update_to_api "failed" "${cmd}"
- cleanup_vmid || true
- exit "$ec"
+function error_handler() {
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ post_update_to_api "failed" "${command}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
-cleanup_vmid() {
- if [[ -n "${VMID:-}" ]] && qm status "$VMID" &>/dev/null; then
- qm stop "$VMID" &>/dev/null || true
- qm destroy "$VMID" &>/dev/null || true
- fi
-}
-
-TEMP_DIR="$(mktemp -d)"
-cleanup() {
- popd >/dev/null 2>&1 || true
- rm -rf "$TEMP_DIR"
- post_update_to_api "done" "none"
-}
-
-pushd "$TEMP_DIR" >/dev/null
-
-# ---- Sanity Checks -----------------------------------------------------------
-check_root() { if [[ "$(id -u)" -ne 0 ]]; then msg_error "Run as root."; exit 1; fi; }
-arch_check() { [[ "$(dpkg --print-architecture)" = "amd64" ]] || { msg_error "ARM/PiMox nicht unterstützt."; exit 1; }; }
-pve_check() {
- local ver; ver="$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1)"
- case "$ver" in
- 8.*|9.*) : ;;
- *) msg_error "Unsupported Proxmox VE: ${ver} (need 8.x or 9.x)"; exit 1 ;;
- esac
-}
-
-check_root; arch_check; pve_check;
-
-# ---- Defaults / UI Vorbelegung ----------------------------------------------
-GEN_MAC="02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/:$//')"
-RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
-NSAPP="docker-vm"
-THIN="discard=on,ssd=1,"
-FORMAT=",efitype=4m"
-DISK_CACHE=""
-DISK_SIZE="10G"
-HN="docker"
-CPU_TYPE=""
-CORE_COUNT="2"
-RAM_SIZE="4096"
-BRG="vmbr0"
-MAC="$GEN_MAC"
-VLAN=""
-MTU=""
-START_VM="yes"
-METHOD="default"
-var_os="debian"
-var_version="12"
-
-# ---- Helper: VMID-Find -------------------------------------------------------
-get_valid_nextid() {
- local id; id=$(pvesh get /cluster/nextid)
- while :; do
- if [[ -f "/etc/pve/qemu-server/${id}.conf" || -f "/etc/pve/lxc/${id}.conf" ]]; then id=$((id+1)); continue; fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${id}($|[-_])"; then id=$((id+1)); continue; fi
+function get_valid_nextid() {
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
break
done
- echo "$id"
+ echo "$try_id"
}
-# ---- Msg Wrapper -------------------------------------------------------------
-exit-script() { clear; echo -e "\n${CROSS}${RD}User exited script${CL}\n"; exit 1; }
-
-default_settings() {
- VMID="$(get_valid_nextid)"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${GN}${VMID}${CL}"
- echo -e "${OSI}${BOLD}${DGN}CPU Model: ${GN}KVM64${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${GN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${GN}${RAM_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${GN}${DISK_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${GN}None${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${GN}${HN}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${GN}${BRG}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${GN}${MAC}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${GN}Default${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${GN}Default${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${GN}yes${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a Docker VM using the above default settings${CL}"
+function cleanup_vmid() {
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
-advanced_settings() {
- METHOD="advanced"
- [[ -z "${VMID:-}" ]] && VMID="$(get_valid_nextid)"
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 "$VMID" \
- --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- [[ -z "$VMID" ]] && VMID="$(get_valid_nextid)"
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"; sleep 1.5; continue
+function cleanup() {
+ popd >/dev/null
+ post_update_to_api "done" "none"
+ rm -rf $TEMP_DIR
+}
+
+TEMP_DIR=$(mktemp -d)
+pushd $TEMP_DIR >/dev/null
+if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Docker VM" --yesno "This will create a New Docker VM. Proceed?" 10 58; then
+ :
+else
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+fi
+
+function msg_info() {
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+}
+
+function msg_ok() {
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
+}
+
+function msg_error() {
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+}
+
+function spinner() {
+ local pid=$1
+ local msg="$2"
+ local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
+ local i=0
+
+ echo -ne "${TAB}${YW}${msg} "
+ while kill -0 $pid 2>/dev/null; do
+ i=$(((i + 1) % 10))
+ echo -ne "\b${spin:$i:1}"
+ sleep 0.1
+ done
+ echo -ne "\b"
+}
+
+function check_root() {
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
+}
+
+# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
+# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 – 9.1
+pve_check() {
+ local PVE_VER
+ PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
+
+ # Check for Proxmox VE 8.x: allow 8.0–8.9
+ if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
+ local MINOR="${BASH_REMATCH[1]}"
+ if ((MINOR < 0 || MINOR > 9)); then
+ msg_error "This version of Proxmox VE is not supported."
+ msg_error "Supported: Proxmox VE version 8.0 – 8.9"
+ exit 1
+ fi
+ PVE_MAJOR=8
+ return 0
+ fi
+
+ # Check for Proxmox VE 9.x: allow 9.0–9.1
+ if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
+ local MINOR="${BASH_REMATCH[1]}"
+ if ((MINOR < 0 || MINOR > 1)); then
+ msg_error "This version of Proxmox VE is not yet supported."
+ msg_error "Supported: Proxmox VE version 9.0 – 9.1"
+ exit 1
+ fi
+ PVE_MAJOR=9
+ return 0
+ fi
+
+ # All other unsupported versions
+ msg_error "This version of Proxmox VE is not supported."
+ msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0 – 9.1"
+ exit 1
+}
+
+function arch_check() {
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
+}
+
+function ssh_check() {
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
fi
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${GN}$VMID${CL}"
- break
- else exit-script; fi
- done
-
- echo -e "${OSI}${BOLD}${DGN}Machine Type: ${GN}q35${CL}"
-
- if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" \
- --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- DISK_SIZE="$(echo "$DISK_SIZE" | tr -d ' ')"; [[ "$DISK_SIZE" =~ ^[0-9]+$ ]] && DISK_SIZE="${DISK_SIZE}G"
- [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]] || { msg_error "Invalid Disk Size"; exit-script; }
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${GN}$DISK_SIZE${CL}"
- else exit-script; fi
-
- if DISK_CACHE_SEL=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" \
- --radiolist "Choose" --cancel-button Exit-Script 10 58 2 "0" "None (Default)" ON "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [[ "$DISK_CACHE_SEL" = "1" ]]; then DISK_CACHE="cache=writethrough,"; echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${GN}Write Through${CL}"
- else DISK_CACHE=""; echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${GN}None${CL}"
fi
- else exit-script; fi
-
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$HN" \
- --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- [[ -z "$VM_NAME" ]] && VM_NAME="docker"; HN="$(echo "${VM_NAME,,}" | tr -d ' ')"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${GN}$HN${CL}"
- else exit-script; fi
-
- if CPU_TYPE_SEL=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" \
- --radiolist "Choose" --cancel-button Exit-Script 10 58 2 "0" "KVM64 (Default)" ON "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [[ "$CPU_TYPE_SEL" = "1" ]]; then CPU_TYPE=" -cpu host"; echo -e "${OSI}${BOLD}${DGN}CPU Model: ${GN}Host${CL}"
- else CPU_TYPE=""; echo -e "${OSI}${BOLD}${DGN}CPU Model: ${GN}KVM64${CL}"
- fi
- else exit-script; fi
-
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 "$CORE_COUNT" \
- --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- [[ -z "$CORE_COUNT" ]] && CORE_COUNT="2"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${GN}$CORE_COUNT${CL}"
- else exit-script; fi
-
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 "$RAM_SIZE" \
- --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- [[ -z "$RAM_SIZE" ]] && RAM_SIZE="2048"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${GN}$RAM_SIZE${CL}"
- else exit-script; fi
-
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 "$BRG" \
- --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- [[ -z "$BRG" ]] && BRG="vmbr0"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${GN}$BRG${CL}"
- else exit-script; fi
-
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 "$MAC" \
- --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- [[ -z "$MAC1" ]] && MAC1="$GEN_MAC"; MAC="$MAC1"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${GN}$MAC${CL}"
- else exit-script; fi
-
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set VLAN (blank = default)" 8 58 "" \
- --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [[ -z "$VLAN1" ]]; then VLAN1="Default"; VLAN=""; else VLAN=",tag=$VLAN1"; fi
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${GN}$VLAN1${CL}"
- else exit-script; fi
-
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Interface MTU Size (blank = default)" 8 58 "" \
- --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [[ -z "$MTU1" ]]; then MTU1="Default"; MTU=""; else MTU=",mtu=$MTU1"; fi
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${GN}$MTU1${CL}"
- else exit-script; fi
-
- if whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" \
- --yesno "Start VM when completed?" 10 58; then START_VM="yes"; else START_VM="no"; fi
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${GN}${START_VM}${CL}"
-
- if ! whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" \
- --yesno "Ready to create a Docker VM?" --no-button Do-Over 10 58; then
- header_info; echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"; advanced_settings
- else
- echo -e "${CREATING}${BOLD}${DGN}Creating a Docker VM using the above advanced settings${CL}"
fi
}
-start_script() {
- if whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" \
- --yesno "Use Default Settings?" --no-button Advanced 10 58; then
- header_info; echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"; default_settings
- else
- header_info; echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"; advanced_settings
- fi
+function exit-script() {
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
}
-# ---------- Cloud-Init Snippet-Storage ermitteln ----------
-pick_snippet_storage() {
- # Liefert in SNIPPET_STORE und SNIPPET_DIR zurück
- mapfile -t SNIPPET_STORES < <(pvesm status -content snippets | awk 'NR>1 {print $1}')
-
- _store_snippets_dir() {
- local store="$1"
- local p; p="$(pvesm path "$store" 2>/dev/null || true)"
- [[ -n "$p" ]] || return 1
- echo "$p/snippets"
- }
-
- # 1) Gewählter Storage selbst
- if printf '%s\n' "${SNIPPET_STORES[@]}" | grep -qx -- "$STORAGE"; then
- SNIPPET_STORE="$STORAGE"
- SNIPPET_DIR="$(_store_snippets_dir "$STORAGE")" || return 1
- return 0
- fi
-
- # 2) Fallback: "local"
- if printf '%s\n' "${SNIPPET_STORES[@]}" | grep -qx -- "local"; then
- SNIPPET_STORE="local"
- SNIPPET_DIR="$(_store_snippets_dir local)" || true
- [[ -n "$SNIPPET_DIR" ]] && return 0
- fi
-
- # 3) Irgendein anderer
- for s in "${SNIPPET_STORES[@]}"; do
- SNIPPET_DIR="$(_store_snippets_dir "$s")" || continue
- SNIPPET_STORE="$s"
- return 0
- done
-
- return 1
-}
-
-start_script; post_to_api_vm
-
-# ---- OS Auswahl --------------------------------------------------------------
-choose_os() {
- local OS_CHOICE
- if OS_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Choose Base OS" --radiolist \
- "Select the OS for the Docker VM:" 12 70 3 \
- "debian12" "Debian 12 (Bookworm, stable & best for scripts)" ON \
- "debian13" "Debian 13 (Trixie, newer, but repos lag)" OFF \
- "ubuntu24" "Ubuntu 24.04 LTS (modern kernel, GPU/AI friendly)" OFF \
- 3>&1 1>&2 2>&3); then
- case "$OS_CHOICE" in
- debian12) var_os="debian"; var_version="12"; URL="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-$(dpkg --print-architecture).qcow2" ;;
- debian13) var_os="debian"; var_version="13"; URL="https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-$(dpkg --print-architecture).qcow2" ;;
- ubuntu24) var_os="ubuntu"; var_version="24.04"; URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-$(dpkg --print-architecture).img" ;;
+function select_os() {
+ if OS_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT OS" --radiolist \
+ "Choose Operating System for Docker VM" 14 68 4 \
+ "debian13" "Debian 13 (Trixie) - Latest" ON \
+ "debian12" "Debian 12 (Bookworm) - Stable" OFF \
+ "ubuntu2404" "Ubuntu 24.04 LTS (Noble)" OFF \
+ "ubuntu2204" "Ubuntu 22.04 LTS (Jammy)" OFF \
+ 3>&1 1>&2 2>&3); then
+ case $OS_CHOICE in
+ debian13)
+ OS_TYPE="debian"
+ OS_VERSION="13"
+ OS_CODENAME="trixie"
+ OS_DISPLAY="Debian 13 (Trixie)"
+ ;;
+ debian12)
+ OS_TYPE="debian"
+ OS_VERSION="12"
+ OS_CODENAME="bookworm"
+ OS_DISPLAY="Debian 12 (Bookworm)"
+ ;;
+ ubuntu2404)
+ OS_TYPE="ubuntu"
+ OS_VERSION="24.04"
+ OS_CODENAME="noble"
+ OS_DISPLAY="Ubuntu 24.04 LTS"
+ ;;
+ ubuntu2204)
+ OS_TYPE="ubuntu"
+ OS_VERSION="22.04"
+ OS_CODENAME="jammy"
+ OS_DISPLAY="Ubuntu 22.04 LTS"
+ ;;
esac
- echo -e "${OSI}${BOLD}${DGN}Selected OS: ${GN}${OS_CHOICE}${CL}"
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}${OS_DISPLAY}${CL}"
else
exit-script
fi
}
-SSH_PUB_KEYS=()
-while IFS= read -r -d '' key; do
- SSH_PUB_KEYS+=("$key")
-done < <(find /root/.ssh -maxdepth 1 -type f -name "*.pub" -print0 2>/dev/null)
-
-USE_KEYS="no"
-if [[ ${#SSH_PUB_KEYS[@]} -gt 0 ]]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "SSH Key Authentication" \
- --yesno "Found SSH public keys on the host:\n\n${SSH_PUB_KEYS[*]}\n\nUse them for root login in the new VM?" 15 70; then
- USE_KEYS="yes"
+function select_cloud_init() {
+ # Ubuntu only has cloudimg variant (always Cloud-Init), so no choice needed
+ if [ "$OS_TYPE" = "ubuntu" ]; then
+ USE_CLOUD_INIT="yes"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}yes (Ubuntu requires Cloud-Init)${CL}"
+ return
fi
-fi
-# ---- PVE Version + Install-Mode (einmalig) -----------------------------------
-PVE_MAJ="$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1 | cut -d'.' -f1)"
-case "$PVE_MAJ" in
- 8) INSTALL_MODE="direct" ;;
- 9) INSTALL_MODE="cloudinit" ;;
- *) msg_error "Unsupported Proxmox VE major: $PVE_MAJ (need 8 or 9)"; exit 1 ;;
-esac
-
-# Optionaler Override (einmalig)
-if ! whiptail --backtitle "Proxmox VE Helper Scripts" --title "Docker Installation Mode" --yesno \
- "Detected PVE ${PVE_MAJ}. Use ${INSTALL_MODE^^} mode?\n\nYes = ${INSTALL_MODE^^}\nNo = Switch to the other mode" 11 70; then
- INSTALL_MODE=$([ "$INSTALL_MODE" = "direct" ] && echo cloudinit || echo direct)
-fi
-
-# ---- Storage Auswahl ---------------------------------------------------------
-msg_info "Validating Storage"
-DISK_MENU=(); MSG_MAX_LENGTH=0
-while read -r line; do
- 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} + 2 > MSG_MAX_LENGTH )) && MSG_MAX_LENGTH=${#ITEM}+2
- DISK_MENU+=("$TAG" "$ITEM" "OFF")
-done < <(pvesm status -content images | awk 'NR>1')
-
-VALID=$(pvesm status -content images | awk 'NR>1')
-if [[ -z "$VALID" ]]; then
- msg_error "No storage with content=images available. You need at least one images-capable storage."
- exit 1
-elif (( ${#DISK_MENU[@]} / 3 == 1 )); then
- STORAGE=${DISK_MENU[0]}
-else
- while [[ -z "${STORAGE:+x}" ]]; do
- STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Disk Storage" --radiolist \
- "Which storage pool should be used for the VM disk?\n(Use Spacebar to select)" \
- 16 $((MSG_MAX_LENGTH + 23)) 6 "${DISK_MENU[@]}" 3>&1 1>&2 2>&3)
- done
-fi
-msg_ok "Using ${BL}${STORAGE}${CL} for VM disk"
-
-if [[ "$PVE_MAJ" -eq 9 && "$INSTALL_MODE" = "cloudinit" ]]; then
- msg_info "Validating Snippet Storage"
- SNIP_MENU=(); MSG_MAX_LENGTH=0
- while read -r line; do
- 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} + 2 > MSG_MAX_LENGTH )) && MSG_MAX_LENGTH=${#ITEM}+2
- SNIP_MENU+=("$TAG" "$ITEM" "OFF")
- done < <(pvesm status -content snippets | awk 'NR>1')
-
- VALID=$(pvesm status -content snippets | awk 'NR>1')
- if [[ -z "$VALID" ]]; then
- msg_error "No storage with content=snippets available. Please enable 'Snippets' on at least one directory storage (e.g. local)."
- exit 1
- elif (( ${#SNIP_MENU[@]} / 3 == 1 )); then
- SNIPPET_STORE=${SNIP_MENU[0]}
+ # Debian has two image variants, so user can choose
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
+ --yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n• User accounts and passwords\n• SSH keys\n• Network settings (DHCP/Static)\n• DNS configuration\n\nYou can also configure these settings later in Proxmox UI.\n\nNote: Debian without Cloud-Init will use nocloud image with console auto-login." 18 68); then
+ USE_CLOUD_INIT="yes"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}yes${CL}"
else
- while [[ -z "${SNIPPET_STORE:+x}" ]]; do
- SNIPPET_STORE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Snippet Storage" --radiolist \
- "Which storage should be used for the Cloud-Init snippet?\n(Use Spacebar to select)" \
- 16 $((MSG_MAX_LENGTH + 23)) 6 "${SNIP_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ USE_CLOUD_INIT="no"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}no${CL}"
fi
- msg_ok "Using ${BL}${SNIPPET_STORE}${CL} for Cloud-Init snippets"
-fi
+}
-configure_authentication() {
- local SSH_PUB_KEYS=()
- while IFS= read -r -d '' key; do
- SSH_PUB_KEYS+=("$key")
- done < <(find /root/.ssh -maxdepth 1 -type f -name "*.pub" -print0 2>/dev/null)
+function select_portainer() {
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "PORTAINER" \
+ --yesno "Install Portainer for Docker management?\n\nPortainer is a lightweight management UI for Docker.\n\nAccess after installation:\n• HTTP: http://:9000\n• HTTPS: https://:9443" 14 68); then
+ INSTALL_PORTAINER="yes"
+ echo -e "${ADVANCED}${BOLD}${DGN}Portainer: ${BGN}yes${CL}"
+ else
+ INSTALL_PORTAINER="no"
+ echo -e "${ADVANCED}${BOLD}${DGN}Portainer: ${BGN}no${CL}"
+ fi
+}
- if [[ ${#SSH_PUB_KEYS[@]} -gt 0 ]]; then
- # Found keys → ask user
- if whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "SSH Key Authentication" \
- --yesno "Found SSH public keys:\n\n${SSH_PUB_KEYS[*]}\n\nDo you want to use them for root login in the new VM?" \
- 15 70; then
- echo -e "${CM}${GN}Using SSH keys for root login${CL}"
- qm set "$VMID" --ciuser root --sshkeys "${SSH_PUB_KEYS[0]}" >/dev/null
- return
+function get_image_url() {
+ local arch=$(dpkg --print-architecture)
+ case $OS_TYPE in
+ debian)
+ # Debian has two variants:
+ # - generic: For Cloud-Init enabled VMs
+ # - nocloud: For VMs without Cloud-Init (has console auto-login)
+ if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ echo "https://cloud.debian.org/images/cloud/${OS_CODENAME}/latest/debian-${OS_VERSION}-generic-${arch}.qcow2"
+ else
+ echo "https://cloud.debian.org/images/cloud/${OS_CODENAME}/latest/debian-${OS_VERSION}-nocloud-${arch}.qcow2"
fi
- fi
+ ;;
+ ubuntu)
+ # Ubuntu only has cloudimg variant (always with Cloud-Init support)
+ echo "https://cloud-images.ubuntu.com/${OS_CODENAME}/current/${OS_CODENAME}-server-cloudimg-${arch}.img"
+ ;;
+ esac
+}
- # No key or user said No → ask for password twice
- local PASS1 PASS2
+function default_settings() {
+ # OS Selection - ALWAYS ask
+ select_os
+
+ # Cloud-Init Selection - ALWAYS ask
+ select_cloud_init
+
+ # Portainer Selection - ALWAYS ask
+ select_portainer
+
+ # Set defaults for other settings
+ VMID=$(get_valid_nextid)
+ FORMAT=""
+ MACHINE=" -machine q35"
+ DISK_CACHE=""
+ DISK_SIZE="10G"
+ HN="docker"
+ CPU_TYPE=" -cpu host"
+ CORE_COUNT="2"
+ RAM_SIZE="4096"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+
+ # Display summary
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}Q35 (Modern)${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Docker VM using the above settings${CL}"
+}
+
+function advanced_settings() {
+ # OS Selection - ALWAYS ask (at the beginning)
+ select_os
+
+ # Cloud-Init Selection - ALWAYS ask (at the beginning)
+ select_cloud_init
+
+ # Portainer Selection - ALWAYS ask (at the beginning)
+ select_portainer
+
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
while true; do
- PASS1=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "Root Password" \
- --passwordbox "Enter a password for root user" 10 70 3>&1 1>&2 2>&3) || exit-script
-
- PASS2=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "Confirm Root Password" \
- --passwordbox "Re-enter password for confirmation" 10 70 3>&1 1>&2 2>&3) || exit-script
-
- if [[ "$PASS1" == "$PASS2" && -n "$PASS1" ]]; then
- echo -e "${CM}${GN}Root password confirmed and set${CL}"
- qm set "$VMID" --ciuser root --cipassword "$PASS1" >/dev/null
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
break
else
- whiptail --backtitle "Proxmox VE Helper Scripts" \
- --title "Password Mismatch" \
- --msgbox "Passwords did not match or were empty. Please try again." 10 70
+ exit-script
fi
done
-}
-
-# ---- Cloud Image Download ----------------------------------------------------
-choose_os
-msg_info "Retrieving Cloud Image for $var_os $var_version"
-echo -e ""
-echo -e ""
-curl --retry 30 --retry-delay 3 --retry-connrefused -fSL -o "$(basename "$URL")" "$URL"
-FILE="$(basename "$URL")"
-msg_ok "Downloaded ${BL}${FILE}${CL}"
-
-# Ubuntu RAW → qcow2
-if [[ "$FILE" == *.img ]]; then
- msg_info "Converting RAW image to qcow2"
- qemu-img convert -O qcow2 "$FILE" "${FILE%.img}.qcow2"
- rm -f "$FILE"
- FILE="${FILE%.img}.qcow2"
- msg_ok "Converted to ${BL}${FILE}${CL}"
-fi
-
-# ---- Codename & Docker-Repo (einmalig) ---------------------------------------
-detect_codename_and_repo() {
- if [[ "$URL" == *"/bookworm/"* || "$FILE" == *"debian-12-"* ]]; then
- CODENAME="bookworm"; DOCKER_BASE="https://download.docker.com/linux/debian"
- elif [[ "$URL" == *"/trixie/"* || "$FILE" == *"debian-13-"* ]]; then
- CODENAME="trixie"; DOCKER_BASE="https://download.docker.com/linux/debian"
- elif [[ "$URL" == *"/noble/"* || "$FILE" == *"noble-"* ]]; then
- CODENAME="noble"; DOCKER_BASE="https://download.docker.com/linux/ubuntu"
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "q35" "Q35 (Modern, PCIe)" ON \
+ "i440fx" "i440fx (Legacy, PCI)" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}Q35 (Modern)${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx (Legacy)${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- CODENAME="bookworm"; DOCKER_BASE="https://download.docker.com/linux/debian"
+ exit-script
fi
- REPO_CODENAME="$CODENAME"
- if [[ "$DOCKER_BASE" == *"linux/debian"* && "$CODENAME" == "trixie" ]]; then
- REPO_CODENAME="bookworm"
+
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit-script
+ fi
+ else
+ exit-script
+ fi
+
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
+ else
+ exit-script
+ fi
+
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 docker --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="docker"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "1" "Host (Recommended)" ON \
+ "0" "KVM64" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
+ else
+ exit-script
+ fi
+
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
+ else
+ exit-script
+ fi
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
+ else
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
+ fi
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Docker VM?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Docker VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
fi
}
-detect_codename_and_repo
-get_snippet_dir() {
- local store="$1"
- awk -v s="$store" '
- $1 == "dir:" && $2 == s {getline; print $2 "/snippets"}
- ' /etc/pve/storage.cfg
-}
-
-# ---- PVE8: direct inject via virt-customize ----------------------------------
-if [[ "$INSTALL_MODE" = "direct" ]]; then
- msg_info "Injecting Docker & QGA into image (${CODENAME}, repo: $(basename "$DOCKER_BASE"))"
- export LIBGUESTFS_BACKEND=direct
- if ! command -v virt-customize >/dev/null 2>&1; then
- apt-get -qq update >/dev/null
- apt-get -qq install -y libguestfs-tools >/dev/null
+function start_script() {
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
fi
- vrun() { virt-customize -q -a "${FILE}" "$@" >/dev/null; }
- vrun \
- --install qemu-guest-agent,ca-certificates,curl,gnupg,lsb-release,apt-transport-https \
- --run-command "install -m 0755 -d /etc/apt/keyrings" \
- --run-command "curl -fsSL ${DOCKER_BASE}/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg" \
- --run-command "chmod a+r /etc/apt/keyrings/docker.gpg" \
- --run-command "echo 'deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] ${DOCKER_BASE} ${REPO_CODENAME} stable' > /etc/apt/sources.list.d/docker.list" \
- --run-command "apt-get update -qq" \
- --run-command "apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin" \
- --run-command "systemctl enable docker qemu-guest-agent" \
- --run-command "sed -i 's#^ENV_SUPATH.*#ENV_SUPATH PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#' /etc/login.defs || true" \
- --run-command "sed -i 's#^ENV_PATH.*#ENV_PATH PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#' /etc/login.defs || true" \
- --run-command "printf 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n' >/etc/environment" \
- --run-command "grep -q 'export PATH=' /root/.bashrc || echo 'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' >> /root/.bashrc"
- msg_ok "Docker & QGA injected"
+}
+check_root
+arch_check
+pve_check
+ssh_check
+start_script
+post_to_api_vm
+
+msg_info "Validating Storage"
+while read -r line; do
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+done < <(pvesm status -content images | awk 'NR>1')
+VALID=$(pvesm status -content images | awk 'NR>1')
+if [ -z "$VALID" ]; then
+ msg_error "Unable to detect a valid storage location."
+ exit
+elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
+ STORAGE=${STORAGE_MENU[0]}
+else
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
+fi
+msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
+msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
+
+if ! command -v virt-customize &>/dev/null; then
+ msg_info "Installing Pre-Requisite libguestfs-tools onto Host"
+ apt-get -qq update >/dev/null
+ apt-get -qq install libguestfs-tools lsb-release -y >/dev/null
+ # Workaround for Proxmox VE 9.0 libguestfs issue
+ apt-get -qq install dhcpcd-base -y >/dev/null 2>&1 || true
+ msg_ok "Installed libguestfs-tools successfully"
fi
-# ---- PVE9: Cloud-Init Snippet (NoCloud) --------------------------------------
-if [[ "$INSTALL_MODE" = "cloudinit" ]]; then
- msg_info "Preparing Cloud-Init user-data for Docker (${CODENAME})"
+msg_info "Retrieving the URL for the ${OS_DISPLAY} Qcow2 Disk Image"
+URL=$(get_image_url)
+sleep 2
+msg_ok "${CL}${BL}${URL}${CL}"
+curl -f#SL -o "$(basename "$URL")" "$URL"
+echo -en "\e[1A\e[0K"
+FILE=$(basename $URL)
+msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
- # Use SNIPPET_STORE selected earlier
- SNIPPET_DIR="$(get_snippet_dir "$SNIPPET_STORE")"
- mkdir -p "$SNIPPET_DIR"
+STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
+case $STORAGE_TYPE in
+nfs | dir)
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ THIN=""
+ ;;
+btrfs)
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
+esac
+for i in {0,1}; do
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+done
- SNIPPET_FILE="docker-${VMID}-user-data.yaml"
- SNIPPET_PATH="${SNIPPET_DIR}/${SNIPPET_FILE}"
+echo -e "${INFO}${BOLD}${GN}Preparing ${OS_DISPLAY} Qcow2 Disk Image${CL}"
- DOCKER_GPG_B64="$(curl -fsSL "${DOCKER_BASE}/gpg" | gpg --dearmor | base64 -w0)"
+# Set DNS for libguestfs appliance environment (not the guest)
+export LIBGUESTFS_BACKEND_SETTINGS=dns=8.8.8.8,1.1.1.1
-cat >"$SNIPPET_PATH" < /root/install-docker.sh << 'INSTALLEOF'
+#!/bin/bash
+# Log output to file
+exec > /var/log/install-docker.log 2>&1
+echo \"[\\$(date)] Starting Docker installation on first boot\"
-package_update: true
-package_upgrade: false
-packages:
- - ca-certificates
- - curl
- - gnupg
- - qemu-guest-agent
- - cloud-guest-utils
-
-runcmd:
- - install -m 0755 -d /etc/apt/keyrings
- - curl -fsSL ${DOCKER_BASE}/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
- - chmod a+r /etc/apt/keyrings/docker.gpg
- - echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] ${DOCKER_BASE} ${REPO_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
- - apt-get update -qq
- - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
- - systemctl enable --now qemu-guest-agent
- - systemctl enable --now docker
-
-growpart:
- mode: auto
- devices: ['/']
- ignore_growroot_disabled: false
-
-fs_resize: true
-
-power_state:
- mode: reboot
- condition: true
-EOYAML
-
- chmod 0644 "$SNIPPET_PATH"
- msg_ok "Cloud-Init user-data written: ${SNIPPET_PATH}"
+# Check if Docker is already installed
+if command -v docker >/dev/null 2>&1; then
+ echo \"[\\$(date)] Docker already installed, checking if running\"
+ systemctl start docker 2>/dev/null || true
+ if docker info >/dev/null 2>&1; then
+ echo \"[\\$(date)] Docker is already working, exiting\"
+ exit 0
+ fi
fi
-# ---- VM erstellen (q35) ------------------------------------------------------
-msg_info "Creating a Docker VM shell"
-qm create "$VMID" -machine q35 -bios ovmf -agent 1 -tablet 0 -localtime 1 ${CPU_TYPE} \
- -cores "$CORE_COUNT" -memory "$RAM_SIZE" -name "$HN" -tags community-script \
- -net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 -scsihw virtio-scsi-pci >/dev/null
-msg_ok "Created VM shell"
+# Wait for network to be fully available
+for i in {1..30}; do
+ if ping -c 1 8.8.8.8 >/dev/null 2>&1; then
+ echo \"[\\$(date)] Network is available\"
+ break
+ fi
+ echo \"[\\$(date)] Waiting for network... attempt \\$i/30\"
+ sleep 2
+done
-msg_info "Configuring authentication"
-configure_authentication
-msg_ok "Authentication configured"
+# Configure DNS
+echo \"[\\$(date)] Configuring DNS\"
+mkdir -p /etc/systemd/resolved.conf.d
+cat > /etc/systemd/resolved.conf.d/dns.conf << DNSEOF
+[Resolve]
+DNS=8.8.8.8 1.1.1.1
+FallbackDNS=8.8.4.4 1.0.0.1
+DNSEOF
+systemctl restart systemd-resolved 2>/dev/null || true
-# ---- Disk importieren --------------------------------------------------------
-msg_info "Importing disk into storage ($STORAGE)"
-if qm disk import --help >/dev/null 2>&1; then IMPORT_CMD=(qm disk import); else IMPORT_CMD=(qm importdisk); fi
-IMPORT_OUT="$("${IMPORT_CMD[@]}" "$VMID" "${FILE}" "$STORAGE" --format qcow2 2>&1 || true)"
-DISK_REF="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p" | tr -d "\r\"'")"
-[[ -z "$DISK_REF" ]] && DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$5 ~ ("vm-"id"-disk-") {print $1":"$5}' | sort | tail -n1)"
-[[ -z "$DISK_REF" ]] && { msg_error "Unable to determine imported disk reference."; echo "$IMPORT_OUT"; exit 1; }
-msg_ok "Imported disk (${BL}${DISK_REF}${CL})"
+# Update package lists
+echo \"[\\$(date)] Updating package lists\"
+apt-get update
-SSHKEYS_ARG=""
-if [[ -s /root/.ssh/authorized_keys ]]; then
- SSHKEYS_ARG="--sshkeys /root/.ssh/authorized_keys"
+# Install base packages if not already installed
+echo \"[\\$(date)] Installing base packages\"
+apt-get install -y qemu-guest-agent curl ca-certificates 2>/dev/null || true
+
+# Install Docker
+echo \"[\\$(date)] Installing Docker\"
+curl -fsSL https://get.docker.com | sh
+systemctl enable docker
+systemctl start docker
+
+# Wait for Docker to be ready
+for i in {1..10}; do
+ if docker info >/dev/null 2>&1; then
+ echo \"[\\$(date)] Docker is ready\"
+ break
+ fi
+ sleep 1
+done
+
+# Install Portainer if requested
+INSTALL_PORTAINER_PLACEHOLDER
+
+# Create completion flag
+echo \"[\\$(date)] Docker installation completed successfully\"
+touch /root/.docker-installed
+INSTALLEOF" >/dev/null
+
+# Add Portainer installation script if requested
+if [ "$INSTALL_PORTAINER" = "yes" ]; then
+ virt-customize -q -a "${FILE}" --run-command "cat > /root/install-portainer.sh << 'PORTAINEREOF'
+#!/bin/bash
+exec >> /var/log/install-docker.log 2>&1
+echo \"[\\$(date)] Installing Portainer\"
+docker volume create portainer_data
+docker run -d -p 9000:9000 -p 9443:9443 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest
+echo \"[\\$(date)] Portainer installed and started\"
+PORTAINEREOF" >/dev/null
+ virt-customize -q -a "${FILE}" --run-command "chmod +x /root/install-portainer.sh" >/dev/null
+ virt-customize -q -a "${FILE}" --run-command "sed -i 's|INSTALL_PORTAINER_PLACEHOLDER|/root/install-portainer.sh|' /root/install-docker.sh" >/dev/null
+else
+ virt-customize -q -a "${FILE}" --run-command "sed -i 's|INSTALL_PORTAINER_PLACEHOLDER|echo \"[\\\\\\$(date)] Skipping Portainer installation\"|' /root/install-docker.sh" >/dev/null
fi
-# ---- EFI + Root + Cloud-Init anhängen ---------------------------------------
-msg_info "Attaching EFI/root disk and Cloud-Init (Patience)"
-qm set "$VMID" \
- --efidisk0 "${STORAGE}:0${FORMAT}" \
- --scsi0 "${DISK_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE}" \
- --boot order=scsi0 \
- --serial0 socket \
- --agent enabled=1,fstrim_cloned_disks=1 \
- --ide2 "${STORAGE}:cloudinit" \
- --ipconfig0 "ip=dhcp" >/dev/null
+virt-customize -q -a "${FILE}" --run-command "chmod +x /root/install-docker.sh" >/dev/null
-if [[ "$INSTALL_MODE" = "cloudinit" ]]; then
- qm set "$VMID" --cicustom "user=${SNIPPET_STORE}:snippets/${SNIPPET_FILE}" >/dev/null
+virt-customize -q -a "${FILE}" --run-command "cat > /etc/systemd/system/install-docker.service << 'SERVICEEOF'
+[Unit]
+Description=Install Docker on First Boot
+After=network-online.target
+Wants=network-online.target
+ConditionPathExists=!/root/.docker-installed
+
+[Service]
+Type=oneshot
+ExecStart=/root/install-docker.sh
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+SERVICEEOF" >/dev/null
+
+virt-customize -q -a "${FILE}" --run-command "systemctl enable install-docker.service" >/dev/null
+
+# Try to install packages and Docker during image customization
+DOCKER_INSTALLED_ON_FIRST_BOOT="yes" # Assume first-boot by default
+
+msg_info "Installing base packages (qemu-guest-agent, curl, ca-certificates)"
+if virt-customize -a "${FILE}" --install qemu-guest-agent,curl,ca-certificates >/dev/null 2>&1; then
+ msg_ok "Installed base packages"
+
+ msg_info "Installing Docker via get.docker.com"
+ if virt-customize -q -a "${FILE}" --run-command "curl -fsSL https://get.docker.com | sh" >/dev/null 2>&1 &&
+ virt-customize -q -a "${FILE}" --run-command "systemctl enable docker" >/dev/null 2>&1; then
+ msg_ok "Installed Docker"
+
+ # Optimize Docker daemon configuration
+ virt-customize -q -a "${FILE}" --run-command "mkdir -p /etc/docker" >/dev/null 2>&1
+ virt-customize -q -a "${FILE}" --run-command "cat > /etc/docker/daemon.json << 'DOCKEREOF'
+{
+ \"storage-driver\": \"overlay2\",
+ \"log-driver\": \"json-file\",
+ \"log-opts\": {
+ \"max-size\": \"10m\",
+ \"max-file\": \"3\"
+ }
+}
+DOCKEREOF" >/dev/null 2>&1
+
+ # Create completion flag to prevent first-boot script from running
+ virt-customize -q -a "${FILE}" --run-command "touch /root/.docker-installed" >/dev/null 2>&1
+
+ DOCKER_INSTALLED_ON_FIRST_BOOT="no"
+ else
+ msg_ok "Docker will be installed on first boot (installation failed during image preparation)"
+ fi
+else
+ msg_ok "Packages will be installed on first boot (network not available during image preparation)"
fi
-msg_ok "Attached EFI/root and Cloud-Init"
-# ---- Disk auf Zielgröße im PVE-Layer (Cloud-Init wächst FS) ------------------
-msg_info "Resizing disk to $DISK_SIZE (PVE layer)"
-qm resize "$VMID" scsi0 "${DISK_SIZE}" >/dev/null || true
-msg_ok "Resized disk"
+# Set hostname and clean machine-id
+virt-customize -q -a "${FILE}" --hostname "${HN}" >/dev/null 2>&1
+virt-customize -q -a "${FILE}" --run-command "truncate -s 0 /etc/machine-id" >/dev/null 2>&1
+virt-customize -q -a "${FILE}" --run-command "rm -f /var/lib/dbus/machine-id" >/dev/null 2>&1
+
+# Configure SSH to allow root login with password when Cloud-Init is enabled
+# (Cloud-Init will set the password, but SSH needs to accept password authentication)
+if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ virt-customize -q -a "${FILE}" --run-command "sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config" >/dev/null 2>&1 || true
+ virt-customize -q -a "${FILE}" --run-command "sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config" >/dev/null 2>&1 || true
+fi
+
+msg_info "Expanding root partition to use full disk space"
+qemu-img create -f qcow2 expanded.qcow2 ${DISK_SIZE} >/dev/null 2>&1
+virt-resize --quiet --expand /dev/sda1 ${FILE} expanded.qcow2 >/dev/null 2>&1
+mv expanded.qcow2 ${FILE} >/dev/null 2>&1
+msg_ok "Expanded image to full size"
+
+msg_info "Creating a Docker VM"
+
+qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
+ -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 \
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
+ -boot order=scsi0 \
+ -serial0 socket >/dev/null
+qm set $VMID --agent enabled=1 >/dev/null
+
+# Proxmox 9: Enable I/O Thread for better disk performance
+if [ "${PVE_MAJOR:-8}" = "9" ]; then
+ qm set $VMID -iothread 1 >/dev/null 2>&1 || true
+fi
+
+msg_ok "Created a Docker VM ${CL}${BL}(${HN})${CL}"
+
+# Add Cloud-Init drive if requested
+if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ msg_info "Configuring Cloud-Init"
+ setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" >/dev/null 2>&1
+ msg_ok "Cloud-Init configured"
+fi
-# ---- Beschreibung ------------------------------------------------------------
DESCRIPTION=$(
- cat <<'EOF'
+ cat <
+
Docker VM
+
+
GitHub
@@ -619,21 +858,59 @@ DESCRIPTION=$(
EOF
)
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
-msg_ok "Created a Docker VM ${BL}(${HN})${CL}"
-# ---- Start -------------------------------------------------------------------
-if [[ "$START_VM" == "yes" ]]; then
+if [ "$START_VM" == "yes" ]; then
msg_info "Starting Docker VM"
- qm start "$VMID"
+ qm start $VMID >/dev/null 2>&1
msg_ok "Started Docker VM"
fi
+# Try to get VM IP address silently in background (max 10 seconds)
+VM_IP=""
+if [ "$START_VM" == "yes" ]; then
+ for i in {1..5}; do
+ VM_IP=$(qm guest cmd "$VMID" network-get-interfaces 2>/dev/null |
+ jq -r '.[] | select(.name != "lo") | ."ip-addresses"[]? | select(."ip-address-type" == "ipv4") | ."ip-address"' 2>/dev/null |
+ grep -v "^127\." | head -1)
+
+ if [ -n "$VM_IP" ]; then
+ break
+ fi
+ sleep 2
+ done
+fi
+
+# Display information about installed components
+echo -e "\n${INFO}${BOLD}${GN}VM Configuration Summary:${CL}"
+echo -e "${TAB}${DGN}VM ID: ${BGN}${VMID}${CL}"
+echo -e "${TAB}${DGN}Hostname: ${BGN}${HN}${CL}"
+echo -e "${TAB}${DGN}OS: ${BGN}${OS_DISPLAY}${CL}"
+
+if [ -n "$VM_IP" ]; then
+ echo -e "${TAB}${DGN}IP Address: ${BGN}${VM_IP}${CL}"
+fi
+
+if [ "$DOCKER_INSTALLED_ON_FIRST_BOOT" = "yes" ]; then
+ echo -e "${TAB}${DGN}Docker: ${BGN}Will be installed on first boot${CL}"
+ echo -e "${TAB}${YW}⚠️ Docker installation will happen automatically after VM starts${CL}"
+ echo -e "${TAB}${YW}⚠️ Wait 2-3 minutes after boot for installation to complete${CL}"
+ echo -e "${TAB}${YW}⚠️ Check installation progress: ${BL}cat /var/log/install-docker.log${CL}"
+else
+ echo -e "${TAB}${DGN}Docker: ${BGN}Latest (via get.docker.com)${CL}"
+fi
+
+if [ "$INSTALL_PORTAINER" = "yes" ]; then
+ if [ -n "$VM_IP" ]; then
+ echo -e "${TAB}${DGN}Portainer: ${BGN}https://${VM_IP}:9443${CL}"
+ else
+ echo -e "${TAB}${DGN}Portainer: ${BGN}Will be accessible at https://:9443${CL}"
+ echo -e "${TAB}${YW}⚠️ Wait 2-3 minutes after boot for installation to complete${CL}"
+ echo -e "${TAB}${YW}⚠️ Get IP with: ${BL}qm guest cmd ${VMID} network-get-interfaces${CL}"
+ fi
+fi
+if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ display_cloud_init_info "$VMID" "$HN"
+fi
+
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"
-
-# ---- Hinweise/Debug (Cloud-Init) --------------------------------------------
-# In der VM prüfen:
-# journalctl -u cloud-init -b
-# cat /var/log/cloud-init.log
-# cat /var/log/cloud-init-output.log
-# cloud-init status --long
diff --git a/vm/docker-vm.sh.bak b/vm/docker-vm.sh.bak
new file mode 100644
index 000000000..36f7177ff
--- /dev/null
+++ b/vm/docker-vm.sh.bak
@@ -0,0 +1,639 @@
+#!/usr/bin/env bash
+# Docker VM (Debian/Ubuntu Cloud-Image) für Proxmox VE 8/9
+#
+# PVE 8: direct inject via virt-customize
+# PVE 9: Cloud-Init (user-data via local:snippets)
+#
+# Copyright (c) 2021-2025 community-scripts ORG
+# Author: thost96 (thost96) | Co-Author: michelroegl-brunner
+# Refactor (q35 + PVE9 cloud-init + Robustheit): MickLesk
+# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+
+set -euo pipefail
+
+# ---- API-Funktionen laden ----------------------------------------------------
+source /dev/stdin <<<"$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/api.func)"
+
+# ---- UI / Farben -------------------------------------------------------------
+YW=$'\033[33m'; BL=$'\033[36m'; RD=$'\033[01;31m'; GN=$'\033[1;92m'; DGN=$'\033[32m'; CL=$'\033[m'
+BOLD=$'\033[1m'; BFR=$'\\r\\033[K'; TAB=" "
+CM="${TAB}✔️${TAB}${CL}"; CROSS="${TAB}✖️${TAB}${CL}"; INFO="${TAB}💡${TAB}${CL}"
+OSI="${TAB}🖥️${TAB}${CL}"; DISKSIZE="${TAB}💾${TAB}${CL}"; CPUCORE="${TAB}🧠${TAB}${CL}"
+RAMSIZE="${TAB}🛠️${TAB}${CL}"; CONTAINERID="${TAB}🆔${TAB}${CL}"; HOSTNAME="${TAB}🏠${TAB}${CL}"
+BRIDGE="${TAB}🌉${TAB}${CL}"; GATEWAY="${TAB}🌐${TAB}${CL}"; DEFAULT="${TAB}⚙️${TAB}${CL}"
+MACADDRESS="${TAB}🔗${TAB}${CL}"; VLANTAG="${TAB}🏷️${TAB}${CL}"; CREATING="${TAB}🚀${TAB}${CL}"
+ADVANCED="${TAB}🧩${TAB}${CL}"
+
+# ---- Spinner-/Msg-Funktionen (kompakt) ---------------------------------------
+msg_info() { echo -ne "${TAB}${YW}$1${CL}"; }
+msg_ok() { echo -e "${BFR}${CM}${GN}$1${CL}"; }
+msg_error() { echo -e "${BFR}${CROSS}${RD}$1${CL}"; }
+
+# ---- Header ------------------------------------------------------------------
+header_info() {
+ clear
+ cat <<"EOF"
+ ____ __ _ ____ ___
+ / __ \____ _____/ /_____ _____ | | / / |/ /
+ / / / / __ \/ ___/ //_/ _ \/ ___/ | | / / /|_/ /
+ / /_/ / /_/ / /__/ ,< / __/ / | |/ / / / /
+/_____/\____/\___/_/|_|\___/_/ |___/_/ /_/
+
+EOF
+}
+header_info; echo -e "\n Loading..."
+
+trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
+trap 'cleanup' EXIT
+trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
+trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
+
+error_handler() {
+ local ec=$? ln="$1" cmd="$2"
+ msg_error "in line ${ln}: exit code ${ec}: while executing: ${YW}${cmd}${CL}"
+ post_update_to_api "failed" "${cmd}"
+ cleanup_vmid || true
+ exit "$ec"
+}
+
+cleanup_vmid() {
+ if [[ -n "${VMID:-}" ]] && qm status "$VMID" &>/dev/null; then
+ qm stop "$VMID" &>/dev/null || true
+ qm destroy "$VMID" &>/dev/null || true
+ fi
+}
+
+TEMP_DIR="$(mktemp -d)"
+cleanup() {
+ popd >/dev/null 2>&1 || true
+ rm -rf "$TEMP_DIR"
+ post_update_to_api "done" "none"
+}
+
+pushd "$TEMP_DIR" >/dev/null
+
+# ---- Sanity Checks -----------------------------------------------------------
+check_root() { if [[ "$(id -u)" -ne 0 ]]; then msg_error "Run as root."; exit 1; fi; }
+arch_check() { [[ "$(dpkg --print-architecture)" = "amd64" ]] || { msg_error "ARM/PiMox nicht unterstützt."; exit 1; }; }
+pve_check() {
+ local ver; ver="$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1)"
+ case "$ver" in
+ 8.*|9.*) : ;;
+ *) msg_error "Unsupported Proxmox VE: ${ver} (need 8.x or 9.x)"; exit 1 ;;
+ esac
+}
+
+check_root; arch_check; pve_check;
+
+# ---- Defaults / UI Vorbelegung ----------------------------------------------
+GEN_MAC="02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/:$//')"
+RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
+NSAPP="docker-vm"
+THIN="discard=on,ssd=1,"
+FORMAT=",efitype=4m"
+DISK_CACHE=""
+DISK_SIZE="10G"
+HN="docker"
+CPU_TYPE=""
+CORE_COUNT="2"
+RAM_SIZE="4096"
+BRG="vmbr0"
+MAC="$GEN_MAC"
+VLAN=""
+MTU=""
+START_VM="yes"
+METHOD="default"
+var_os="debian"
+var_version="12"
+
+# ---- Helper: VMID-Find -------------------------------------------------------
+get_valid_nextid() {
+ local id; id=$(pvesh get /cluster/nextid)
+ while :; do
+ if [[ -f "/etc/pve/qemu-server/${id}.conf" || -f "/etc/pve/lxc/${id}.conf" ]]; then id=$((id+1)); continue; fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${id}($|[-_])"; then id=$((id+1)); continue; fi
+ break
+ done
+ echo "$id"
+}
+
+# ---- Msg Wrapper -------------------------------------------------------------
+exit-script() { clear; echo -e "\n${CROSS}${RD}User exited script${CL}\n"; exit 1; }
+
+default_settings() {
+ VMID="$(get_valid_nextid)"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${GN}${VMID}${CL}"
+ echo -e "${OSI}${BOLD}${DGN}CPU Model: ${GN}KVM64${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${GN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${GN}${RAM_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${GN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${GN}None${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${GN}${HN}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${GN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${GN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${GN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${GN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${GN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Docker VM using the above default settings${CL}"
+}
+
+advanced_settings() {
+ METHOD="advanced"
+ [[ -z "${VMID:-}" ]] && VMID="$(get_valid_nextid)"
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 "$VMID" \
+ --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ [[ -z "$VMID" ]] && VMID="$(get_valid_nextid)"
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"; sleep 1.5; continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${GN}$VMID${CL}"
+ break
+ else exit-script; fi
+ done
+
+ echo -e "${OSI}${BOLD}${DGN}Machine Type: ${GN}q35${CL}"
+
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" \
+ --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE="$(echo "$DISK_SIZE" | tr -d ' ')"; [[ "$DISK_SIZE" =~ ^[0-9]+$ ]] && DISK_SIZE="${DISK_SIZE}G"
+ [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]] || { msg_error "Invalid Disk Size"; exit-script; }
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${GN}$DISK_SIZE${CL}"
+ else exit-script; fi
+
+ if DISK_CACHE_SEL=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" \
+ --radiolist "Choose" --cancel-button Exit-Script 10 58 2 "0" "None (Default)" ON "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [[ "$DISK_CACHE_SEL" = "1" ]]; then DISK_CACHE="cache=writethrough,"; echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${GN}Write Through${CL}"
+ else DISK_CACHE=""; echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${GN}None${CL}"
+ fi
+ else exit-script; fi
+
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$HN" \
+ --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ [[ -z "$VM_NAME" ]] && VM_NAME="docker"; HN="$(echo "${VM_NAME,,}" | tr -d ' ')"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${GN}$HN${CL}"
+ else exit-script; fi
+
+ if CPU_TYPE_SEL=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" \
+ --radiolist "Choose" --cancel-button Exit-Script 10 58 2 "0" "KVM64 (Default)" ON "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [[ "$CPU_TYPE_SEL" = "1" ]]; then CPU_TYPE=" -cpu host"; echo -e "${OSI}${BOLD}${DGN}CPU Model: ${GN}Host${CL}"
+ else CPU_TYPE=""; echo -e "${OSI}${BOLD}${DGN}CPU Model: ${GN}KVM64${CL}"
+ fi
+ else exit-script; fi
+
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 "$CORE_COUNT" \
+ --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ [[ -z "$CORE_COUNT" ]] && CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${GN}$CORE_COUNT${CL}"
+ else exit-script; fi
+
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 "$RAM_SIZE" \
+ --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ [[ -z "$RAM_SIZE" ]] && RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${GN}$RAM_SIZE${CL}"
+ else exit-script; fi
+
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 "$BRG" \
+ --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ [[ -z "$BRG" ]] && BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${GN}$BRG${CL}"
+ else exit-script; fi
+
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 "$MAC" \
+ --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ [[ -z "$MAC1" ]] && MAC1="$GEN_MAC"; MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${GN}$MAC${CL}"
+ else exit-script; fi
+
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set VLAN (blank = default)" 8 58 "" \
+ --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [[ -z "$VLAN1" ]]; then VLAN1="Default"; VLAN=""; else VLAN=",tag=$VLAN1"; fi
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${GN}$VLAN1${CL}"
+ else exit-script; fi
+
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Interface MTU Size (blank = default)" 8 58 "" \
+ --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [[ -z "$MTU1" ]]; then MTU1="Default"; MTU=""; else MTU=",mtu=$MTU1"; fi
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${GN}$MTU1${CL}"
+ else exit-script; fi
+
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" \
+ --yesno "Start VM when completed?" 10 58; then START_VM="yes"; else START_VM="no"; fi
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${GN}${START_VM}${CL}"
+
+ if ! whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" \
+ --yesno "Ready to create a Docker VM?" --no-button Do-Over 10 58; then
+ header_info; echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"; advanced_settings
+ else
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Docker VM using the above advanced settings${CL}"
+ fi
+}
+
+start_script() {
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" \
+ --yesno "Use Default Settings?" --no-button Advanced 10 58; then
+ header_info; echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"; default_settings
+ else
+ header_info; echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"; advanced_settings
+ fi
+}
+
+# ---------- Cloud-Init Snippet-Storage ermitteln ----------
+pick_snippet_storage() {
+ # Liefert in SNIPPET_STORE und SNIPPET_DIR zurück
+ mapfile -t SNIPPET_STORES < <(pvesm status -content snippets | awk 'NR>1 {print $1}')
+
+ _store_snippets_dir() {
+ local store="$1"
+ local p; p="$(pvesm path "$store" 2>/dev/null || true)"
+ [[ -n "$p" ]] || return 1
+ echo "$p/snippets"
+ }
+
+ # 1) Gewählter Storage selbst
+ if printf '%s\n' "${SNIPPET_STORES[@]}" | grep -qx -- "$STORAGE"; then
+ SNIPPET_STORE="$STORAGE"
+ SNIPPET_DIR="$(_store_snippets_dir "$STORAGE")" || return 1
+ return 0
+ fi
+
+ # 2) Fallback: "local"
+ if printf '%s\n' "${SNIPPET_STORES[@]}" | grep -qx -- "local"; then
+ SNIPPET_STORE="local"
+ SNIPPET_DIR="$(_store_snippets_dir local)" || true
+ [[ -n "$SNIPPET_DIR" ]] && return 0
+ fi
+
+ # 3) Irgendein anderer
+ for s in "${SNIPPET_STORES[@]}"; do
+ SNIPPET_DIR="$(_store_snippets_dir "$s")" || continue
+ SNIPPET_STORE="$s"
+ return 0
+ done
+
+ return 1
+}
+
+start_script; post_to_api_vm
+
+# ---- OS Auswahl --------------------------------------------------------------
+choose_os() {
+ local OS_CHOICE
+ if OS_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Choose Base OS" --radiolist \
+ "Select the OS for the Docker VM:" 12 70 3 \
+ "debian12" "Debian 12 (Bookworm, stable & best for scripts)" ON \
+ "debian13" "Debian 13 (Trixie, newer, but repos lag)" OFF \
+ "ubuntu24" "Ubuntu 24.04 LTS (modern kernel, GPU/AI friendly)" OFF \
+ 3>&1 1>&2 2>&3); then
+ case "$OS_CHOICE" in
+ debian12) var_os="debian"; var_version="12"; URL="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-$(dpkg --print-architecture).qcow2" ;;
+ debian13) var_os="debian"; var_version="13"; URL="https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-$(dpkg --print-architecture).qcow2" ;;
+ ubuntu24) var_os="ubuntu"; var_version="24.04"; URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-$(dpkg --print-architecture).img" ;;
+ esac
+ echo -e "${OSI}${BOLD}${DGN}Selected OS: ${GN}${OS_CHOICE}${CL}"
+ else
+ exit-script
+ fi
+}
+
+SSH_PUB_KEYS=()
+while IFS= read -r -d '' key; do
+ SSH_PUB_KEYS+=("$key")
+done < <(find /root/.ssh -maxdepth 1 -type f -name "*.pub" -print0 2>/dev/null)
+
+USE_KEYS="no"
+if [[ ${#SSH_PUB_KEYS[@]} -gt 0 ]]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "SSH Key Authentication" \
+ --yesno "Found SSH public keys on the host:\n\n${SSH_PUB_KEYS[*]}\n\nUse them for root login in the new VM?" 15 70; then
+ USE_KEYS="yes"
+ fi
+fi
+
+# ---- PVE Version + Install-Mode (einmalig) -----------------------------------
+PVE_MAJ="$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1 | cut -d'.' -f1)"
+case "$PVE_MAJ" in
+ 8) INSTALL_MODE="direct" ;;
+ 9) INSTALL_MODE="cloudinit" ;;
+ *) msg_error "Unsupported Proxmox VE major: $PVE_MAJ (need 8 or 9)"; exit 1 ;;
+esac
+
+# Optionaler Override (einmalig)
+if ! whiptail --backtitle "Proxmox VE Helper Scripts" --title "Docker Installation Mode" --yesno \
+ "Detected PVE ${PVE_MAJ}. Use ${INSTALL_MODE^^} mode?\n\nYes = ${INSTALL_MODE^^}\nNo = Switch to the other mode" 11 70; then
+ INSTALL_MODE=$([ "$INSTALL_MODE" = "direct" ] && echo cloudinit || echo direct)
+fi
+
+# ---- Storage Auswahl ---------------------------------------------------------
+msg_info "Validating Storage"
+DISK_MENU=(); MSG_MAX_LENGTH=0
+while read -r line; do
+ 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} + 2 > MSG_MAX_LENGTH )) && MSG_MAX_LENGTH=${#ITEM}+2
+ DISK_MENU+=("$TAG" "$ITEM" "OFF")
+done < <(pvesm status -content images | awk 'NR>1')
+
+VALID=$(pvesm status -content images | awk 'NR>1')
+if [[ -z "$VALID" ]]; then
+ msg_error "No storage with content=images available. You need at least one images-capable storage."
+ exit 1
+elif (( ${#DISK_MENU[@]} / 3 == 1 )); then
+ STORAGE=${DISK_MENU[0]}
+else
+ while [[ -z "${STORAGE:+x}" ]]; do
+ STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Disk Storage" --radiolist \
+ "Which storage pool should be used for the VM disk?\n(Use Spacebar to select)" \
+ 16 $((MSG_MAX_LENGTH + 23)) 6 "${DISK_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
+fi
+msg_ok "Using ${BL}${STORAGE}${CL} for VM disk"
+
+if [[ "$PVE_MAJ" -eq 9 && "$INSTALL_MODE" = "cloudinit" ]]; then
+ msg_info "Validating Snippet Storage"
+ SNIP_MENU=(); MSG_MAX_LENGTH=0
+ while read -r line; do
+ 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} + 2 > MSG_MAX_LENGTH )) && MSG_MAX_LENGTH=${#ITEM}+2
+ SNIP_MENU+=("$TAG" "$ITEM" "OFF")
+ done < <(pvesm status -content snippets | awk 'NR>1')
+
+ VALID=$(pvesm status -content snippets | awk 'NR>1')
+ if [[ -z "$VALID" ]]; then
+ msg_error "No storage with content=snippets available. Please enable 'Snippets' on at least one directory storage (e.g. local)."
+ exit 1
+ elif (( ${#SNIP_MENU[@]} / 3 == 1 )); then
+ SNIPPET_STORE=${SNIP_MENU[0]}
+ else
+ while [[ -z "${SNIPPET_STORE:+x}" ]]; do
+ SNIPPET_STORE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Snippet Storage" --radiolist \
+ "Which storage should be used for the Cloud-Init snippet?\n(Use Spacebar to select)" \
+ 16 $((MSG_MAX_LENGTH + 23)) 6 "${SNIP_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
+ fi
+ msg_ok "Using ${BL}${SNIPPET_STORE}${CL} for Cloud-Init snippets"
+fi
+
+configure_authentication() {
+ local SSH_PUB_KEYS=()
+ while IFS= read -r -d '' key; do
+ SSH_PUB_KEYS+=("$key")
+ done < <(find /root/.ssh -maxdepth 1 -type f -name "*.pub" -print0 2>/dev/null)
+
+ if [[ ${#SSH_PUB_KEYS[@]} -gt 0 ]]; then
+ # Found keys → ask user
+ if whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "SSH Key Authentication" \
+ --yesno "Found SSH public keys:\n\n${SSH_PUB_KEYS[*]}\n\nDo you want to use them for root login in the new VM?" \
+ 15 70; then
+ echo -e "${CM}${GN}Using SSH keys for root login${CL}"
+ qm set "$VMID" --ciuser root --sshkeys "${SSH_PUB_KEYS[0]}" >/dev/null
+ return
+ fi
+ fi
+
+ # No key or user said No → ask for password twice
+ local PASS1 PASS2
+ while true; do
+ PASS1=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "Root Password" \
+ --passwordbox "Enter a password for root user" 10 70 3>&1 1>&2 2>&3) || exit-script
+
+ PASS2=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "Confirm Root Password" \
+ --passwordbox "Re-enter password for confirmation" 10 70 3>&1 1>&2 2>&3) || exit-script
+
+ if [[ "$PASS1" == "$PASS2" && -n "$PASS1" ]]; then
+ echo -e "${CM}${GN}Root password confirmed and set${CL}"
+ qm set "$VMID" --ciuser root --cipassword "$PASS1" >/dev/null
+ break
+ else
+ whiptail --backtitle "Proxmox VE Helper Scripts" \
+ --title "Password Mismatch" \
+ --msgbox "Passwords did not match or were empty. Please try again." 10 70
+ fi
+ done
+}
+
+
+# ---- Cloud Image Download ----------------------------------------------------
+choose_os
+msg_info "Retrieving Cloud Image for $var_os $var_version"
+echo -e ""
+echo -e ""
+curl --retry 30 --retry-delay 3 --retry-connrefused -fSL -o "$(basename "$URL")" "$URL"
+FILE="$(basename "$URL")"
+msg_ok "Downloaded ${BL}${FILE}${CL}"
+
+# Ubuntu RAW → qcow2
+if [[ "$FILE" == *.img ]]; then
+ msg_info "Converting RAW image to qcow2"
+ qemu-img convert -O qcow2 "$FILE" "${FILE%.img}.qcow2"
+ rm -f "$FILE"
+ FILE="${FILE%.img}.qcow2"
+ msg_ok "Converted to ${BL}${FILE}${CL}"
+fi
+
+# ---- Codename & Docker-Repo (einmalig) ---------------------------------------
+detect_codename_and_repo() {
+ if [[ "$URL" == *"/bookworm/"* || "$FILE" == *"debian-12-"* ]]; then
+ CODENAME="bookworm"; DOCKER_BASE="https://download.docker.com/linux/debian"
+ elif [[ "$URL" == *"/trixie/"* || "$FILE" == *"debian-13-"* ]]; then
+ CODENAME="trixie"; DOCKER_BASE="https://download.docker.com/linux/debian"
+ elif [[ "$URL" == *"/noble/"* || "$FILE" == *"noble-"* ]]; then
+ CODENAME="noble"; DOCKER_BASE="https://download.docker.com/linux/ubuntu"
+ else
+ CODENAME="bookworm"; DOCKER_BASE="https://download.docker.com/linux/debian"
+ fi
+ REPO_CODENAME="$CODENAME"
+ if [[ "$DOCKER_BASE" == *"linux/debian"* && "$CODENAME" == "trixie" ]]; then
+ REPO_CODENAME="bookworm"
+ fi
+}
+detect_codename_and_repo
+
+get_snippet_dir() {
+ local store="$1"
+ awk -v s="$store" '
+ $1 == "dir:" && $2 == s {getline; print $2 "/snippets"}
+ ' /etc/pve/storage.cfg
+}
+
+# ---- PVE8: direct inject via virt-customize ----------------------------------
+if [[ "$INSTALL_MODE" = "direct" ]]; then
+ msg_info "Injecting Docker & QGA into image (${CODENAME}, repo: $(basename "$DOCKER_BASE"))"
+ export LIBGUESTFS_BACKEND=direct
+ if ! command -v virt-customize >/dev/null 2>&1; then
+ apt-get -qq update >/dev/null
+ apt-get -qq install -y libguestfs-tools >/dev/null
+ fi
+ vrun() { virt-customize -q -a "${FILE}" "$@" >/dev/null; }
+ vrun \
+ --install qemu-guest-agent,ca-certificates,curl,gnupg,lsb-release,apt-transport-https \
+ --run-command "install -m 0755 -d /etc/apt/keyrings" \
+ --run-command "curl -fsSL ${DOCKER_BASE}/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg" \
+ --run-command "chmod a+r /etc/apt/keyrings/docker.gpg" \
+ --run-command "echo 'deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] ${DOCKER_BASE} ${REPO_CODENAME} stable' > /etc/apt/sources.list.d/docker.list" \
+ --run-command "apt-get update -qq" \
+ --run-command "apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin" \
+ --run-command "systemctl enable docker qemu-guest-agent" \
+ --run-command "sed -i 's#^ENV_SUPATH.*#ENV_SUPATH PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#' /etc/login.defs || true" \
+ --run-command "sed -i 's#^ENV_PATH.*#ENV_PATH PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#' /etc/login.defs || true" \
+ --run-command "printf 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n' >/etc/environment" \
+ --run-command "grep -q 'export PATH=' /root/.bashrc || echo 'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' >> /root/.bashrc"
+ msg_ok "Docker & QGA injected"
+fi
+
+# ---- PVE9: Cloud-Init Snippet (NoCloud) --------------------------------------
+if [[ "$INSTALL_MODE" = "cloudinit" ]]; then
+ msg_info "Preparing Cloud-Init user-data for Docker (${CODENAME})"
+
+ # Use SNIPPET_STORE selected earlier
+ SNIPPET_DIR="$(get_snippet_dir "$SNIPPET_STORE")"
+ mkdir -p "$SNIPPET_DIR"
+
+ SNIPPET_FILE="docker-${VMID}-user-data.yaml"
+ SNIPPET_PATH="${SNIPPET_DIR}/${SNIPPET_FILE}"
+
+ DOCKER_GPG_B64="$(curl -fsSL "${DOCKER_BASE}/gpg" | gpg --dearmor | base64 -w0)"
+
+cat >"$SNIPPET_PATH" < /etc/apt/sources.list.d/docker.list
+ - apt-get update -qq
+ - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
+ - systemctl enable --now qemu-guest-agent
+ - systemctl enable --now docker
+
+growpart:
+ mode: auto
+ devices: ['/']
+ ignore_growroot_disabled: false
+
+fs_resize: true
+
+power_state:
+ mode: reboot
+ condition: true
+EOYAML
+
+ chmod 0644 "$SNIPPET_PATH"
+ msg_ok "Cloud-Init user-data written: ${SNIPPET_PATH}"
+fi
+
+# ---- VM erstellen (q35) ------------------------------------------------------
+msg_info "Creating a Docker VM shell"
+qm create "$VMID" -machine q35 -bios ovmf -agent 1 -tablet 0 -localtime 1 ${CPU_TYPE} \
+ -cores "$CORE_COUNT" -memory "$RAM_SIZE" -name "$HN" -tags community-script \
+ -net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 -scsihw virtio-scsi-pci >/dev/null
+msg_ok "Created VM shell"
+
+msg_info "Configuring authentication"
+configure_authentication
+msg_ok "Authentication configured"
+
+# ---- Disk importieren --------------------------------------------------------
+msg_info "Importing disk into storage ($STORAGE)"
+if qm disk import --help >/dev/null 2>&1; then IMPORT_CMD=(qm disk import); else IMPORT_CMD=(qm importdisk); fi
+IMPORT_OUT="$("${IMPORT_CMD[@]}" "$VMID" "${FILE}" "$STORAGE" --format qcow2 2>&1 || true)"
+DISK_REF="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p" | tr -d "\r\"'")"
+[[ -z "$DISK_REF" ]] && DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$5 ~ ("vm-"id"-disk-") {print $1":"$5}' | sort | tail -n1)"
+[[ -z "$DISK_REF" ]] && { msg_error "Unable to determine imported disk reference."; echo "$IMPORT_OUT"; exit 1; }
+msg_ok "Imported disk (${BL}${DISK_REF}${CL})"
+
+SSHKEYS_ARG=""
+if [[ -s /root/.ssh/authorized_keys ]]; then
+ SSHKEYS_ARG="--sshkeys /root/.ssh/authorized_keys"
+fi
+
+# ---- EFI + Root + Cloud-Init anhängen ---------------------------------------
+msg_info "Attaching EFI/root disk and Cloud-Init (Patience)"
+qm set "$VMID" \
+ --efidisk0 "${STORAGE}:0${FORMAT}" \
+ --scsi0 "${DISK_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE}" \
+ --boot order=scsi0 \
+ --serial0 socket \
+ --agent enabled=1,fstrim_cloned_disks=1 \
+ --ide2 "${STORAGE}:cloudinit" \
+ --ipconfig0 "ip=dhcp" >/dev/null
+
+if [[ "$INSTALL_MODE" = "cloudinit" ]]; then
+ qm set "$VMID" --cicustom "user=${SNIPPET_STORE}:snippets/${SNIPPET_FILE}" >/dev/null
+fi
+msg_ok "Attached EFI/root and Cloud-Init"
+
+# ---- Disk auf Zielgröße im PVE-Layer (Cloud-Init wächst FS) ------------------
+msg_info "Resizing disk to $DISK_SIZE (PVE layer)"
+qm resize "$VMID" scsi0 "${DISK_SIZE}" >/dev/null || true
+msg_ok "Resized disk"
+
+# ---- Beschreibung ------------------------------------------------------------
+DESCRIPTION=$(
+ cat <<'EOF'
+
+EOF
+)
+qm set "$VMID" -description "$DESCRIPTION" >/dev/null
+msg_ok "Created a Docker VM ${BL}(${HN})${CL}"
+
+# ---- Start -------------------------------------------------------------------
+if [[ "$START_VM" == "yes" ]]; then
+ msg_info "Starting Docker VM"
+ qm start "$VMID"
+ msg_ok "Started Docker VM"
+fi
+
+post_update_to_api "done" "none"
+msg_ok "Completed Successfully!\n"
+
+# ---- Hinweise/Debug (Cloud-Init) --------------------------------------------
+# In der VM prüfen:
+# journalctl -u cloud-init -b
+# cat /var/log/cloud-init.log
+# cat /var/log/cloud-init-output.log
+# cloud-init status --long
diff --git a/vm/nextcloud-vm.sh b/vm/nextcloud-vm.sh
index d67102593..083534c3f 100644
--- a/vm/nextcloud-vm.sh
+++ b/vm/nextcloud-vm.sh
@@ -8,12 +8,12 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
______ __ __ _ __ __ __ __ _ ____ ___
/_ __/_ _________ / //_/__ __ __ / |/ /____ __/ /_____/ /__ __ _____/ / | | / / |/ /
- / / / // / __/ _ \/ ,< / -_) // / / / -_) \ / __/ __/ / _ \/ // / _ / | |/ / /|_/ /
-/_/ \_,_/_/ /_//_/_/|_|\__/\_, / /_/|_/\__/_\_\\__/\__/_/\___/\_,_/\_,_/ |___/_/ /_/
+ / / / // / __/ _ \/ ,< / -_) // / / / -_) \ / __/ __/ / _ \/ // / _ / | |/ / /|_/ /
+/_/ \_,_/_/ /_//_/_/|_|\__/\_, / /_/|_/\__/_\_\\__/\__/_/\___/\_,_/\_,_/ |___/_/ /_/
/___/
EOF
}
@@ -46,322 +46,322 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "${command}"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "${command}"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "$NAME" --yesno "This will create a New $NAME. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "⚠ User exited script \n" && exit
+ header_info && echo -e "⚠ User exited script \n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne " ${HOLD} ${YW}${msg}..."
+ local msg="$1"
+ echo -ne " ${HOLD} ${YW}${msg}..."
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- msg_error "This script will not work with PiMox! \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ msg_error "This script will not work with PiMox! \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "⚠ User exited script \n"
- exit
+ clear
+ echo -e "⚠ User exited script \n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=",efitype=4m"
- MACHINE=""
- DISK_CACHE=""
- HN="turnkey-nextcloud-vm"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="2048"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="no"
- METHOD="default"
- echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${DGN}Using Machine Type: ${BGN}i440fx${CL}"
- echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
- echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
- echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
- echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${DGN}Using Bridge: ${BGN}${BRG}${CL}"
- echo -e "${DGN}Using MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${DGN}Using VLAN: ${BGN}Default${CL}"
- echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
- echo -e "${BL}Creating a $NAME using the above default settings${CL}"
+ VMID=$(get_valid_nextid)
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ DISK_CACHE=""
+ HN="turnkey-nextcloud-vm"
+ CPU_TYPE=""
+ CORE_COUNT="2"
+ RAM_SIZE="2048"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="no"
+ METHOD="default"
+ echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${DGN}Using Machine Type: ${BGN}i440fx${CL}"
+ echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
+ echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
+ echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
+ echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${DGN}Using Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${DGN}Using MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${DGN}Using VLAN: ${BGN}Default${CL}"
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
+ echo -e "${BL}Creating a $NAME using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit-script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 turnkey-nextcloud-vm --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="$HN"
+ echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
+ fi
else
- echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit-script
fi
- else
- exit-script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 turnkey-nextcloud-vm --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="$HN"
- echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $CPU_TYPE1 = "1" ]; then
- echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
else
- echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
+ exit-script
fi
- else
- exit-script
- fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ fi
else
- echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="2048"
- echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
+ fi
else
- echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
else
- MTU=",mtu=$MTU1"
- echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
fi
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
-
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a $NAME?" --no-button Do-Over 10 58); then
- echo -e "${RD}Creating a $NAME using the above advanced settings${CL}"
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a $NAME?" --no-button Do-Over 10 58); then
+ echo -e "${RD}Creating a $NAME using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
@@ -374,29 +374,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -412,38 +412,38 @@ msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1,2}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Creating a $NAME"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios seabios${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
- -name $HN -tags community-script -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
pvesm alloc $STORAGE $VMID $DISK1 12G 1>&/dev/null
qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null
qm set $VMID \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN} \
- -scsi1 ${DISK2_REF},${DISK_CACHE}${THIN} \
- -boot order='scsi1;scsi0' >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN} \
+ -scsi1 ${DISK2_REF},${DISK_CACHE}${THIN} \
+ -boot order='scsi1;scsi0' >/dev/null
DESCRIPTION=$(
- cat <
@@ -456,7 +456,7 @@ DESCRIPTION=$(
-
+
GitHub
@@ -476,9 +476,9 @@ qm set "$VMID" -description "$DESCRIPTION" >/dev/null
msg_ok "Created a $NAME ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting $NAME"
- qm start $VMID
- msg_ok "Started $NAME"
+ msg_info "Starting $NAME"
+ qm start $VMID
+ msg_ok "Started $NAME"
fi
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"
diff --git a/vm/openwrt.sh b/vm/openwrt.sh
index 86f6e4e4f..d5c8a0f38 100644
--- a/vm/openwrt.sh
+++ b/vm/openwrt.sh
@@ -10,8 +10,8 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
____ _ __ __
/ __ \____ ___ ____| | / /____/ /_
/ / / / __ \/ _ \/ __ \ | /| / / ___/ __/
@@ -50,380 +50,380 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "$command"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "$command"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
function send_line_to_vm() {
- echo -e "${DGN}Sending line: ${YW}$1${CL}"
- for ((i = 0; i < ${#1}; i++)); do
- character=${1:i:1}
- case $character in
- " ") character="spc" ;;
- "-") character="minus" ;;
- "=") character="equal" ;;
- ",") character="comma" ;;
- ".") character="dot" ;;
- "/") character="slash" ;;
- "'") character="apostrophe" ;;
- ";") character="semicolon" ;;
- '\') character="backslash" ;;
- '`') character="grave_accent" ;;
- "[") character="bracket_left" ;;
- "]") character="bracket_right" ;;
- "_") character="shift-minus" ;;
- "+") character="shift-equal" ;;
- "?") character="shift-slash" ;;
- "<") character="shift-comma" ;;
- ">") character="shift-dot" ;;
- '"') character="shift-apostrophe" ;;
- ":") character="shift-semicolon" ;;
- "|") character="shift-backslash" ;;
- "~") character="shift-grave_accent" ;;
- "{") character="shift-bracket_left" ;;
- "}") character="shift-bracket_right" ;;
- "A") character="shift-a" ;;
- "B") character="shift-b" ;;
- "C") character="shift-c" ;;
- "D") character="shift-d" ;;
- "E") character="shift-e" ;;
- "F") character="shift-f" ;;
- "G") character="shift-g" ;;
- "H") character="shift-h" ;;
- "I") character="shift-i" ;;
- "J") character="shift-j" ;;
- "K") character="shift-k" ;;
- "L") character="shift-l" ;;
- "M") character="shift-m" ;;
- "N") character="shift-n" ;;
- "O") character="shift-o" ;;
- "P") character="shift-p" ;;
- "Q") character="shift-q" ;;
- "R") character="shift-r" ;;
- "S") character="shift-s" ;;
- "T") character="shift-t" ;;
- "U") character="shift-u" ;;
- "V") character="shift-v" ;;
- "W") character="shift-w" ;;
- "X") character="shift=x" ;;
- "Y") character="shift-y" ;;
- "Z") character="shift-z" ;;
- "!") character="shift-1" ;;
- "@") character="shift-2" ;;
- "#") character="shift-3" ;;
- '$') character="shift-4" ;;
- "%") character="shift-5" ;;
- "^") character="shift-6" ;;
- "&") character="shift-7" ;;
- "*") character="shift-8" ;;
- "(") character="shift-9" ;;
- ")") character="shift-0" ;;
- esac
- qm sendkey $VMID "$character"
- done
- qm sendkey $VMID ret
+ echo -e "${DGN}Sending line: ${YW}$1${CL}"
+ for ((i = 0; i < ${#1}; i++)); do
+ character=${1:i:1}
+ case $character in
+ " ") character="spc" ;;
+ "-") character="minus" ;;
+ "=") character="equal" ;;
+ ",") character="comma" ;;
+ ".") character="dot" ;;
+ "/") character="slash" ;;
+ "'") character="apostrophe" ;;
+ ";") character="semicolon" ;;
+ '\') character="backslash" ;;
+ '`') character="grave_accent" ;;
+ "[") character="bracket_left" ;;
+ "]") character="bracket_right" ;;
+ "_") character="shift-minus" ;;
+ "+") character="shift-equal" ;;
+ "?") character="shift-slash" ;;
+ "<") character="shift-comma" ;;
+ ">") character="shift-dot" ;;
+ '"') character="shift-apostrophe" ;;
+ ":") character="shift-semicolon" ;;
+ "|") character="shift-backslash" ;;
+ "~") character="shift-grave_accent" ;;
+ "{") character="shift-bracket_left" ;;
+ "}") character="shift-bracket_right" ;;
+ "A") character="shift-a" ;;
+ "B") character="shift-b" ;;
+ "C") character="shift-c" ;;
+ "D") character="shift-d" ;;
+ "E") character="shift-e" ;;
+ "F") character="shift-f" ;;
+ "G") character="shift-g" ;;
+ "H") character="shift-h" ;;
+ "I") character="shift-i" ;;
+ "J") character="shift-j" ;;
+ "K") character="shift-k" ;;
+ "L") character="shift-l" ;;
+ "M") character="shift-m" ;;
+ "N") character="shift-n" ;;
+ "O") character="shift-o" ;;
+ "P") character="shift-p" ;;
+ "Q") character="shift-q" ;;
+ "R") character="shift-r" ;;
+ "S") character="shift-s" ;;
+ "T") character="shift-t" ;;
+ "U") character="shift-u" ;;
+ "V") character="shift-v" ;;
+ "W") character="shift-w" ;;
+ "X") character="shift=x" ;;
+ "Y") character="shift-y" ;;
+ "Z") character="shift-z" ;;
+ "!") character="shift-1" ;;
+ "@") character="shift-2" ;;
+ "#") character="shift-3" ;;
+ '$') character="shift-4" ;;
+ "%") character="shift-5" ;;
+ "^") character="shift-6" ;;
+ "&") character="shift-7" ;;
+ "*") character="shift-8" ;;
+ "(") character="shift-9" ;;
+ ")") character="shift-0" ;;
+ esac
+ qm sendkey $VMID "$character"
+ done
+ qm sendkey $VMID ret
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "OpenWrt VM" --yesno "This will create a New OpenWrt VM. Proceed?" 10 58); then
- :
+ :
else
- header_info && echo -e "⚠ User exited script \n" && exit
+ header_info && echo -e "⚠ User exited script \n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne " ${HOLD} ${YW}${msg}..."
+ local msg="$1"
+ echo -ne " ${HOLD} ${YW}${msg}..."
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${CROSS} This script will not work with PiMox! \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${CROSS} This script will not work with PiMox! \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "⚠ User exited script \n"
- exit
+ clear
+ echo -e "⚠ User exited script \n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- HN=openwrt
- CORE_COUNT="1"
- RAM_SIZE="256"
- BRG="vmbr0"
- VLAN=""
- MAC=$GEN_MAC
- LAN_MAC=$GEN_MAC_LAN
- LAN_BRG="vmbr0"
- LAN_IP_ADDR="192.168.1.1"
- LAN_NETMASK="255.255.255.0"
- LAN_VLAN=",tag=999"
- MTU=""
- START_VM="yes"
- METHOD="default"
- echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
- echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${DGN}Using WAN Bridge: ${BGN}${BRG}${CL}"
- echo -e "${DGN}Using WAN VLAN: ${BGN}Default${CL}"
- echo -e "${DGN}Using WAN MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${DGN}Using LAN MAC Address: ${BGN}${LAN_MAC}${CL}"
- echo -e "${DGN}Using LAN Bridge: ${BGN}${LAN_BRG}${CL}"
- echo -e "${DGN}Using LAN VLAN: ${BGN}999${CL}"
- echo -e "${DGN}Using LAN IP Address: ${BGN}${LAN_IP_ADDR}${CL}"
- echo -e "${DGN}Using LAN NETMASK: ${BGN}${LAN_NETMASK}${CL}"
- echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${BL}Creating a OpenWrt VM using the above default settings${CL}"
+ VMID=$(get_valid_nextid)
+ HN=openwrt
+ CORE_COUNT="1"
+ RAM_SIZE="256"
+ BRG="vmbr0"
+ VLAN=""
+ MAC=$GEN_MAC
+ LAN_MAC=$GEN_MAC_LAN
+ LAN_BRG="vmbr0"
+ LAN_IP_ADDR="192.168.1.1"
+ LAN_NETMASK="255.255.255.0"
+ LAN_VLAN=",tag=999"
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+ echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
+ echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${DGN}Using WAN Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${DGN}Using WAN VLAN: ${BGN}Default${CL}"
+ echo -e "${DGN}Using WAN MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${DGN}Using LAN MAC Address: ${BGN}${LAN_MAC}${CL}"
+ echo -e "${DGN}Using LAN Bridge: ${BGN}${LAN_BRG}${CL}"
+ echo -e "${DGN}Using LAN VLAN: ${BGN}999${CL}"
+ echo -e "${DGN}Using LAN IP Address: ${BGN}${LAN_IP_ADDR}${CL}"
+ echo -e "${DGN}Using LAN NETMASK: ${BGN}${LAN_NETMASK}${CL}"
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${BL}Creating a OpenWrt VM using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 openwrt --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="openwrt"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ fi
+ echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
else
- exit-script
+ exit-script
fi
- done
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 openwrt --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="openwrt"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 1 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="1"
+ fi
+ echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ exit-script
fi
- echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
- else
- exit-script
- fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 1 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="1"
- fi
- echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
- else
- exit-script
- fi
-
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 256 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="256"
- fi
- echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
- else
- exit-script
- fi
-
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Bridge" 8 58 vmbr0 --title "WAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- fi
- echo -e "${DGN}Using WAN Bridge: ${BGN}$BRG${CL}"
- else
- exit-script
- fi
-
- if LAN_BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Bridge" 8 58 vmbr0 --title "LAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $LAN_BRG ]; then
- LAN_BRG="vmbr0"
- fi
- echo -e "${DGN}Using LAN Bridge: ${BGN}$LAN_BRG${CL}"
- else
- exit-script
- fi
-
- if LAN_IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a router IP" 8 58 $LAN_IP_ADDR --title "LAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $LAN_IP_ADDR ]; then
- LAN_IP_ADDR="192.168.1.1"
- fi
- echo -e "${DGN}Using LAN IP ADDRESS: ${BGN}$LAN_IP_ADDR${CL}"
- else
- exit-script
- fi
-
- if LAN_NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a router netmask" 8 58 $LAN_NETMASK --title "LAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $LAN_NETMASK ]; then
- LAN_NETMASK="255.255.255.0"
- fi
- echo -e "${DGN}Using LAN NETMASK: ${BGN}$LAN_NETMASK${CL}"
- else
- exit-script
- fi
-
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN MAC Address" 8 58 $GEN_MAC --title "WAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 256 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="256"
+ fi
+ echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
else
- MAC="$MAC1"
+ exit-script
fi
- echo -e "${DGN}Using WAN MAC Address: ${BGN}$MAC${CL}"
- else
- exit-script
- fi
- if MAC2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN MAC Address" 8 58 $GEN_MAC_LAN --title "LAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC2 ]; then
- LAN_MAC="$GEN_MAC_LAN"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Bridge" 8 58 vmbr0 --title "WAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ fi
+ echo -e "${DGN}Using WAN Bridge: ${BGN}$BRG${CL}"
else
- LAN_MAC="$MAC2"
+ exit-script
fi
- echo -e "${DGN}Using LAN MAC Address: ${BGN}$LAN_MAC${CL}"
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Vlan (leave blank for default)" 8 58 --title "WAN VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
+ if LAN_BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Bridge" 8 58 vmbr0 --title "LAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $LAN_BRG ]; then
+ LAN_BRG="vmbr0"
+ fi
+ echo -e "${DGN}Using LAN Bridge: ${BGN}$LAN_BRG${CL}"
else
- VLAN=",tag=$VLAN1"
+ exit-script
fi
- echo -e "${DGN}Using WAN Vlan: ${BGN}$VLAN1${CL}"
- else
- exit-script
- fi
- if VLAN2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Vlan" 8 58 999 --title "LAN VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN2 ]; then
- VLAN2="999"
- LAN_VLAN=",tag=$VLAN2"
+ if LAN_IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a router IP" 8 58 $LAN_IP_ADDR --title "LAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $LAN_IP_ADDR ]; then
+ LAN_IP_ADDR="192.168.1.1"
+ fi
+ echo -e "${DGN}Using LAN IP ADDRESS: ${BGN}$LAN_IP_ADDR${CL}"
else
- LAN_VLAN=",tag=$VLAN2"
+ exit-script
fi
- echo -e "${DGN}Using LAN Vlan: ${BGN}$VLAN2${CL}"
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
+ if LAN_NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a router netmask" 8 58 $LAN_NETMASK --title "LAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $LAN_NETMASK ]; then
+ LAN_NETMASK="255.255.255.0"
+ fi
+ echo -e "${DGN}Using LAN NETMASK: ${BGN}$LAN_NETMASK${CL}"
else
- MTU=",mtu=$MTU1"
+ exit-script
fi
- echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- START_VM="yes"
- else
- START_VM="no"
- fi
- echo -e "${DGN}Start VM when completed: ${BGN}$START_VM${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN MAC Address" 8 58 $GEN_MAC --title "WAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ else
+ MAC="$MAC1"
+ fi
+ echo -e "${DGN}Using WAN MAC Address: ${BGN}$MAC${CL}"
+ else
+ exit-script
+ fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create OpenWrt VM?" --no-button Do-Over 10 58); then
- echo -e "${RD}Creating a OpenWrt VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if MAC2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN MAC Address" 8 58 $GEN_MAC_LAN --title "LAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC2 ]; then
+ LAN_MAC="$GEN_MAC_LAN"
+ else
+ LAN_MAC="$MAC2"
+ fi
+ echo -e "${DGN}Using LAN MAC Address: ${BGN}$LAN_MAC${CL}"
+ else
+ exit-script
+ fi
+
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Vlan (leave blank for default)" 8 58 --title "WAN VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ else
+ VLAN=",tag=$VLAN1"
+ fi
+ echo -e "${DGN}Using WAN Vlan: ${BGN}$VLAN1${CL}"
+ else
+ exit-script
+ fi
+
+ if VLAN2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Vlan" 8 58 999 --title "LAN VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN2 ]; then
+ VLAN2="999"
+ LAN_VLAN=",tag=$VLAN2"
+ else
+ LAN_VLAN=",tag=$VLAN2"
+ fi
+ echo -e "${DGN}Using LAN Vlan: ${BGN}$VLAN2${CL}"
+ else
+ exit-script
+ fi
+
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ else
+ MTU=",mtu=$MTU1"
+ fi
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ exit-script
+ fi
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ START_VM="yes"
+ else
+ START_VM="no"
+ fi
+ echo -e "${DGN}Start VM when completed: ${BGN}$START_VM${CL}"
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create OpenWrt VM?" --no-button Do-Over 10 58); then
+ echo -e "${RD}Creating a OpenWrt VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
arch_check
@@ -434,30 +434,30 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- echo -e "\n${RD}⚠ Unable to detect a valid storage location.${CL}"
- echo -e "Exiting..."
- exit
+ echo -e "\n${RD}⚠ Unable to detect a valid storage location.${CL}"
+ echo -e "Exiting..."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 OpenWrt VM?\n\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 OpenWrt VM?\n\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -482,35 +482,35 @@ msg_ok "Extracted & Resized OpenWrt Disk Image ${CL}${BL}$FILE${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir)
- DISK_EXT=".qcow2"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format qcow2"
- ;;
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ ;;
esac
for i in {0,1}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Creating OpenWrt VM"
qm create $VMID -cores $CORE_COUNT -memory $RAM_SIZE -name $HN \
- -onboot 1 -ostype l26 -scsihw virtio-scsi-pci --tablet 0
+ -onboot 1 -ostype l26 -scsihw virtio-scsi-pci --tablet 0
pvesm alloc $STORAGE $VMID $DISK0 4M 1>&/dev/null
qm importdisk $VMID ${FILE%.*} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null
qm set $VMID \
- -efidisk0 ${DISK0_REF},efitype=4m,size=4M \
- -scsi0 ${DISK1_REF},size=512M \
- -boot order=scsi0 \
- -tags community-script >/dev/null
+ -efidisk0 ${DISK0_REF},efitype=4m,size=4M \
+ -scsi0 ${DISK1_REF},size=512M \
+ -boot order=scsi0 \
+ -tags community-script >/dev/null
DESCRIPTION=$(
- cat <
@@ -523,7 +523,7 @@ DESCRIPTION=$(
-
+
GitHub
@@ -561,21 +561,21 @@ send_line_to_vm "uci commit"
send_line_to_vm "halt"
msg_ok "Network interfaces have been successfully configured."
until qm status $VMID | grep -q "stopped"; do
- sleep 2
+ sleep 2
done
msg_info "Bridge interfaces are being added."
qm set $VMID \
- -net0 virtio,bridge=${LAN_BRG},macaddr=${LAN_MAC}${LAN_VLAN}${MTU} \
- -net1 virtio,bridge=${BRG},macaddr=${MAC}${VLAN}${MTU} >/dev/null 2>/dev/null
+ -net0 virtio,bridge=${LAN_BRG},macaddr=${LAN_MAC}${LAN_VLAN}${MTU} \
+ -net1 virtio,bridge=${BRG},macaddr=${MAC}${VLAN}${MTU} >/dev/null 2>/dev/null
msg_ok "Bridge interfaces have been successfully added."
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting OpenWrt VM"
- qm start $VMID
- msg_ok "Started OpenWrt VM"
+ msg_info "Starting OpenWrt VM"
+ qm start $VMID
+ msg_ok "Started OpenWrt VM"
fi
VLAN_FINISH=""
if [ "$VLAN" == "" ] && [ "$VLAN2" != "999" ]; then
- VLAN_FINISH=" Please remember to adjust the VLAN tags to suit your network."
+ VLAN_FINISH=" Please remember to adjust the VLAN tags to suit your network."
fi
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n${VLAN_FINISH}"
diff --git a/vm/opnsense-vm.sh b/vm/opnsense-vm.sh
index ff2c2aac7..156fb913d 100644
--- a/vm/opnsense-vm.sh
+++ b/vm/opnsense-vm.sh
@@ -7,14 +7,14 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
- ____ ____ _ __
- / __ \/ __ \/ | / /_______ ____ ________
+ clear
+ cat <<"EOF"
+ ____ ____ _ __
+ / __ \/ __ \/ | / /_______ ____ ________
/ / / / /_/ / |/ / ___/ _ \/ __ \/ ___/ _ \
/ /_/ / ____/ /| (__ ) __/ / / (__ ) __/
-\____/_/ /_/ |_/____/\___/_/ /_/____/\___/
-
+\____/_/ /_/ |_/____/\___/_/ /_/____/\___/
+
EOF
}
header_info
@@ -45,466 +45,466 @@ set -Eeo pipefail
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
trap cleanup EXIT
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "$command"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "$command"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- post_update_to_api "done" "none"
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ post_update_to_api "done" "none"
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
function send_line_to_vm() {
- echo -e "${DGN}Sending line: ${YW}$1${CL}"
- for ((i = 0; i < ${#1}; i++)); do
- character=${1:i:1}
- case $character in
- " ") character="spc" ;;
- "-") character="minus" ;;
- "=") character="equal" ;;
- ",") character="comma" ;;
- ".") character="dot" ;;
- "/") character="slash" ;;
- "'") character="apostrophe" ;;
- ";") character="semicolon" ;;
- '\') character="backslash" ;;
- '`') character="grave_accent" ;;
- "[") character="bracket_left" ;;
- "]") character="bracket_right" ;;
- "_") character="shift-minus" ;;
- "+") character="shift-equal" ;;
- "?") character="shift-slash" ;;
- "<") character="shift-comma" ;;
- ">") character="shift-dot" ;;
- '"') character="shift-apostrophe" ;;
- ":") character="shift-semicolon" ;;
- "|") character="shift-backslash" ;;
- "~") character="shift-grave_accent" ;;
- "{") character="shift-bracket_left" ;;
- "}") character="shift-bracket_right" ;;
- "A") character="shift-a" ;;
- "B") character="shift-b" ;;
- "C") character="shift-c" ;;
- "D") character="shift-d" ;;
- "E") character="shift-e" ;;
- "F") character="shift-f" ;;
- "G") character="shift-g" ;;
- "H") character="shift-h" ;;
- "I") character="shift-i" ;;
- "J") character="shift-j" ;;
- "K") character="shift-k" ;;
- "L") character="shift-l" ;;
- "M") character="shift-m" ;;
- "N") character="shift-n" ;;
- "O") character="shift-o" ;;
- "P") character="shift-p" ;;
- "Q") character="shift-q" ;;
- "R") character="shift-r" ;;
- "S") character="shift-s" ;;
- "T") character="shift-t" ;;
- "U") character="shift-u" ;;
- "V") character="shift-v" ;;
- "W") character="shift-w" ;;
- "X") character="shift=x" ;;
- "Y") character="shift-y" ;;
- "Z") character="shift-z" ;;
- "!") character="shift-1" ;;
- "@") character="shift-2" ;;
- "#") character="shift-3" ;;
- '$') character="shift-4" ;;
- "%") character="shift-5" ;;
- "^") character="shift-6" ;;
- "&") character="shift-7" ;;
- "*") character="shift-8" ;;
- "(") character="shift-9" ;;
- ")") character="shift-0" ;;
- esac
- qm sendkey $VMID "$character"
- done
- qm sendkey $VMID ret
+ echo -e "${DGN}Sending line: ${YW}$1${CL}"
+ for ((i = 0; i < ${#1}; i++)); do
+ character=${1:i:1}
+ case $character in
+ " ") character="spc" ;;
+ "-") character="minus" ;;
+ "=") character="equal" ;;
+ ",") character="comma" ;;
+ ".") character="dot" ;;
+ "/") character="slash" ;;
+ "'") character="apostrophe" ;;
+ ";") character="semicolon" ;;
+ '\') character="backslash" ;;
+ '`') character="grave_accent" ;;
+ "[") character="bracket_left" ;;
+ "]") character="bracket_right" ;;
+ "_") character="shift-minus" ;;
+ "+") character="shift-equal" ;;
+ "?") character="shift-slash" ;;
+ "<") character="shift-comma" ;;
+ ">") character="shift-dot" ;;
+ '"') character="shift-apostrophe" ;;
+ ":") character="shift-semicolon" ;;
+ "|") character="shift-backslash" ;;
+ "~") character="shift-grave_accent" ;;
+ "{") character="shift-bracket_left" ;;
+ "}") character="shift-bracket_right" ;;
+ "A") character="shift-a" ;;
+ "B") character="shift-b" ;;
+ "C") character="shift-c" ;;
+ "D") character="shift-d" ;;
+ "E") character="shift-e" ;;
+ "F") character="shift-f" ;;
+ "G") character="shift-g" ;;
+ "H") character="shift-h" ;;
+ "I") character="shift-i" ;;
+ "J") character="shift-j" ;;
+ "K") character="shift-k" ;;
+ "L") character="shift-l" ;;
+ "M") character="shift-m" ;;
+ "N") character="shift-n" ;;
+ "O") character="shift-o" ;;
+ "P") character="shift-p" ;;
+ "Q") character="shift-q" ;;
+ "R") character="shift-r" ;;
+ "S") character="shift-s" ;;
+ "T") character="shift-t" ;;
+ "U") character="shift-u" ;;
+ "V") character="shift-v" ;;
+ "W") character="shift-w" ;;
+ "X") character="shift=x" ;;
+ "Y") character="shift-y" ;;
+ "Z") character="shift-z" ;;
+ "!") character="shift-1" ;;
+ "@") character="shift-2" ;;
+ "#") character="shift-3" ;;
+ '$') character="shift-4" ;;
+ "%") character="shift-5" ;;
+ "^") character="shift-6" ;;
+ "&") character="shift-7" ;;
+ "*") character="shift-8" ;;
+ "(") character="shift-9" ;;
+ ")") character="shift-0" ;;
+ esac
+ qm sendkey $VMID "$character"
+ done
+ qm sendkey $VMID ret
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "OPNsense VM" --yesno "This will create a New OPNsense VM. Proceed?" 10 58); then
- :
+ :
else
- header_info && echo -e "⚠ User exited script \n" && exit
+ header_info && echo -e "⚠ User exited script \n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne " ${HOLD} ${YW}${msg}..."
+ local msg="$1"
+ echo -ne " ${HOLD} ${YW}${msg}..."
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${CROSS} This script will not work with PiMox! \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${CROSS} This script will not work with PiMox! \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "⚠ User exited script \n"
- exit
+ clear
+ echo -e "⚠ User exited script \n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=",efitype=4m"
- MACHINE=""
- DISK_CACHE=""
- HN="opnsense"
- CPU_TYPE=""
- CORE_COUNT="4"
- RAM_SIZE="8192"
- BRG="vmbr0"
- IP_ADDR=""
- WAN_IP_ADDR=""
- LAN_GW=""
- WAN_GW=""
- NETMASK=""
- WAN_NETMASK=""
- VLAN=""
- MAC=$GEN_MAC
- WAN_MAC=$GEN_MAC_LAN
- WAN_BRG="vmbr1"
- MTU=""
- START_VM="yes"
- METHOD="default"
+ VMID=$(get_valid_nextid)
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ DISK_CACHE=""
+ HN="opnsense"
+ CPU_TYPE=""
+ CORE_COUNT="4"
+ RAM_SIZE="8192"
+ BRG="vmbr0"
+ IP_ADDR=""
+ WAN_IP_ADDR=""
+ LAN_GW=""
+ WAN_GW=""
+ NETMASK=""
+ WAN_NETMASK=""
+ VLAN=""
+ MAC=$GEN_MAC
+ WAN_MAC=$GEN_MAC_LAN
+ WAN_BRG="vmbr1"
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
- echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
- echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
- if ! grep -q "^iface ${BRG}" /etc/network/interfaces; then
- msg_error "Bridge '${BRG}' does not exist in /etc/network/interfaces"
- exit
- else
- echo -e "${DGN}Using LAN Bridge: ${BGN}${BRG}${CL}"
- fi
- echo -e "${DGN}Using LAN VLAN: ${BGN}Default${CL}"
- echo -e "${DGN}Using LAN MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${DGN}Using WAN MAC Address: ${BGN}${WAN_MAC}${CL}"
- if ! grep -q "^iface ${WAN_BRG}" /etc/network/interfaces; then
- msg_error "Bridge '${WAN_BRG}' does not exist in /etc/network/interfaces"
- exit
- else
- echo -e "${DGN}Using WAN Bridge: ${BGN}${WAN_BRG}${CL}"
- fi
- echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${BL}Creating a OPNsense VM using the above default settings${CL}"
+ echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
+ echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
+ if ! grep -q "^iface ${BRG}" /etc/network/interfaces; then
+ msg_error "Bridge '${BRG}' does not exist in /etc/network/interfaces"
+ exit
+ else
+ echo -e "${DGN}Using LAN Bridge: ${BGN}${BRG}${CL}"
+ fi
+ echo -e "${DGN}Using LAN VLAN: ${BGN}Default${CL}"
+ echo -e "${DGN}Using LAN MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${DGN}Using WAN MAC Address: ${BGN}${WAN_MAC}${CL}"
+ if ! grep -q "^iface ${WAN_BRG}" /etc/network/interfaces; then
+ msg_error "Bridge '${WAN_BRG}' does not exist in /etc/network/interfaces"
+ exit
+ else
+ echo -e "${DGN}Using WAN Bridge: ${BGN}${WAN_BRG}${CL}"
+ fi
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${BL}Creating a OPNsense VM using the above default settings${CL}"
}
function advanced_settings() {
- local ip_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$'
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
- else
- exit-script
- fi
- done
-
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
- else
- echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
- fi
- else
- exit-script
- fi
-
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $CPU_TYPE1 = "1" ]; then
- echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
- else
- echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
- fi
- else
- exit-script
- fi
-
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
- else
- echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
- fi
- else
- exit-script
- fi
-
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 OPNsense --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="OPNsense"
- else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- fi
- echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
- else
- exit-script
- fi
-
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 4 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- fi
- echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
- else
- exit-script
- fi
-
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 8192 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="8192"
- fi
- echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
- else
- exit-script
- fi
-
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Bridge" 8 58 vmbr0 --title "LAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- fi
- if ! grep -q "^iface ${BRG}" /etc/network/interfaces; then
- msg_error "Bridge '${BRG}' does not exist in /etc/network/interfaces"
- exit
- fi
- echo -e "${DGN}Using LAN Bridge: ${BGN}$BRG${CL}"
- else
- exit-script
- fi
-
- if IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN IP" 8 58 $IP_ADDR --title "LAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $IP_ADDR ]; then
- echo -e "${DGN}Using DHCP AS LAN IP ADDRESS${CL}"
- else
- if [[ -n "$IP_ADDR" && ! "$IP_ADDR" =~ $ip_regex ]]; then
- msg_error "Invalid IP Address format for LAN IP. Needs to be 0.0.0.0, was $IP_ADDR"
- exit
- fi
- echo -e "${DGN}Using LAN IP ADDRESS: ${BGN}$IP_ADDR${CL}"
- if LAN_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN GATEWAY IP" 8 58 $LAN_GW --title "LAN GATEWAY IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $LAN_GW ]; then
- echo -e "${DGN}Gateway needs to be set if ip is not dhcp${CL}"
- exit-script
+ local ip_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$'
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
fi
- if [[ -n "$LAN_GW" && ! "$LAN_GW" =~ $ip_regex ]]; then
- msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was $LAN_GW"
- exit
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
fi
- echo -e "${DGN}Using LAN GATEWAY ADDRESS: ${BGN}$LAN_GW${CL}"
- fi
- if NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN netmmask (24 for example)" 8 58 $NETMASK --title "LAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $NETMASK ]; then
- echo -e "${DGN}Netmask needs to be set if ip is not dhcp${CL}"
- fi
- if [[ -n "$NETMASK" && ! ("$NETMASK" =~ ^[0-9]+$ && "$NETMASK" -ge 1 && "$NETMASK" -le 32) ]]; then
- msg_error "Invalid LAN NETMASK format. Needs to be 1-32, was $NETMASK"
- exit
- fi
- echo -e "${DGN}Using LAN NETMASK: ${BGN}$NETMASK${CL}"
- else
+ else
exit-script
- fi
fi
- else
- exit-script
- fi
- if WAN_BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Bridge" 8 58 vmbr1 --title "WAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $WAN_BRG ]; then
- WAN_BRG="vmbr1"
- fi
- if ! grep -q "^iface ${WAN_BRG}" /etc/network/interfaces; then
- msg_error "WAN Bridge '${WAN_BRG}' does not exist in /etc/network/interfaces"
- exit
- fi
- echo -e "${DGN}Using WAN Bridge: ${BGN}$WAN_BRG${CL}"
- else
- exit-script
- fi
-
- if WAN_IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN IP" 8 58 $WAN_IP_ADDR --title "WAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $WAN_IP_ADDR ]; then
- echo -e "${DGN}Using DHCP AS WAN IP ADDRESS${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
else
- if [[ -n "$WAN_IP_ADDR" && ! "$WAN_IP_ADDR" =~ $ip_regex ]]; then
- msg_error "Invalid IP Address format for WAN IP. Needs to be 0.0.0.0, was $WAN_IP_ADDR"
- exit
- fi
- echo -e "${DGN}Using WAN IP ADDRESS: ${BGN}$WAN_IP_ADDR${CL}"
- if WAN_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN GATEWAY IP" 8 58 $WAN_GW --title "WAN GATEWAY IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $WAN_GW ]; then
- echo -e "${DGN}Gateway needs to be set if ip is not dhcp${CL}"
- exit-script
- fi
- if [[ -n "$WAN_GW" && ! "$WAN_GW" =~ $ip_regex ]]; then
- msg_error "Invalid IP Address format for WAN Gateway. Needs to be 0.0.0.0, was $WAN_GW"
- exit
- fi
- echo -e "${DGN}Using WAN GATEWAY ADDRESS: ${BGN}$WAN_GW${CL}"
- else
exit-script
- fi
- if WAN_NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN netmmask (24 for example)" 8 58 $WAN_NETMASK --title "WAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $WAN_NETMASK ]; then
- echo -e "${DGN}WAN Netmask needs to be set if ip is not dhcp${CL}"
+ fi
+
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
fi
- if [[ -n "$WAN_NETMASK" && ! ("$WAN_NETMASK" =~ ^[0-9]+$ && "$WAN_NETMASK" -ge 1 && "$WAN_NETMASK" -le 32) ]]; then
- msg_error "Invalid WAN NETMASK format. Needs to be 1-32, was $WAN_NETMASK"
- exit
- fi
- echo -e "${DGN}Using WAN NETMASK: ${BGN}$WAN_NETMASK${CL}"
- else
+ else
exit-script
- fi
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN MAC Address" 8 58 $GEN_MAC --title "WAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- else
- MAC="$MAC1"
- fi
- echo -e "${DGN}Using LAN MAC Address: ${BGN}$MAC${CL}"
- else
- exit-script
- fi
- if MAC2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN MAC Address" 8 58 $GEN_MAC_LAN --title "LAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC2 ]; then
- WAN_MAC="$GEN_MAC_LAN"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 OPNsense --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="OPNsense"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ fi
+ echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
else
- WAN_MAC="$MAC2"
+ exit-script
fi
- echo -e "${DGN}Using WAN MAC Address: ${BGN}$WAN_MAC${CL}"
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create OPNsense VM?" --no-button Do-Over 10 58); then
- echo -e "${RD}Creating a OPNsense VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 4 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ fi
+ echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ exit-script
+ fi
+
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 8192 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="8192"
+ fi
+ echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ else
+ exit-script
+ fi
+
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Bridge" 8 58 vmbr0 --title "LAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ fi
+ if ! grep -q "^iface ${BRG}" /etc/network/interfaces; then
+ msg_error "Bridge '${BRG}' does not exist in /etc/network/interfaces"
+ exit
+ fi
+ echo -e "${DGN}Using LAN Bridge: ${BGN}$BRG${CL}"
+ else
+ exit-script
+ fi
+
+ if IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN IP" 8 58 $IP_ADDR --title "LAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $IP_ADDR ]; then
+ echo -e "${DGN}Using DHCP AS LAN IP ADDRESS${CL}"
+ else
+ if [[ -n "$IP_ADDR" && ! "$IP_ADDR" =~ $ip_regex ]]; then
+ msg_error "Invalid IP Address format for LAN IP. Needs to be 0.0.0.0, was $IP_ADDR"
+ exit
+ fi
+ echo -e "${DGN}Using LAN IP ADDRESS: ${BGN}$IP_ADDR${CL}"
+ if LAN_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN GATEWAY IP" 8 58 $LAN_GW --title "LAN GATEWAY IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $LAN_GW ]; then
+ echo -e "${DGN}Gateway needs to be set if ip is not dhcp${CL}"
+ exit-script
+ fi
+ if [[ -n "$LAN_GW" && ! "$LAN_GW" =~ $ip_regex ]]; then
+ msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was $LAN_GW"
+ exit
+ fi
+ echo -e "${DGN}Using LAN GATEWAY ADDRESS: ${BGN}$LAN_GW${CL}"
+ fi
+ if NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN netmmask (24 for example)" 8 58 $NETMASK --title "LAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $NETMASK ]; then
+ echo -e "${DGN}Netmask needs to be set if ip is not dhcp${CL}"
+ fi
+ if [[ -n "$NETMASK" && ! ("$NETMASK" =~ ^[0-9]+$ && "$NETMASK" -ge 1 && "$NETMASK" -le 32) ]]; then
+ msg_error "Invalid LAN NETMASK format. Needs to be 1-32, was $NETMASK"
+ exit
+ fi
+ echo -e "${DGN}Using LAN NETMASK: ${BGN}$NETMASK${CL}"
+ else
+ exit-script
+ fi
+ fi
+ else
+ exit-script
+ fi
+
+ if WAN_BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Bridge" 8 58 vmbr1 --title "WAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $WAN_BRG ]; then
+ WAN_BRG="vmbr1"
+ fi
+ if ! grep -q "^iface ${WAN_BRG}" /etc/network/interfaces; then
+ msg_error "WAN Bridge '${WAN_BRG}' does not exist in /etc/network/interfaces"
+ exit
+ fi
+ echo -e "${DGN}Using WAN Bridge: ${BGN}$WAN_BRG${CL}"
+ else
+ exit-script
+ fi
+
+ if WAN_IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN IP" 8 58 $WAN_IP_ADDR --title "WAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $WAN_IP_ADDR ]; then
+ echo -e "${DGN}Using DHCP AS WAN IP ADDRESS${CL}"
+ else
+ if [[ -n "$WAN_IP_ADDR" && ! "$WAN_IP_ADDR" =~ $ip_regex ]]; then
+ msg_error "Invalid IP Address format for WAN IP. Needs to be 0.0.0.0, was $WAN_IP_ADDR"
+ exit
+ fi
+ echo -e "${DGN}Using WAN IP ADDRESS: ${BGN}$WAN_IP_ADDR${CL}"
+ if WAN_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN GATEWAY IP" 8 58 $WAN_GW --title "WAN GATEWAY IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $WAN_GW ]; then
+ echo -e "${DGN}Gateway needs to be set if ip is not dhcp${CL}"
+ exit-script
+ fi
+ if [[ -n "$WAN_GW" && ! "$WAN_GW" =~ $ip_regex ]]; then
+ msg_error "Invalid IP Address format for WAN Gateway. Needs to be 0.0.0.0, was $WAN_GW"
+ exit
+ fi
+ echo -e "${DGN}Using WAN GATEWAY ADDRESS: ${BGN}$WAN_GW${CL}"
+ else
+ exit-script
+ fi
+ if WAN_NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN netmmask (24 for example)" 8 58 $WAN_NETMASK --title "WAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $WAN_NETMASK ]; then
+ echo -e "${DGN}WAN Netmask needs to be set if ip is not dhcp${CL}"
+ fi
+ if [[ -n "$WAN_NETMASK" && ! ("$WAN_NETMASK" =~ ^[0-9]+$ && "$WAN_NETMASK" -ge 1 && "$WAN_NETMASK" -le 32) ]]; then
+ msg_error "Invalid WAN NETMASK format. Needs to be 1-32, was $WAN_NETMASK"
+ exit
+ fi
+ echo -e "${DGN}Using WAN NETMASK: ${BGN}$WAN_NETMASK${CL}"
+ else
+ exit-script
+ fi
+ fi
+ else
+ exit-script
+ fi
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN MAC Address" 8 58 $GEN_MAC --title "WAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ else
+ MAC="$MAC1"
+ fi
+ echo -e "${DGN}Using LAN MAC Address: ${BGN}$MAC${CL}"
+ else
+ exit-script
+ fi
+
+ if MAC2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN MAC Address" 8 58 $GEN_MAC_LAN --title "LAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC2 ]; then
+ WAN_MAC="$GEN_MAC_LAN"
+ else
+ WAN_MAC="$MAC2"
+ fi
+ echo -e "${DGN}Using WAN MAC Address: ${BGN}$WAN_MAC${CL}"
+ else
+ exit-script
+ fi
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create OPNsense VM?" --no-button Do-Over 10 58); then
+ echo -e "${RD}Creating a OPNsense VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
arch_check
@@ -515,29 +515,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -554,39 +554,39 @@ msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir)
- DISK_EXT=".qcow2"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format qcow2"
- THIN=""
- ;;
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
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 proxmox-helper-scripts -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 \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=2G \
- -boot order=scsi0 \
- -serial0 socket \
- -tags community-script >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=2G \
+ -boot order=scsi0 \
+ -serial0 socket \
+ -tags community-script >/dev/null
qm resize $VMID scsi0 10G >/dev/null
DESCRIPTION=$(
- cat <
@@ -599,7 +599,7 @@ DESCRIPTION=$(
-
+
GitHub
@@ -619,7 +619,7 @@ qm set "$VMID" -description "$DESCRIPTION" >/dev/null
msg_info "Bridge interfaces are being added."
qm set $VMID \
- -net0 virtio,bridge=${BRG},macaddr=${MAC}${VLAN}${MTU} 2>/dev/null
+ -net0 virtio,bridge=${BRG},macaddr=${MAC}${VLAN}${MTU} 2>/dev/null
msg_ok "Bridge interfaces have been successfully added."
msg_ok "Created a OPNsense VM ${CL}${BL}(${HN})"
@@ -629,7 +629,7 @@ sleep 90
send_line_to_vm "root"
send_line_to_vm "fetch https://raw.githubusercontent.com/opnsense/update/master/src/bootstrap/opnsense-bootstrap.sh.in"
qm set $VMID \
- -net1 virtio,bridge=${WAN_BRG},macaddr=${WAN_MAC} &>/dev/null
+ -net1 virtio,bridge=${WAN_BRG},macaddr=${WAN_MAC} &>/dev/null
sleep 10
send_line_to_vm "sh ./opnsense-bootstrap.sh.in -y -f -r 25.1"
msg_ok "OPNsense VM is being installed, do not close the terminal, or the installation will fail."
@@ -640,47 +640,47 @@ send_line_to_vm "opnsense"
send_line_to_vm "2"
if [ "$IP_ADDR" != "" ]; then
- send_line_to_vm "1"
- send_line_to_vm "n"
- send_line_to_vm "${IP_ADDR}"
- send_line_to_vm "${NETMASK}"
- send_line_to_vm "${LAN_GW}"
- send_line_to_vm "n"
- send_line_to_vm " "
- send_line_to_vm "n"
- send_line_to_vm "n"
- send_line_to_vm " "
- send_line_to_vm "n"
- send_line_to_vm "n"
- send_line_to_vm "n"
- send_line_to_vm "n"
- send_line_to_vm "n"
+ send_line_to_vm "1"
+ send_line_to_vm "n"
+ send_line_to_vm "${IP_ADDR}"
+ send_line_to_vm "${NETMASK}"
+ send_line_to_vm "${LAN_GW}"
+ send_line_to_vm "n"
+ send_line_to_vm " "
+ send_line_to_vm "n"
+ send_line_to_vm "n"
+ send_line_to_vm " "
+ send_line_to_vm "n"
+ send_line_to_vm "n"
+ send_line_to_vm "n"
+ send_line_to_vm "n"
+ send_line_to_vm "n"
else
- send_line_to_vm "1"
- send_line_to_vm "y"
- send_line_to_vm "n"
- send_line_to_vm "n"
- send_line_to_vm " "
- send_line_to_vm "n"
- send_line_to_vm "n"
- send_line_to_vm "n"
+ send_line_to_vm "1"
+ send_line_to_vm "y"
+ send_line_to_vm "n"
+ send_line_to_vm "n"
+ send_line_to_vm " "
+ send_line_to_vm "n"
+ send_line_to_vm "n"
+ send_line_to_vm "n"
fi
#we need to wait for the Config changes to be saved
sleep 20
if [ "$WAN_IP_ADDR" != "" ]; then
- send_line_to_vm "2"
- send_line_to_vm "2"
- send_line_to_vm "n"
- send_line_to_vm "${WAN_IP_ADDR}"
- send_line_to_vm "${NETMASK}"
- send_line_to_vm "${LAN_GW}"
- send_line_to_vm "n"
- send_line_to_vm " "
- send_line_to_vm "n"
- send_line_to_vm " "
- send_line_to_vm "n"
- send_line_to_vm "n"
- send_line_to_vm "n"
+ send_line_to_vm "2"
+ send_line_to_vm "2"
+ send_line_to_vm "n"
+ send_line_to_vm "${WAN_IP_ADDR}"
+ send_line_to_vm "${NETMASK}"
+ send_line_to_vm "${LAN_GW}"
+ send_line_to_vm "n"
+ send_line_to_vm " "
+ send_line_to_vm "n"
+ send_line_to_vm " "
+ send_line_to_vm "n"
+ send_line_to_vm "n"
+ send_line_to_vm "n"
fi
sleep 10
send_line_to_vm "0"
@@ -688,9 +688,9 @@ msg_ok "Started OPNsense VM"
msg_ok "Completed Successfully!\n"
if [ "$IP_ADDR" != "" ]; then
- echo -e "${INFO}${YW} Access it using the following URL:${CL}"
- echo -e "${TAB}${GATEWAY}${BGN}http://${IP_ADDR}${CL}"
+ echo -e "${INFO}${YW} Access it using the following URL:${CL}"
+ echo -e "${TAB}${GATEWAY}${BGN}http://${IP_ADDR}${CL}"
else
- echo -e "${INFO}${YW} LAN IP was DHCP.${CL}"
- echo -e "${INFO}${BGN}To find the IP login to the VM shell${CL}"
+ echo -e "${INFO}${YW} LAN IP was DHCP.${CL}"
+ echo -e "${INFO}${BGN}To find the IP login to the VM shell${CL}"
fi
diff --git a/vm/owncloud-vm.sh b/vm/owncloud-vm.sh
index 5b6c186ef..7db765ad3 100644
--- a/vm/owncloud-vm.sh
+++ b/vm/owncloud-vm.sh
@@ -8,8 +8,8 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
______ __ __ _______ __ _ ____ ___
/_ __/_ _________ / //_/__ __ __ ___ _ _____ / ___/ /__ __ _____/ / | | / / |/ /
/ / / // / __/ _ \/ ,< / -_) // / / _ \ |/|/ / _ \/ /__/ / _ \/ // / _ / | |/ / /|_/ /
@@ -47,322 +47,322 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "$command"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "$command"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "$NAME" --yesno "This will create a New $NAME. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "⚠ User exited script \n" && exit
+ header_info && echo -e "⚠ User exited script \n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne " ${HOLD} ${YW}${msg}..."
+ local msg="$1"
+ echo -ne " ${HOLD} ${YW}${msg}..."
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- msg_error "This script will not work with PiMox! \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ msg_error "This script will not work with PiMox! \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "⚠ User exited script \n"
- exit
+ clear
+ echo -e "⚠ User exited script \n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=",efitype=4m"
- MACHINE=""
- DISK_CACHE=""
- HN="turnkey-owncloud-vm"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="2048"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="no"
- METHOD="default"
- echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${DGN}Using Machine Type: ${BGN}i440fx${CL}"
- echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
- echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
- echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
- echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${DGN}Using Bridge: ${BGN}${BRG}${CL}"
- echo -e "${DGN}Using MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${DGN}Using VLAN: ${BGN}Default${CL}"
- echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
- echo -e "${BL}Creating a $NAME using the above default settings${CL}"
+ VMID=$(get_valid_nextid)
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ DISK_CACHE=""
+ HN="turnkey-owncloud-vm"
+ CPU_TYPE=""
+ CORE_COUNT="2"
+ RAM_SIZE="2048"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="no"
+ METHOD="default"
+ echo -e "${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${DGN}Using Machine Type: ${BGN}i440fx${CL}"
+ echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
+ echo -e "${DGN}Using Hostname: ${BGN}${HN}${CL}"
+ echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
+ echo -e "${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${DGN}Using Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${DGN}Using MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${DGN}Using VLAN: ${BGN}Default${CL}"
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
+ echo -e "${BL}Creating a $NAME using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit-script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DGN}Using Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 turnkey-owncloud-vm --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="turnkey-owncloud-vm"
+ echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
+ fi
else
- echo -e "${DGN}Using Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit-script
fi
- else
- exit-script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 turnkey-owncloud-vm --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="turnkey-owncloud-vm"
- echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $CPU_TYPE1 = "1" ]; then
- echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
else
- echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
+ exit-script
fi
- else
- exit-script
- fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ fi
else
- echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="2048"
- echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
+ fi
else
- echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
else
- MTU=",mtu=$MTU1"
- echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
+ echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
fi
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
-
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a $NAME?" --no-button Do-Over 10 58); then
- echo -e "${RD}Creating a $NAME using the above advanced settings${CL}"
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a $NAME?" --no-button Do-Over 10 58); then
+ echo -e "${RD}Creating a $NAME using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
@@ -375,29 +375,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -413,39 +413,39 @@ msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1,2}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Creating a $NAME"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios seabios${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
- -name $HN -tags community-script -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
pvesm alloc $STORAGE $VMID $DISK1 12G 1>&/dev/null
qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null
qm set $VMID \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN} \
- -scsi1 ${DISK2_REF},${DISK_CACHE}${THIN} \
- -boot order='scsi1;scsi0' >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN} \
+ -scsi1 ${DISK2_REF},${DISK_CACHE}${THIN} \
+ -boot order='scsi1;scsi0' >/dev/null
DESCRIPTION=$(
- cat <
@@ -458,7 +458,7 @@ DESCRIPTION=$(
-
+
GitHub
@@ -477,9 +477,9 @@ EOF
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
msg_ok "Created a $NAME ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting $NAME"
- qm start $VMID
- msg_ok "Started $NAME"
+ msg_info "Starting $NAME"
+ qm start $VMID
+ msg_ok "Started $NAME"
fi
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"
diff --git a/vm/ubuntu2204-vm.sh b/vm/ubuntu2204-vm.sh
index a662efb3c..2946da3e7 100644
--- a/vm/ubuntu2204-vm.sh
+++ b/vm/ubuntu2204-vm.sh
@@ -7,8 +7,8 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
__ ____ __ ___ ___ ____ __ __ _ ____ ___
/ / / / /_ __ ______ / /___ __ |__ \|__ \ / __ \/ // / | | / / |/ /
/ / / / __ \/ / / / __ \/ __/ / / / __/ /__/ / / / / / // /_ | | / / /|_/ /
@@ -63,340 +63,340 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "$command"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "$command"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Ubuntu 22.04 VM" --yesno "This will create a New Ubuntu 22.04 VM. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR}${CM}${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
- echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "\n${CROSS}${RD}User exited script${CL}\n"
- exit
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=",efitype=4m"
- MACHINE=""
- DISK_SIZE="5G"
- DISK_CACHE=""
- HN="ubuntu"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="2048"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="yes"
- METHOD="default"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a Ubuntu 22.04 VM using the above default settings${CL}"
+ VMID=$(get_valid_nextid)
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ DISK_SIZE="5G"
+ DISK_CACHE=""
+ HN="ubuntu"
+ CPU_TYPE=""
+ CORE_COUNT="2"
+ RAM_SIZE="2048"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Ubuntu 22.04 VM using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit-script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit-script
+ fi
else
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
- if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
- DISK_SIZE="${DISK_SIZE}G"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
- elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
- exit-script
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 ubuntu --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="ubuntu"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit-script
fi
- else
- exit-script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 ubuntu --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="ubuntu"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $CPU_TYPE1 = "1" ]; then
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
else
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
+ exit-script
fi
- else
- exit-script
- fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ fi
else
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="2048"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
else
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
else
- MTU=",mtu=$MTU1"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
fi
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
-
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Ubuntu 22.04 VM?" --no-button Do-Over 10 58); then
- echo -e "${CREATING}${BOLD}${DGN}Creating a Ubuntu 22.04 VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Ubuntu 22.04 VM?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Ubuntu 22.04 VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
@@ -408,29 +408,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -446,38 +446,38 @@ msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir | cifs)
- DISK_EXT=".qcow2"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format qcow2"
- THIN=""
- ;;
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Creating a Ubuntu 22.04 VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
- -name $HN -tags community-script -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 \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
- -ide2 ${STORAGE}:cloudinit \
- -boot order=scsi0 \
- -serial0 socket >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
+ -ide2 ${STORAGE}:cloudinit \
+ -boot order=scsi0 \
+ -serial0 socket >/dev/null
DESCRIPTION=$(
- cat <
@@ -490,7 +490,7 @@ DESCRIPTION=$(
-
+
GitHub
@@ -508,18 +508,18 @@ EOF
)
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
if [ -n "$DISK_SIZE" ]; then
- msg_info "Resizing disk to $DISK_SIZE GB"
- qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
+ msg_info "Resizing disk to $DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
else
- msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
- qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
+ msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
fi
msg_ok "Created a Ubuntu 22.04 VM ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting Ubuntu 22.04 VM"
- qm start $VMID
- msg_ok "Started Ubuntu 22.04 VM"
+ msg_info "Starting Ubuntu 22.04 VM"
+ qm start $VMID
+ msg_ok "Started Ubuntu 22.04 VM"
fi
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"
diff --git a/vm/ubuntu2404-vm.sh b/vm/ubuntu2404-vm.sh
index 28f1f4e20..04afdc82a 100644
--- a/vm/ubuntu2404-vm.sh
+++ b/vm/ubuntu2404-vm.sh
@@ -8,8 +8,8 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
__ ____ __ ___ __ __ ____ __ __ _ ____ ___
/ / / / /_ __ ______ / /___ __ |__ \/ // / / __ \/ // / | | / / |/ /
/ / / / __ \/ / / / __ \/ __/ / / / __/ / // /_ / / / / // /_ | | / / /|_/ /
@@ -66,340 +66,340 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "$command"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "$command"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Ubuntu 24.04 VM" --yesno "This will create a New Ubuntu 24.04 VM. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR}${CM}${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
- echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "\n${CROSS}${RD}User exited script${CL}\n"
- exit
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=",efitype=4m"
- MACHINE=""
- DISK_SIZE="7G"
- DISK_CACHE=""
- HN="ubuntu"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="2048"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="yes"
- METHOD="default"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a Ubuntu 24.04 VM using the above default settings${CL}"
+ VMID=$(get_valid_nextid)
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ DISK_SIZE="7G"
+ DISK_CACHE=""
+ HN="ubuntu"
+ CPU_TYPE=""
+ CORE_COUNT="2"
+ RAM_SIZE="2048"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Ubuntu 24.04 VM using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit-script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit-script
+ fi
else
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
- if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
- DISK_SIZE="${DISK_SIZE}G"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
- elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
- exit-script
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 ubuntu --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="ubuntu"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit-script
fi
- else
- exit-script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 ubuntu --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="ubuntu"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $CPU_TYPE1 = "1" ]; then
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
else
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
+ exit-script
fi
- else
- exit-script
- fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ fi
else
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="2048"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
else
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
else
- MTU=",mtu=$MTU1"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
fi
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
-
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Ubuntu 24.04 VM?" --no-button Do-Over 10 58); then
- echo -e "${CREATING}${BOLD}${DGN}Creating a Ubuntu 24.04 VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Ubuntu 24.04 VM?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Ubuntu 24.04 VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
arch_check
@@ -410,29 +410,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -448,38 +448,38 @@ msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir | cifs)
- DISK_EXT=".qcow2"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format qcow2"
- THIN=""
- ;;
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Creating a Ubuntu 24.04 VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
- -name $HN -tags community-script -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 \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
- -ide2 ${STORAGE}:cloudinit \
- -boot order=scsi0 \
- -serial0 socket >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
+ -ide2 ${STORAGE}:cloudinit \
+ -boot order=scsi0 \
+ -serial0 socket >/dev/null
DESCRIPTION=$(
- cat <
@@ -492,7 +492,7 @@ DESCRIPTION=$(
-
+
GitHub
@@ -510,18 +510,18 @@ EOF
)
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
if [ -n "$DISK_SIZE" ]; then
- msg_info "Resizing disk to $DISK_SIZE GB"
- qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
+ msg_info "Resizing disk to $DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
else
- msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
- qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
+ msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
fi
msg_ok "Created a Ubuntu 24.04 VM ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting Ubuntu 24.04 VM"
- qm start $VMID
- msg_ok "Started Ubuntu 24.04 VM"
+ msg_info "Starting Ubuntu 24.04 VM"
+ qm start $VMID
+ msg_ok "Started Ubuntu 24.04 VM"
fi
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"
diff --git a/vm/ubuntu2410-vm.sh b/vm/ubuntu2410-vm.sh
index 82d8519c3..071062fc4 100644
--- a/vm/ubuntu2410-vm.sh
+++ b/vm/ubuntu2410-vm.sh
@@ -7,8 +7,8 @@
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
function header_info {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
__ ____ __ ___ __ __ _______ _ ____ ___
/ / / / /_ __ ______ / /___ __ |__ \/ // / < / __ \ | | / / |/ /
/ / / / __ \/ / / / __ \/ __/ / / / __/ / // /_ / / / / / | | / / /|_/ /
@@ -66,467 +66,467 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "$command"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "$command"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "$APP" --yesno "This will create a New $APP. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR}${CM}${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
- echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "\n${CROSS}${RD}User exited script${CL}\n"
- exit
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
}
function init_settings() {
- VMID="$(get_valid_nextid)"
- HN="ubuntu"
- DISK_SIZE="8G"
- DISK_CACHE=""
- DISK_EXT=".qcow2"
- DISK_REF="$VMID/"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="2048"
- MACHINE_TYPE="i440fx"
- MACHINE=""
- FORMAT=",efitype=4m"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="yes"
+ VMID="$(get_valid_nextid)"
+ HN="ubuntu"
+ DISK_SIZE="8G"
+ DISK_CACHE=""
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ CPU_TYPE=""
+ CORE_COUNT="2"
+ RAM_SIZE="2048"
+ MACHINE_TYPE="i440fx"
+ MACHINE=""
+ FORMAT=",efitype=4m"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
}
function default_settings() {
- METHOD="default"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}${DISK_CACHE:-None}${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}${MACHINE_TYPE}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}${var_vlan:-Default}${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}${var_mtu:-Default}${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a $APP using default settings${CL}"
+ METHOD="default"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}${DISK_CACHE:-None}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}${MACHINE_TYPE}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}${var_vlan:-Default}${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}${var_mtu:-Default}${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a $APP using default settings${CL}"
}
function apply_env_overrides() {
- METHOD="env"
- [ -n "$var_vmid" ] && VMID="$var_vmid"
- HN=$(echo "${var_hostname,,}" | tr -cd '[:alnum:]-')
- [[ -z "$HN" ]] && HN="ubuntu"
- [[ ! "$HN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]] && {
- msg_error "Invalid hostname: $HN"
- exit 1
- }
+ METHOD="env"
+ [ -n "$var_vmid" ] && VMID="$var_vmid"
+ HN=$(echo "${var_hostname,,}" | tr -cd '[:alnum:]-')
+ [[ -z "$HN" ]] && HN="ubuntu"
+ [[ ! "$HN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]] && {
+ msg_error "Invalid hostname: $HN"
+ exit 1
+ }
- case "$var_machine" in
- q35)
- MACHINE_TYPE="q35"
- FORMAT=""
- MACHINE=" -machine q35"
- ;;
- *)
- MACHINE_TYPE="i440fx"
- FORMAT=",efitype=4m"
- MACHINE=""
- ;;
- esac
+ case "$var_machine" in
+ q35)
+ MACHINE_TYPE="q35"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ ;;
+ *)
+ MACHINE_TYPE="i440fx"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ ;;
+ esac
- case "$var_cpu_type" in
- 1) CPU_TYPE=" -cpu host" ;;
- *) CPU_TYPE="" ;;
- esac
+ case "$var_cpu_type" in
+ 1) CPU_TYPE=" -cpu host" ;;
+ *) CPU_TYPE="" ;;
+ esac
- case "$var_disk_cache" in
- 1) DISK_CACHE="cache=writethrough," ;;
- *) DISK_CACHE="" ;;
- esac
+ case "$var_disk_cache" in
+ 1) DISK_CACHE="cache=writethrough," ;;
+ *) DISK_CACHE="" ;;
+ esac
- [[ "$var_cpu" =~ ^[1-9][0-9]*$ ]] && CORE_COUNT="$var_cpu" || CORE_COUNT="2"
- [[ "$var_ram" =~ ^[1-9][0-9]*$ ]] && RAM_SIZE="$var_ram" || RAM_SIZE="2048"
- [[ -n "$var_disk" ]] && DISK_SIZE="$var_disk" || DISK_SIZE="8G"
- [ -n "$var_bridge" ] && BRG="$var_bridge"
- [ -z "$BRG" ] && BRG="vmbr0"
+ [[ "$var_cpu" =~ ^[1-9][0-9]*$ ]] && CORE_COUNT="$var_cpu" || CORE_COUNT="2"
+ [[ "$var_ram" =~ ^[1-9][0-9]*$ ]] && RAM_SIZE="$var_ram" || RAM_SIZE="2048"
+ [[ -n "$var_disk" ]] && DISK_SIZE="$var_disk" || DISK_SIZE="8G"
+ [ -n "$var_bridge" ] && BRG="$var_bridge"
+ [ -z "$BRG" ] && BRG="vmbr0"
- [ -n "$var_mac" ] && MAC="$var_mac"
- [ -z "$MAC" ] && MAC="$GEN_MAC"
- VLAN=${var_vlan:+",tag=$var_vlan"}
- MTU=${var_mtu:+",mtu=$var_mtu"}
- START_VM="$var_start_vm"
+ [ -n "$var_mac" ] && MAC="$var_mac"
+ [ -z "$MAC" ] && MAC="$GEN_MAC"
+ VLAN=${var_vlan:+",tag=$var_vlan"}
+ MTU=${var_mtu:+",mtu=$var_mtu"}
+ START_VM="$var_start_vm"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}${MACHINE_TYPE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}${DISK_CACHE:-None}${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}${CPU_TYPE:+Host}${CPU_TYPE:-KVM64}${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}${var_vlan:-Default}${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}${var_mtu:-Default}${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a $APP using environment settings${CL}"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}${MACHINE_TYPE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}${DISK_CACHE:-None}${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}${CPU_TYPE:+Host}${CPU_TYPE:-KVM64}${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}${var_vlan:-Default}${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}${var_mtu:-Default}${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a $APP using environment settings${CL}"
}
function validate_env_settings() {
- [[ -n "$var_hostname" ]] && {
- HN_CLEANED=$(echo "$var_hostname" | tr -cd '[:alnum:]-')
- if [[ ! "$HN_CLEANED" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]]; then
- msg_error "Invalid hostname: $var_hostname"
- exit 1
- fi
- }
+ [[ -n "$var_hostname" ]] && {
+ HN_CLEANED=$(echo "$var_hostname" | tr -cd '[:alnum:]-')
+ if [[ ! "$HN_CLEANED" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]]; then
+ msg_error "Invalid hostname: $var_hostname"
+ exit 1
+ fi
+ }
- [[ -n "$var_vmid" && ! "$var_vmid" =~ ^[1-9][0-9]{2,}$ ]] && {
- msg_error "Invalid VMID: must be a number >= 100"
- exit 1
- }
+ [[ -n "$var_vmid" && ! "$var_vmid" =~ ^[1-9][0-9]{2,}$ ]] && {
+ msg_error "Invalid VMID: must be a number >= 100"
+ exit 1
+ }
- [[ -n "$var_cpu" && ! "$var_cpu" =~ ^[1-9][0-9]*$ ]] && {
- msg_error "Invalid CPU core count: must be > 0"
- exit 1
- }
+ [[ -n "$var_cpu" && ! "$var_cpu" =~ ^[1-9][0-9]*$ ]] && {
+ msg_error "Invalid CPU core count: must be > 0"
+ exit 1
+ }
- [[ -n "$var_ram" && ! "$var_ram" =~ ^[1-9][0-9]*$ ]] && {
- msg_error "Invalid RAM size: must be > 0"
- exit 1
- }
+ [[ -n "$var_ram" && ! "$var_ram" =~ ^[1-9][0-9]*$ ]] && {
+ msg_error "Invalid RAM size: must be > 0"
+ exit 1
+ }
- [[ -n "$var_disk" && ! "$var_disk" =~ ^[1-9][0-9]*G$ ]] && {
- msg_error "Invalid disk size: must be like 10G"
- exit 1
- }
+ [[ -n "$var_disk" && ! "$var_disk" =~ ^[1-9][0-9]*G$ ]] && {
+ msg_error "Invalid disk size: must be like 10G"
+ exit 1
+ }
- [[ -n "$var_mac" && ! "$var_mac" =~ ^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$ ]] && {
- msg_error "Invalid MAC address: $var_mac"
- exit 1
- }
+ [[ -n "$var_mac" && ! "$var_mac" =~ ^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$ ]] && {
+ msg_error "Invalid MAC address: $var_mac"
+ exit 1
+ }
- [[ -n "$var_mtu" && ! "$var_mtu" =~ ^[1-9][0-9]{2,4}$ ]] && {
- msg_error "Invalid MTU value: $var_mtu"
- exit 1
- }
+ [[ -n "$var_mtu" && ! "$var_mtu" =~ ^[1-9][0-9]{2,4}$ ]] && {
+ msg_error "Invalid MTU value: $var_mtu"
+ exit 1
+ }
- [[ -n "$var_vlan" && ! "$var_vlan" =~ ^[0-9]{1,4}$ ]] && {
- msg_error "Invalid VLAN tag: must be numeric"
- exit 1
- }
+ [[ -n "$var_vlan" && ! "$var_vlan" =~ ^[0-9]{1,4}$ ]] && {
+ msg_error "Invalid VLAN tag: must be numeric"
+ exit 1
+ }
- [[ -n "$var_start_vm" && ! "$var_start_vm" =~ ^(yes|no)$ ]] && {
- msg_error "var_start_vm must be 'yes' or 'no'"
- exit 1
- }
+ [[ -n "$var_start_vm" && ! "$var_start_vm" =~ ^(yes|no)$ ]] && {
+ msg_error "var_start_vm must be 'yes' or 'no'"
+ exit 1
+ }
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit-script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit-script
+ fi
else
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
- if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
- DISK_SIZE="${DISK_SIZE}G"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
- elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
- exit-script
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 ubuntu --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VM_NAME" ]; then
+ HN="ubuntu"
+ else
+ HN=$(echo "${VM_NAME,,}" | tr -cd '[:alnum:]-')
+ fi
+ if [[ ! "$HN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]]; then
+ msg_error "Invalid hostname: $HN. Must be 1–63 chars, alphanumeric or hyphen, and not start/end with hyphen."
+ exit-script
+ fi
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
else
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit-script
fi
- else
- exit-script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 ubuntu --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VM_NAME" ]; then
- HN="ubuntu"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ "$CPU_TYPE1" = "1" ]; then
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
else
- HN=$(echo "${VM_NAME,,}" | tr -cd '[:alnum:]-')
+ exit-script
fi
- if [[ ! "$HN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$ ]]; then
- msg_error "Invalid hostname: $HN. Must be 1–63 chars, alphanumeric or hyphen, and not start/end with hyphen."
- exit-script
- fi
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
- else
- exit-script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ "$CPU_TYPE1" = "1" ]; then
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ CORE_COUNT=$(echo "$CORE_COUNT" | tr -cd '[:digit:]')
+ if [[ ! "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
+ msg_error "CPU core count must be a positive integer."
+ exit-script
+ fi
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
else
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
+ exit-script
fi
- else
- exit-script
- fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- CORE_COUNT=$(echo "$CORE_COUNT" | tr -cd '[:digit:]')
- if [[ ! "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
- msg_error "CPU core count must be a positive integer."
- exit-script
- fi
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
- else
- exit-script
- fi
-
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- RAM_SIZE=$(echo "$RAM_SIZE" | tr -cd '[:digit:]')
- if [[ ! "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
- msg_error "RAM size must be a positive integer (in MiB)."
- exit-script
- fi
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
- else
- exit-script
- fi
-
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ RAM_SIZE=$(echo "$RAM_SIZE" | tr -cd '[:digit:]')
+ if [[ ! "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
+ msg_error "RAM size must be a positive integer (in MiB)."
+ exit-script
+ fi
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
else
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
else
- MTU=",mtu=$MTU1"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
+ else
+ exit-script
+ fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a $APP?" --no-button Do-Over 10 58); then
- echo -e "${CREATING}${BOLD}${DGN}Creating a $APP using the above advanced settings${CL}"
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
+ else
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
+ fi
+
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a $APP?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a $APP using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function has_env_overrides() {
- env | grep -qE "^var_(bridge|cpu|cpu_type|disk|disk_cache|hostname|mac|machine|mtu|ram|start_vm|vlan|vmid)="
+ env | grep -qE "^var_(bridge|cpu|cpu_type|disk|disk_cache|hostname|mac|machine|mtu|ram|start_vm|vlan|vmid)="
}
function start_script() {
- header_info
- init_settings
- if has_env_overrides; then
- echo -e "${ADVANCED}${BOLD}${BL}Using Environment Variable Overrides${CL}"
- METHOD="env"
- apply_env_overrides
- elif (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
- default_settings
- else
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ header_info
+ init_settings
+ if has_env_overrides; then
+ echo -e "${ADVANCED}${BOLD}${BL}Using Environment Variable Overrides${CL}"
+ METHOD="env"
+ apply_env_overrides
+ elif (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
@@ -538,29 +538,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -576,42 +576,42 @@ msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir | cifs)
- DISK_EXT=".qcow2"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format qcow2"
- THIN=""
- ;;
+ DISK_EXT=".qcow2"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format qcow2"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Creating a $APP"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
- -name $HN -tags "community-script;ubuntu" -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
+ -name $HN -tags "community-script;ubuntu" -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
if [[ "$STORAGE_TYPE" != "lvmthin" ]]; then
- pvesm alloc $STORAGE $VMID $DISK0 4M >/dev/null
+ pvesm alloc $STORAGE $VMID $DISK0 4M >/dev/null
fi
qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null
qm set $VMID \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
- -ide2 ${STORAGE}:cloudinit \
- -boot order=scsi0 \
- -serial0 socket \
- -smbios1 type=1 \
- --ciuser "ubuntu" -cipassword "ubuntu" >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
+ -ide2 ${STORAGE}:cloudinit \
+ -boot order=scsi0 \
+ -serial0 socket \
+ -smbios1 type=1 \
+ --ciuser "ubuntu" -cipassword "ubuntu" >/dev/null
DESCRIPTION=$(
- cat <
@@ -642,20 +642,20 @@ EOF
)
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
if [ -n "$DISK_SIZE" ]; then
- msg_info "Resizing disk to $DISK_SIZE GB"
- qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
- msg_ok "Resized disk to ${CL}${BL}${DISK_SIZE}${CL} GB"
+ msg_info "Resizing disk to $DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
+ msg_ok "Resized disk to ${CL}${BL}${DISK_SIZE}${CL} GB"
else
- msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
- qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
- msg_ok "Resized disk to ${CL}${BL}${DEFAULT_DISK_SIZE}${CL} GB"
+ msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
+ msg_ok "Resized disk to ${CL}${BL}${DEFAULT_DISK_SIZE}${CL} GB"
fi
msg_ok "Created a $APP ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting $APP"
- qm start $VMID
- msg_ok "Started $APP"
+ msg_info "Starting $APP"
+ qm start $VMID
+ msg_ok "Started $APP"
fi
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"
diff --git a/vm/umbrel-os-vm.sh b/vm/umbrel-os-vm.sh
index 0e238d6ee..bdaca5ca2 100644
--- a/vm/umbrel-os-vm.sh
+++ b/vm/umbrel-os-vm.sh
@@ -57,341 +57,341 @@ trap cleanup EXIT
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "${command}"
- local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
- echo -e "\n$error_message\n"
- cleanup_vmid
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "${command}"
+ local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
+ echo -e "\n$error_message\n"
+ cleanup_vmid
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- post_update_to_api "done" "none"
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ post_update_to_api "done" "none"
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Umbrel OS VM" --yesno "This will create a New Umbrel OS VM. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR}${CM}${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
function pve_check() {
- if ! pveversion | grep -Eq "pve-manager/8\.[1-4](\.[0-9]+)*"; then
- msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
- echo -e "Requires Proxmox Virtual Environment Version 8.1 or later."
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
+ msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported"
+ echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
- echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit_script() {
- clear
- echo -e "\n${CROSS}${RD}User exited script${CL}\n"
- exit
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=",efitype=4m"
- MACHINE=""
- DISK_CACHE=""
- DISK_SIZE="32G"
- HN="umbrel-os"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="4096"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="yes"
- METHOD="default"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a Umbrel OS VM using the above default settings${CL}"
+ VMID=$(get_valid_nextid)
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ DISK_CACHE=""
+ DISK_SIZE="32G"
+ HN="umbrel-os"
+ CPU_TYPE=""
+ CORE_COUNT="2"
+ RAM_SIZE="4096"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Umbrel OS VM using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
+ "i440fx" "Machine i440fx" ON \
+ "q35" "Machine q35" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $MACH = q35 ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit_script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
- "i440fx" "Machine i440fx" ON \
- "q35" "Machine q35" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $MACH = q35 ]; then
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit_script
+ fi
else
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}$MACH${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit_script
fi
- else
- exit_script
- fi
- if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
- if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
- DISK_SIZE="${DISK_SIZE}G"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
- elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
- exit_script
+ exit_script
fi
- else
- exit_script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None (Default)" ON \
- "1" "Write Through" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 umbrel-os --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="umbrel-os"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit_script
fi
- else
- exit_script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 umbrel-os --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="umbrel-os"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "KVM64 (Default)" ON \
+ "1" "Host" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $CPU_TYPE1 = "1" ]; then
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ else
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ fi
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ exit_script
fi
- else
- exit_script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "KVM64 (Default)" ON \
- "1" "Host" OFF \
- 3>&1 1>&2 2>&3); then
- if [ $CPU_TYPE1 = "1" ]; then
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
else
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
+ exit_script
fi
- else
- exit_script
- fi
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ fi
else
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ exit_script
fi
- else
- exit_script
- fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="2048"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
else
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ exit_script
fi
- else
- exit_script
- fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ exit_script
fi
- else
- exit_script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ exit_script
fi
- else
- exit_script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ exit_script
fi
- else
- exit_script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
else
- MTU=",mtu=$MTU1"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
fi
- else
- exit_script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
-
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Umbrel OS VM?" --no-button Do-Over 10 58); then
- echo -e "${CREATING}${BOLD}${DGN}Creating a Umbrel OS VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Umbrel OS VM?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Umbrel OS VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
arch_check
@@ -402,29 +402,29 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ 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 ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
@@ -437,7 +437,7 @@ curl -f#SL -o "$FILE" "$URL"
msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
if ! command -v pv &>/dev/null; then
- apt-get update &>/dev/null && apt-get install -y pv &>/dev/null
+ apt-get update &>/dev/null && apt-get install -y pv &>/dev/null
fi
msg_info "Decompressing $FILE with progress${CL}\n"
@@ -449,39 +449,39 @@ msg_ok "Decompressed to ${CL}${BL}${FILE%.xz}${CL}"
STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
case $STORAGE_TYPE in
nfs | dir)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ THIN=""
+ ;;
btrfs)
- DISK_EXT=".raw"
- DISK_REF="$VMID/"
- DISK_IMPORT="-format raw"
- FORMAT=",efitype=4m"
- THIN=""
- ;;
+ DISK_EXT=".raw"
+ DISK_REF="$VMID/"
+ DISK_IMPORT="-format raw"
+ FORMAT=",efitype=4m"
+ THIN=""
+ ;;
esac
for i in {0,1,2}; do
- disk="DISK$i"
- eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
- eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
+ disk="DISK$i"
+ eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
+ eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
done
msg_info "Creating a Umbrel OS VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
- -name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci >/dev/null
+ -name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci >/dev/null
pvesm alloc $STORAGE $VMID $DISK0 4M >/dev/null
qm importdisk $VMID ${FILE_IMG} $STORAGE ${DISK_IMPORT:-} >/dev/null
qm set $VMID \
- -efidisk0 ${DISK0_REF}${FORMAT} \
- -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
- -boot order=scsi0 \
- -serial0 socket >/dev/null
+ -efidisk0 ${DISK0_REF}${FORMAT} \
+ -scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=${DISK_SIZE} \
+ -boot order=scsi0 \
+ -serial0 socket >/dev/null
qm set $VMID --agent enabled=1 >/dev/null
DESCRIPTION=$(
- cat <
@@ -513,18 +513,18 @@ EOF
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
if [ -n "$DISK_SIZE" ]; then
- msg_info "Resizing disk to $DISK_SIZE GB"
- qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
+ msg_info "Resizing disk to $DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
else
- msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
- qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
+ msg_info "Using default disk size of $DEFAULT_DISK_SIZE GB"
+ qm resize $VMID scsi0 ${DEFAULT_DISK_SIZE} >/dev/null
fi
msg_ok "Created a Umbrel OS VM ${CL}${BL}(${HN})"
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting Umbrel OS VM"
- qm start $VMID
- msg_ok "Started Umbrel OS VM"
+ msg_info "Starting Umbrel OS VM"
+ qm start $VMID
+ msg_ok "Started Umbrel OS VM"
fi
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"
diff --git a/vm/unifi-os-vm.sh b/vm/unifi-os-vm.sh
index 576cb2376..9ac19dbb1 100644
--- a/vm/unifi-os-vm.sh
+++ b/vm/unifi-os-vm.sh
@@ -5,10 +5,12 @@
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
+# Load Cloud-Init library for VM configuration
+source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/cloud-init.sh) 2>/dev/null || true
function header_info() {
- clear
- cat <<"EOF"
+ clear
+ cat <<"EOF"
__ __ _ _____ ____ _____ _____
/ / / /___ (_) __(_) / __ \/ ___/ / ___/___ ______ _____ _____
/ / / / __ \/ / /_/ / / / / /\__ \ \__ \/ _ \/ ___/ | / / _ \/ ___/
@@ -23,11 +25,13 @@ GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
METHOD=""
NSAPP="UniFi OS Server"
-var_os="linux"
-var_version="x64"
-UOS_VERSION="4.3.5"
-UOS_URL="https://fw-download.ubnt.com/data/unifi-os-server/da70-linux-x64-4.3.5-5306ffbb-fc6d-4414-912b-29cbfb0ce85a.5-x64"
-UOS_INSTALLER="unifi-os-server-${UOS_VERSION}.bin"
+var_os="debian"
+var_version="13"
+USE_CLOUD_INIT="no"
+OS_TYPE=""
+OS_VERSION=""
+OS_CODENAME=""
+OS_DISPLAY=""
YW=$(echo "\033[33m")
BL=$(echo "\033[36m")
@@ -61,6 +65,7 @@ MACADDRESS="${TAB}🔗${TAB}${CL}"
VLANTAG="${TAB}🏷️${TAB}${CL}"
CREATING="${TAB}🚀${TAB}${CL}"
ADVANCED="${TAB}🧩${TAB}${CL}"
+CLOUD="${TAB}☁️${TAB}${CL}"
THIN="discard=on,ssd=1,"
set -Eeuo pipefail
@@ -70,365 +75,446 @@ trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
function error_handler() {
- local exit_code="$?"
- local line_number="$1"
- local command="$2"
- post_update_to_api "failed" "${command}"
- echo -e "\n${RD}[ERROR]${CL} line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing ${YW}$command${CL}\n"
- if qm status $VMID &>/dev/null; then qm stop $VMID &>/dev/null || true; fi
+ local exit_code="$?"
+ local line_number="$1"
+ local command="$2"
+ post_update_to_api "failed" "${command}"
+ echo -e "\n${RD}[ERROR]${CL} line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing ${YW}$command${CL}\n"
+ if qm status $VMID &>/dev/null; then qm stop $VMID &>/dev/null || true; fi
}
function get_valid_nextid() {
- local try_id
- try_id=$(pvesh get /cluster/nextid)
- while true; do
- if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
- try_id=$((try_id + 1))
- continue
- fi
- if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
- try_id=$((try_id + 1))
- continue
- fi
- break
- done
- echo "$try_id"
+ local try_id
+ try_id=$(pvesh get /cluster/nextid)
+ while true; do
+ if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
+ try_id=$((try_id + 1))
+ continue
+ fi
+ break
+ done
+ echo "$try_id"
}
function cleanup_vmid() {
- if qm status $VMID &>/dev/null; then
- qm stop $VMID &>/dev/null
- qm destroy $VMID &>/dev/null
- fi
+ if qm status $VMID &>/dev/null; then
+ qm stop $VMID &>/dev/null
+ qm destroy $VMID &>/dev/null
+ fi
}
function cleanup() {
- popd >/dev/null
- post_update_to_api "done" "none"
- rm -rf $TEMP_DIR
+ popd >/dev/null
+ post_update_to_api "done" "none"
+ rm -rf $TEMP_DIR
}
TEMP_DIR=$(mktemp -d)
pushd $TEMP_DIR >/dev/null
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Unifi OS VM" --yesno "This will create a New Unifi OS VM. Proceed?" 10 58; then
- :
+ :
else
- header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
+ header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
fi
function msg_info() {
- local msg="$1"
- echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
+ local msg="$1"
+ echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
}
function msg_ok() {
- local msg="$1"
- echo -e "${BFR}${CM}${GN}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CM}${GN}${msg}${CL}"
}
function msg_error() {
- local msg="$1"
- echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
+ local msg="$1"
+ echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
}
function check_root() {
- if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
- clear
- msg_error "Please run this script as root."
- echo -e "\nExiting..."
- sleep 2
- exit
- fi
+ if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
+ clear
+ msg_error "Please run this script as root."
+ echo -e "\nExiting..."
+ sleep 2
+ exit
+ fi
}
# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
-# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 (NOT 9.1+)
+# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 – 9.1
pve_check() {
- local PVE_VER
- PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
+ local PVE_VER
+ PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
- # Check for Proxmox VE 8.x: allow 8.0–8.9
- if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
- local MINOR="${BASH_REMATCH[1]}"
- if ((MINOR < 0 || MINOR > 9)); then
- msg_error "This version of Proxmox VE is not supported."
- msg_error "Supported: Proxmox VE version 8.0 – 8.9"
- exit 1
+ # Check for Proxmox VE 8.x: allow 8.0–8.9
+ if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
+ local MINOR="${BASH_REMATCH[1]}"
+ if ((MINOR < 0 || MINOR > 9)); then
+ msg_error "This version of Proxmox VE is not supported."
+ msg_error "Supported: Proxmox VE version 8.0 – 8.9"
+ exit 1
+ fi
+ return 0
fi
- return 0
- fi
- # Check for Proxmox VE 9.x: allow ONLY 9.0
- if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
- local MINOR="${BASH_REMATCH[1]}"
- if ((MINOR != 0)); then
- msg_error "This version of Proxmox VE is not yet supported."
- msg_error "Supported: Proxmox VE version 9.0"
- exit 1
+ # Check for Proxmox VE 9.x: allow 9.0–9.1
+ if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
+ local MINOR="${BASH_REMATCH[1]}"
+ if ((MINOR < 0 || MINOR > 1)); then
+ msg_error "This version of Proxmox VE is not yet supported."
+ msg_error "Supported: Proxmox VE version 9.0 – 9.1"
+ exit 1
+ fi
+ return 0
fi
- return 0
- fi
- # All other unsupported versions
- msg_error "This version of Proxmox VE is not supported."
- msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0"
- exit 1
+ # All other unsupported versions
+ msg_error "This version of Proxmox VE is not supported."
+ msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0"
+ exit 1
}
function arch_check() {
- if [ "$(dpkg --print-architecture)" != "amd64" ]; then
- echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
- echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
- echo -e "Exiting..."
- sleep 2
- exit
- fi
+ if [ "$(dpkg --print-architecture)" != "amd64" ]; then
+ echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
+ echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
+ echo -e "Exiting..."
+ sleep 2
+ exit
+ fi
}
function ssh_check() {
- if command -v pveversion >/dev/null 2>&1; then
- if [ -n "${SSH_CLIENT:+x}" ]; then
- if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
- echo "you've been warned"
- else
- clear
- exit
- fi
+ if command -v pveversion >/dev/null 2>&1; then
+ if [ -n "${SSH_CLIENT:+x}" ]; then
+ if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
+ echo "you've been warned"
+ else
+ clear
+ exit
+ fi
+ fi
fi
- fi
}
function exit-script() {
- clear
- echo -e "\n${CROSS}${RD}User exited script${CL}\n"
- exit
+ clear
+ echo -e "\n${CROSS}${RD}User exited script${CL}\n"
+ exit
+}
+
+function select_os() {
+ if OS_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT OS" --radiolist \
+ "Choose Operating System for UniFi OS VM" 12 68 2 \
+ "debian13" "Debian 13 (Trixie) - Latest" ON \
+ "ubuntu2404" "Ubuntu 24.04 LTS (Noble)" OFF \
+ 3>&1 1>&2 2>&3); then
+ case $OS_CHOICE in
+ debian13)
+ OS_TYPE="debian"
+ OS_VERSION="13"
+ OS_CODENAME="trixie"
+ OS_DISPLAY="Debian 13 (Trixie)"
+ ;;
+ ubuntu2404)
+ OS_TYPE="ubuntu"
+ OS_VERSION="24.04"
+ OS_CODENAME="noble"
+ OS_DISPLAY="Ubuntu 24.04 LTS"
+ ;;
+ esac
+ echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}${OS_DISPLAY}${CL}"
+ else
+ exit-script
+ fi
+}
+
+function select_cloud_init() {
+ # Ubuntu only has cloudimg variant (always Cloud-Init), so no choice needed
+ if [ "$OS_TYPE" = "ubuntu" ]; then
+ USE_CLOUD_INIT="yes"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}yes (Ubuntu requires Cloud-Init)${CL}"
+ return
+ fi
+
+ # Debian has two image variants, so user can choose
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
+ --yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n• User accounts and passwords\n• SSH keys\n• Network settings (DHCP/Static)\n• DNS configuration\n\nYou can also configure these settings later in Proxmox UI.\n\nNote: Debian without Cloud-Init will use nocloud image with console auto-login." 18 68); then
+ USE_CLOUD_INIT="yes"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}yes${CL}"
+ else
+ USE_CLOUD_INIT="no"
+ echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}no${CL}"
+ fi
+}
+
+function get_image_url() {
+ local arch=$(dpkg --print-architecture)
+ case $OS_TYPE in
+ debian)
+ # Debian has two variants:
+ # - generic: For Cloud-Init enabled VMs
+ # - nocloud: For VMs without Cloud-Init (has console auto-login)
+ if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ echo "https://cloud.debian.org/images/cloud/${OS_CODENAME}/latest/debian-${OS_VERSION}-generic-${arch}.qcow2"
+ else
+ echo "https://cloud.debian.org/images/cloud/${OS_CODENAME}/latest/debian-${OS_VERSION}-nocloud-${arch}.qcow2"
+ fi
+ ;;
+ ubuntu)
+ # Ubuntu only has cloudimg variant (always with Cloud-Init support)
+ echo "https://cloud-images.ubuntu.com/${OS_CODENAME}/current/${OS_CODENAME}-server-cloudimg-${arch}.img"
+ ;;
+ esac
}
function default_settings() {
- VMID=$(get_valid_nextid)
- FORMAT=""
- MACHINE="q35"
- DISK_SIZE="30G"
- HN="unifi-server-os"
- CPU_TYPE=""
- CORE_COUNT="2"
- RAM_SIZE="4096"
- BRG="vmbr0"
- MAC="$GEN_MAC"
- VLAN=""
- MTU=""
- START_VM="yes"
- METHOD="default"
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}q35${CL}"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- echo -e "${CREATING}${BOLD}${DGN}Creating a Unifi OS VM using the above default settings${CL}"
+ # OS Selection - ALWAYS ask
+ select_os
+
+ # Cloud-Init Selection - ALWAYS ask
+ select_cloud_init
+
+ # Set defaults for other settings
+ VMID=$(get_valid_nextid)
+ FORMAT=""
+ MACHINE=" -machine q35"
+ DISK_CACHE=""
+ DISK_SIZE="32G"
+ HN="unifi-server-os"
+ CPU_TYPE=" -cpu host"
+ CORE_COUNT="2"
+ RAM_SIZE="4096"
+ BRG="vmbr0"
+ MAC="$GEN_MAC"
+ VLAN=""
+ MTU=""
+ START_VM="yes"
+ METHOD="default"
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}Q35 (Modern)${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ echo -e "${CREATING}${BOLD}${DGN}Creating a UniFi OS VM using the above default settings${CL}"
}
function advanced_settings() {
- METHOD="advanced"
- [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
- while true; do
- if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z "$VMID" ]; then
- VMID=$(get_valid_nextid)
- fi
- if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
- echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
- sleep 2
- continue
- fi
- echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
- break
+ METHOD="advanced"
+
+ # OS Selection - ALWAYS ask
+ select_os
+
+ # Cloud-Init Selection - ALWAYS ask
+ select_cloud_init
+
+ [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
+ while true; do
+ if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z "$VMID" ]; then
+ VMID=$(get_valid_nextid)
+ fi
+ if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
+ echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
+ sleep 2
+ continue
+ fi
+ echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
+ break
+ else
+ exit-script
+ fi
+ done
+
+ if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Machine Type" 10 58 2 \
+ "q35" "Q35 (Modern, PCIe, UEFI)" ON \
+ "i440fx" "i440fx (Legacy)" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ "$MACH" = "q35" ]; then
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}Q35 (Modern)${CL}"
+ FORMAT=""
+ MACHINE=" -machine q35"
+ else
+ echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx (Legacy)${CL}"
+ FORMAT=",efitype=4m"
+ MACHINE=""
+ fi
else
- exit-script
+ exit-script
fi
- done
- if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Machine Type" 10 58 2 \
- "q35" "Modern (PCIe, UEFI, default)" ON \
- "i440fx" "Legacy (older compatibility)" OFF \
- 3>&1 1>&2 2>&3); then
- if [ "$MACH" = "q35" ]; then
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}q35${CL}"
- FORMAT=""
- MACHINE=" -machine q35"
+ if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
+ if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
+ DISK_SIZE="${DISK_SIZE}G"
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ else
+ echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
+ exit-script
+ fi
else
- echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
- FORMAT=",efitype=4m"
- MACHINE=""
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
- if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
- DISK_SIZE="${DISK_SIZE}G"
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
- elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
+ if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
+ "0" "None (Default)" ON \
+ "1" "Write Through" OFF \
+ 3>&1 1>&2 2>&3); then
+ if [ $DISK_CACHE = "1" ]; then
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
+ DISK_CACHE="cache=writethrough,"
+ else
+ echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
+ DISK_CACHE=""
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
- exit-script
+ exit-script
fi
- else
- exit-script
- fi
- if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
- "0" "None" OFF \
- "1" "Write Through (Default)" ON \
- 3>&1 1>&2 2>&3); then
- if [ $DISK_CACHE = "1" ]; then
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
- DISK_CACHE="cache=writethrough,"
+ if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 unifi-os-server --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VM_NAME ]; then
+ HN="unifi-os-server"
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ else
+ HN=$(echo ${VM_NAME,,} | tr -d ' ')
+ echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ fi
else
- echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
- DISK_CACHE=""
+ exit-script
fi
- else
- exit-script
- fi
- if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 unifi-os-server --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VM_NAME ]; then
- HN="unifi-os-server"
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose CPU Model" --cancel-button Exit-Script 10 58 2 \
+ "Host" "Host (Faster, recommended)" ON \
+ "KVM64" "KVM64 (Compatibility)" OFF \
+ 3>&1 1>&2 2>&3); then
+ case "$CPU_TYPE1" in
+ Host)
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
+ CPU_TYPE=" -cpu host"
+ ;;
+ *)
+ echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
+ CPU_TYPE=""
+ ;;
+ esac
else
- HN=$(echo ${VM_NAME,,} | tr -d ' ')
- echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose CPU Model" --cancel-button Exit-Script 10 58 2 \
- "KVM64" "Default – safe for migration/compatibility" ON \
- "Host" "Use host CPU features (faster, no migration)" OFF \
- 3>&1 1>&2 2>&3); then
- case "$CPU_TYPE1" in
- Host)
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
- CPU_TYPE=" -cpu host"
- ;;
- *)
- echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
- CPU_TYPE=""
- ;;
- esac
- else
- exit-script
- fi
-
- if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $CORE_COUNT ]; then
- CORE_COUNT="2"
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $CORE_COUNT ]; then
+ CORE_COUNT="2"
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ else
+ echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ fi
else
- echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $RAM_SIZE ]; then
- RAM_SIZE="2048"
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $RAM_SIZE ]; then
+ RAM_SIZE="2048"
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ else
+ echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ fi
else
- echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $BRG ]; then
- BRG="vmbr0"
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $BRG ]; then
+ BRG="vmbr0"
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ else
+ echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ fi
else
- echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MAC1 ]; then
- MAC="$GEN_MAC"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MAC1 ]; then
+ MAC="$GEN_MAC"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
+ else
+ MAC="$MAC1"
+ echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ fi
else
- MAC="$MAC1"
- echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $VLAN1 ]; then
- VLAN1="Default"
- VLAN=""
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $VLAN1 ]; then
+ VLAN1="Default"
+ VLAN=""
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ else
+ VLAN=",tag=$VLAN1"
+ echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ fi
else
- VLAN=",tag=$VLAN1"
- echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
- if [ -z $MTU1 ]; then
- MTU1="Default"
- MTU=""
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
+ if [ -z $MTU1 ]; then
+ MTU1="Default"
+ MTU=""
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ else
+ MTU=",mtu=$MTU1"
+ echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ fi
else
- MTU=",mtu=$MTU1"
- echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
+ exit-script
fi
- else
- exit-script
- fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
- START_VM="yes"
- else
- echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
- START_VM="no"
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
+ START_VM="yes"
+ else
+ echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
+ START_VM="no"
+ fi
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Unifi OS VM?" --no-button Do-Over 10 58); then
- echo -e "${CREATING}${BOLD}${DGN}Creating a Unifi OS VM using the above advanced settings${CL}"
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Unifi OS VM?" --no-button Do-Over 10 58); then
+ echo -e "${CREATING}${BOLD}${DGN}Creating a Unifi OS VM using the above advanced settings${CL}"
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
function start_script() {
- if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
- header_info
- echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
- default_settings
- else
- header_info
- echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
- advanced_settings
- fi
+ if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
+ header_info
+ echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
+ default_settings
+ else
+ header_info
+ echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
+ advanced_settings
+ fi
}
check_root
arch_check
@@ -439,92 +525,315 @@ post_to_api_vm
msg_info "Validating Storage"
while read -r line; do
- 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 "
- OFFSET=2
- if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
- MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
- fi
- STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
+ 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 "
+ OFFSET=2
+ if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
+ MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
+ fi
+ STORAGE_MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
VALID=$(pvesm status -content images | awk 'NR>1')
if [ -z "$VALID" ]; then
- msg_error "Unable to detect a valid storage location."
- exit
+ msg_error "Unable to detect a valid storage location."
+ exit
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
- STORAGE=${STORAGE_MENU[0]}
+ STORAGE=${STORAGE_MENU[0]}
else
- while [ -z "${STORAGE:+x}" ]; do
- #if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null; fi
- printf "\e[?25h"
- STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
- "Which storage pool would you like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \
- 16 $(($MSG_MAX_LENGTH + 23)) 6 \
- "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
- done
+ while [ -z "${STORAGE:+x}" ]; do
+ #if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null; fi
+ printf "\e[?25h"
+ STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
+ "Which storage pool would you like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \
+ 16 $(($MSG_MAX_LENGTH + 23)) 6 \
+ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
+ done
fi
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
-# --- Download Debian Cloud Image ---
-msg_info "Downloading Debian 13 Cloud Image"
-URL="https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.qcow2"
-CACHE_DIR="/var/lib/vz/template/cache"
-CACHE_FILE="$CACHE_DIR/$(basename "$URL")"
-FILE_IMG="/var/lib/vz/template/tmp/${CACHE_FILE##*/%.xz}" # .qcow2
-if [[ ! -s "$CACHE_FILE" ]]; then
- curl -f#SL -o "$CACHE_FILE" "$URL"
- msg_ok "Downloaded ${CL}${BL}$(basename "$CACHE_FILE")${CL}"
-else
- msg_ok "Using cached image ${CL}${BL}$(basename "$CACHE_FILE")${CL}"
+# Fetch latest UniFi OS Server version and download URL
+msg_info "Fetching latest UniFi OS Server version"
+
+# Install jq if not available
+if ! command -v jq &>/dev/null; then
+ msg_info "Installing jq for JSON parsing"
+ apt-get update -qq >/dev/null 2>&1
+ apt-get install -y jq -qq >/dev/null 2>&1
fi
-FILE="debian-13-nocloud-amd64.qcow2"
-msg_ok "Downloaded Debian Cloud Image"
+
+# Download firmware list from Ubiquiti API
+API_URL="https://fw-update.ui.com/api/firmware-latest"
+TEMP_JSON=$(mktemp)
+
+if ! curl -fsSL "$API_URL" -o "$TEMP_JSON"; then
+ rm -f "$TEMP_JSON"
+ msg_error "Failed to fetch data from Ubiquiti API"
+ exit 1
+fi
+
+# Parse JSON to find latest unifi-os-server linux-x64 version
+LATEST=$(jq -r '
+ ._embedded.firmware
+ | map(select(.product == "unifi-os-server"))
+ | map(select(.platform == "linux-x64"))
+ | sort_by(.version_major, .version_minor, .version_patch)
+ | last
+' "$TEMP_JSON")
+
+UOS_VERSION=$(echo "$LATEST" | jq -r '.version' | sed 's/^v//')
+UOS_URL=$(echo "$LATEST" | jq -r '._links.data.href')
+
+# Cleanup temp file
+rm -f "$TEMP_JSON"
+
+if [ -z "$UOS_URL" ] || [ -z "$UOS_VERSION" ]; then
+ msg_error "Failed to parse UniFi OS Server version or download URL"
+ exit 1
+fi
+
+UOS_INSTALLER="unifi-os-server-${UOS_VERSION}.bin"
+msg_ok "Found UniFi OS Server ${UOS_VERSION}"
+
+# --- Download Cloud Image ---
+msg_info "Downloading ${OS_DISPLAY} Cloud Image"
+URL=$(get_image_url)
+sleep 2
+msg_ok "${CL}${BL}${URL}${CL}"
+curl -f#SL -o "$(basename "$URL")" "$URL"
+echo -en "\e[1A\e[0K"
+FILE=$(basename $URL)
+msg_ok "Downloaded ${CL}${BL}${FILE}${CL}"
# --- Inject UniFi Installer ---
if ! command -v virt-customize &>/dev/null; then
- msg_info "Installing libguestfs-tools on host"
- apt-get -qq update >/dev/null
- apt-get -qq install libguestfs-tools -y >/dev/null
- msg_ok "Installed libguestfs-tools"
+ msg_info "Installing libguestfs-tools on host"
+ apt-get -qq update >/dev/null
+ apt-get -qq install libguestfs-tools -y >/dev/null
+ msg_ok "Installed libguestfs-tools"
fi
-msg_info "Injecting UniFi OS Installer into Cloud Image"
-virt-customize -q -a "$FILE" \
- --run-command "echo 'nameserver 1.1.1.1' > /etc/resolv.conf" \
- --install qemu-guest-agent,ca-certificates,curl,lsb-release,podman \
- --run-command "curl -fsSL '${UOS_URL}' -o /root/${UOS_INSTALLER} && chmod +x /root/${UOS_INSTALLER}" \
- --run-command 'mkdir -p /etc/systemd/system/getty@tty1.service.d' \
- --run-command "bash -c 'echo -e \"[Service]\nExecStart=\nExecStart=-/sbin/agetty --autologin root --noclear %I \$TERM\" > /etc/systemd/system/getty@tty1.service.d/override.conf'" \
- >/dev/null
-msg_ok "UniFi OS Installer integrated"
+msg_info "Preparing ${OS_DISPLAY} Qcow2 Disk Image"
+
+# Set DNS for libguestfs appliance environment
+export LIBGUESTFS_BACKEND_SETTINGS=dns=8.8.8.8,1.1.1.1
+
+# Always create first-boot installation script as fallback
+virt-customize -q -a "${FILE}" --run-command "cat > /root/install-unifi.sh << 'INSTALLEOF'
+#!/bin/bash
+set -e
+# Log output to file
+exec > /var/log/install-unifi.log 2>&1
+echo \"[\$(date)] Starting UniFi OS installation on first boot\"
+
+# Check if already installed
+if [ -f /root/.unifi-installed ]; then
+ echo \"[\$(date)] UniFi OS already installed, exiting\"
+ exit 0
+fi
+
+# Wait for cloud-init to complete if present
+if command -v cloud-init >/dev/null 2>&1; then
+ echo \"[\$(date)] Waiting for cloud-init to complete...\"
+ cloud-init status --wait 2>/dev/null || true
+fi
+
+# Wait for network to be fully available
+echo \"[\$(date)] Waiting for network connectivity...\"
+for i in {1..60}; do
+ if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
+ echo \"[\$(date)] Network is available\"
+ break
+ fi
+ echo \"[\$(date)] Waiting for network... attempt \$i/60\"
+ sleep 2
+done
+
+# Configure DNS
+echo \"[\$(date)] Configuring DNS\"
+mkdir -p /etc/systemd/resolved.conf.d
+cat > /etc/systemd/resolved.conf.d/dns.conf << DNSEOF
+[Resolve]
+DNS=8.8.8.8 1.1.1.1
+FallbackDNS=8.8.4.4 1.0.0.1
+DNSEOF
+systemctl restart systemd-resolved 2>/dev/null || true
+sleep 3
+
+# Wait for apt locks to be released
+echo \"[\$(date)] Waiting for package manager to be ready...\"
+for i in {1..30}; do
+ if ! fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 && ! fuser /var/lib/apt/lists/lock >/dev/null 2>&1; then
+ echo \"[\$(date)] Package manager is ready\"
+ break
+ fi
+ echo \"[\$(date)] Waiting for other package managers to finish... attempt \$i/30\"
+ sleep 5
+done
+
+# Update package lists
+echo \"[\$(date)] Updating package lists\"
+apt-get update
+
+# Install base packages
+echo \"[\$(date)] Installing base packages (this may take several minutes)\"
+if DEBIAN_FRONTEND=noninteractive apt-get install -y qemu-guest-agent curl wget ca-certificates podman uidmap slirp4netns iptables; then
+ echo \"[\$(date)] ✓ Base packages installed successfully\"
+else
+ echo \"[\$(date)] ✗ ERROR: Failed to install packages\"
+ exit 1
+fi
+
+# Start QEMU Guest Agent
+echo \"[\$(date)] Starting QEMU Guest Agent\"
+systemctl enable qemu-guest-agent 2>/dev/null || true
+systemctl start qemu-guest-agent 2>/dev/null || true
+
+# Configure and start Podman
+echo \"[\$(date)] Configuring Podman\"
+loginctl enable-linger root 2>/dev/null || true
+
+# Verify Podman is working
+echo \"[\$(date)] Verifying Podman installation\"
+if ! podman --version >/dev/null 2>&1; then
+ echo \"[\$(date)] ✗ ERROR: Podman not working after installation\"
+ exit 1
+fi
+echo \"[\$(date)] ✓ Podman $(podman --version)\"
+
+# Download UniFi OS installer
+echo \"[\$(date)] Downloading UniFi OS Server ${UOS_VERSION}\"
+if ! curl -fsSL '${UOS_URL}' -o /root/${UOS_INSTALLER}; then
+ echo \"[\$(date)] ✗ ERROR: Failed to download UniFi OS installer\"
+ exit 1
+fi
+chmod +x /root/${UOS_INSTALLER}
+echo \"[\$(date)] ✓ Downloaded UniFi OS installer\"
+
+# Run UniFi OS installer
+echo \"[\$(date)] Running UniFi OS installer (this will take 2-5 minutes)\"
+if /root/${UOS_INSTALLER} install; then
+ echo \"[\$(date)] ✓ UniFi OS installer completed successfully\"
+else
+ EXIT_CODE=\$?
+ echo \"[\$(date)] ⚠ Installer exited with code \${EXIT_CODE}\"
+fi
+
+# Wait for installation to settle
+sleep 10
+
+# Start UniFi OS Server
+if command -v uosserver >/dev/null 2>&1; then
+ echo \"[\$(date)] ✓ uosserver command found\"
+ if id -u uosserver >/dev/null 2>&1; then
+ echo \"[\$(date)] Starting UniFi OS Server as uosserver user\"
+ su - uosserver -c 'uosserver start' 2>&1 || true
+ else
+ echo \"[\$(date)] Starting UniFi OS Server as root\"
+ uosserver start 2>&1 || true
+ fi
+ sleep 3
+ IP=\$(hostname -I | awk '{print \$1}')
+ echo \"[\$(date)] ✓ UniFi OS Server should be accessible at: https://\${IP}:11443\"
+else
+ echo \"[\$(date)] ✗ ERROR: uosserver command not found after installation\"
+ echo \"[\$(date)] Checking installation artifacts...\"
+ ls -la /root/ | grep -i unifi || true
+ which uosserver 2>&1 || true
+fi
+
+# Create completion flag
+echo \"[\$(date)] Installation completed\"
+touch /root/.unifi-installed
+INSTALLEOF" >/dev/null
+
+virt-customize -q -a "${FILE}" --run-command "chmod +x /root/install-unifi.sh" >/dev/null
+
+# Create systemd service
+virt-customize -q -a "${FILE}" --run-command "cat > /etc/systemd/system/unifi-firstboot.service << 'SVCEOF'
+[Unit]
+Description=UniFi OS First Boot Setup
+After=network-online.target
+Wants=network-online.target
+ConditionPathExists=!/root/.unifi-installed
+
+[Service]
+Type=oneshot
+ExecStart=/root/install-unifi.sh
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+SVCEOF" >/dev/null
+
+virt-customize -q -a "${FILE}" --run-command "systemctl enable unifi-firstboot.service" >/dev/null
+
+# Try to install base packages during image customization (faster startup if it works)
+UNIFI_PREINSTALLED="no"
+
+msg_info "Pre-installing base packages (qemu-guest-agent, podman, curl)"
+if virt-customize -a "${FILE}" --install qemu-guest-agent,curl,ca-certificates,podman,uidmap,slirp4netns >/dev/null 2>&1; then
+ msg_ok "Pre-installed base packages (UniFi OS will install on first boot)"
+else
+ msg_info "Pre-installation not possible, packages will install on first boot"
+fi
+
+# Add auto-login if Cloud-Init is disabled
+if [ "$USE_CLOUD_INIT" != "yes" ]; then
+ virt-customize -q -a "${FILE}" \
+ --run-command 'mkdir -p /etc/systemd/system/getty@tty1.service.d' \
+ --run-command "bash -c 'echo -e \"[Service]\nExecStart=\nExecStart=-/sbin/agetty --autologin root --noclear %I \\\$TERM\" > /etc/systemd/system/getty@tty1.service.d/override.conf'" 2>/dev/null
+fi
+
+msg_ok "UniFi OS Server will be installed on first boot"
+
+# Expand root partition to use full disk space
+msg_info "Expanding disk image to ${DISK_SIZE}"
+qemu-img create -f qcow2 expanded.qcow2 ${DISK_SIZE} >/dev/null 2>&1
+
+# Detect partition device (sda1 for Ubuntu, vda1 for Debian)
+PARTITION_DEV=$(virt-filesystems --long -h --all -a "${FILE}" | grep -oP '/dev/\K(s|v)da1' | head -1)
+if [ -z "$PARTITION_DEV" ]; then
+ PARTITION_DEV="sda1" # fallback
+fi
+
+virt-resize --quiet --expand /dev/${PARTITION_DEV} ${FILE} expanded.qcow2 >/dev/null 2>&1
+mv expanded.qcow2 ${FILE}
+msg_ok "Expanded disk image to ${DISK_SIZE}"
msg_info "Creating UniFi OS VM"
-qm create "$VMID" -agent 1 $MACHINE -tablet 0 -localtime 1 -bios ovmf \
- $CPU_TYPE -cores "$CORE_COUNT" -memory "$RAM_SIZE" \
- -name "$HN" -tags community-script \
- -net0 virtio,bridge="$BRG",macaddr="$MAC""$VLAN""$MTU" \
- -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
+qm create "$VMID" -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf \
+ ${CPU_TYPE} -cores "$CORE_COUNT" -memory "$RAM_SIZE" \
+ -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" "vm-$VMID-disk-0" 4M >/dev/null
IMPORT_OUT="$(qm importdisk "$VMID" "$FILE" "$STORAGE" --format qcow2 2>&1 || true)"
DISK_REF="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p")"
if [[ -z "$DISK_REF" ]]; then
- DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$1 ~ ("vm-"id"-disk-") {print $1}' | sort | tail -n1)"
+ DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$1 ~ ("vm-"id"-disk-") {print $1}' | sort | tail -n1)"
fi
qm set "$VMID" \
- -efidisk0 "${STORAGE}:0${FORMAT},size=4M" \
- -scsi0 "${DISK_REF},size=${DISK_SIZE}" \
- -boot order=scsi0 -serial0 socket >/dev/null
+ -efidisk0 "${STORAGE}:0${FORMAT},size=4M" \
+ -scsi0 "${DISK_REF},${DISK_CACHE}size=${DISK_SIZE}" \
+ -boot order=scsi0 -serial0 socket >/dev/null
qm resize "$VMID" scsi0 "$DISK_SIZE" >/dev/null
qm set "$VMID" --agent enabled=1 >/dev/null
+# Add Cloud-Init drive if enabled
+if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ msg_info "Configuring Cloud-Init"
+ setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" >/dev/null 2>&1
+ msg_ok "Cloud-Init configured"
+fi
+
DESCRIPTION=$(
- cat <
@@ -555,11 +864,77 @@ EOF
)
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
-msg_ok "Created a Unifi OS VM ${CL}${BL}(${HN})"
+msg_ok "Created a UniFi OS VM ${CL}${BL}(${HN})"
+msg_info "Operating System: ${OS_DISPLAY}"
+msg_info "Cloud-Init: ${USE_CLOUD_INIT}"
+
if [ "$START_VM" == "yes" ]; then
- msg_info "Starting Unifi OS VM"
- qm start $VMID
- msg_ok "Started Unifi OS VM"
+ msg_info "Starting UniFi OS VM"
+ qm start $VMID
+ msg_ok "Started UniFi OS VM"
+
+ msg_info "Waiting for VM to boot (30 seconds)"
+ sleep 30
+ msg_ok "VM should be booting now"
+
+ msg_info "Detecting VM IP address (may take up to 60 seconds)"
+ VM_IP=""
+ for i in {1..30}; do
+ VM_IP=$(qm guest cmd $VMID network-get-interfaces 2>/dev/null | jq -r '.[1]["ip-addresses"][]? | select(.["ip-address-type"] == "ipv4") | .["ip-address"]' 2>/dev/null | grep -v "127.0.0.1" | head -1 || echo "")
+
+ if [ -n "$VM_IP" ]; then
+ msg_ok "VM IP Address detected: ${VM_IP}"
+ break
+ fi
+ sleep 2
+ done
+
+ if [ -n "$VM_IP" ]; then
+ msg_info "Waiting for UniFi OS installation to complete (this takes 3-5 minutes)"
+
+ WAIT_COUNT=0
+ MAX_WAIT=300 # 5 minutes max
+ PORT_OPEN=0
+ LAST_MSG_TIME=0
+
+ while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
+ if timeout 2 bash -c ">/dev/tcp/${VM_IP}/11443" 2>/dev/null; then
+ PORT_OPEN=1
+ msg_ok "UniFi OS Server installation completed successfully"
+ break
+ fi
+
+ sleep 5
+ WAIT_COUNT=$((WAIT_COUNT + 5))
+
+ # Update message every 20 seconds
+ if [ $((WAIT_COUNT - LAST_MSG_TIME)) -ge 20 ]; then
+ echo -e "${BFR}${TAB}${YW}${HOLD}Installation in progress... ${WAIT_COUNT}s elapsed (check: tail -f /var/log/install-unifi.log in VM)${CL}"
+ LAST_MSG_TIME=$WAIT_COUNT
+ fi
+ done
+
+ if [ $PORT_OPEN -eq 1 ]; then
+ echo -e "\n${TAB}${GATEWAY}${BOLD}${GN}✓ UniFi OS Server is ready!${CL}"
+ echo -e "${TAB}${GATEWAY}${BOLD}${GN}✓ Access at: ${BGN}https://${VM_IP}:11443${CL}\n"
+ else
+ msg_ok "VM is running, but installation is still in progress"
+ echo -e "${TAB}${INFO}${YW}Installation takes 3-5 minutes after first boot${CL}"
+ echo -e "${TAB}${INFO}${YW}Check progress: ${BL}qm guest exec ${VMID} -- tail -f /var/log/install-unifi.log${CL}"
+ echo -e "${TAB}${INFO}${YW}Or SSH to: ${BL}${VM_IP}${CL} and run: ${BL}tail -f /var/log/install-unifi.log${CL}"
+ echo -e "${TAB}${INFO}${YW}Access will be at: ${BGN}https://${VM_IP}:11443${CL}"
+ fi
+ else
+ msg_ok "VM is running (ID: ${VMID})"
+ echo -e "${TAB}${INFO}${YW}Could not auto-detect IP address${CL}"
+ echo -e "${TAB}${INFO}${YW}Access VM console in Proxmox to check status${CL}"
+ echo -e "${TAB}${INFO}${YW}Or check installation log: ${BL}tail -f /var/log/install-unifi.log${CL}"
+ fi
fi
+
+if [ "$USE_CLOUD_INIT" = "yes" ]; then
+ display_cloud_init_info "$VMID" "$HN"
+fi
+
post_update_to_api "done" "none"
msg_ok "Completed Successfully!\n"