diff --git a/ct/alpine-valkey.sh b/ct/alpine-valkey.sh new file mode 100644 index 000000000..1f7f96298 --- /dev/null +++ b/ct/alpine-valkey.sh @@ -0,0 +1,72 @@ +#!/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: pshankinclarke (lazarillo) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://valkey.io/ + +APP="Alpine-Valkey" +var_tags="${var_tags:-alpine;database}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-256}" +var_disk="${var_disk:-1}" +var_os="${var_os:-alpine}" +var_version="${var_version:-3.22}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + if ! apk -e info newt >/dev/null 2>&1; then + apk add -q newt + fi + LXCIP=$(ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) + while true; do + CHOICE=$( + whiptail --backtitle "Proxmox VE Helper Scripts" --title "Valkey Management" --menu "Select option" 11 58 3 \ + "1" "Update Valkey" \ + "2" "Allow 0.0.0.0 for listening" \ + "3" "Allow only ${LXCIP} for listening" 3>&2 2>&1 1>&3 + ) + exit_status=$? + if [ $exit_status == 1 ]; then + clear + exit-script + fi + header_info + case $CHOICE in + 1) + msg_info "Updating Valkey" + apk update && apk upgrade valkey + rc-service valkey restart + msg_ok "Updated successfully!" + exit + ;; + 2) + msg_info "Setting Valkey to listen on all interfaces" + sed -i 's/^bind .*/bind 0.0.0.0/' /etc/valkey/valkey.conf + rc-service valkey restart + msg_ok "Valkey now listens on all interfaces!" + exit + ;; + 3) + msg_info "Setting Valkey to listen only on ${LXCIP}" + sed -i "s/^bind .*/bind ${LXCIP}/" /etc/valkey/valkey.conf + rc-service valkey restart + msg_ok "Valkey now listens only on ${LXCIP}!" + exit + ;; + esac + done +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${APP} should be reachable on port 6379. + ${BL}valkey-cli -h ${IP} -p 6379${CL} \n" diff --git a/ct/heimdall-dashboard.sh b/ct/heimdall-dashboard.sh new file mode 100644 index 000000000..c4fc9349a --- /dev/null +++ b/ct/heimdall-dashboard.sh @@ -0,0 +1,80 @@ +#!/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://heimdall.site/ + +APP="Heimdall-Dashboard" +var_tags="${var_tags:-dashboard}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-512}" +var_disk="${var_disk:-2}" +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/Heimdall ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "Heimdall" "linuxserver/Heimdall"; then + msg_info "Stopping Service" + systemctl stop heimdall + sleep 1 + msg_ok "Stopped Service" + + msg_info "Backing up Data" + cp -R /opt/Heimdall/database database-backup + cp -R /opt/Heimdall/public public-backup + sleep 1 + msg_ok "Backed up Data" + + setup_composer + fetch_and_deploy_gh_release "Heimdall" "linuxserver/Heimdall" "tarball" + + msg_info "Updating Heimdall-Dashboard" + cd /opt/Heimdall + export COMPOSER_ALLOW_SUPERUSER=1 + $STD composer dump-autoload + msg_ok "Updated Heimdall-Dashboard" + + msg_info "Restoring Data" + cd ~ + cp -R database-backup/* /opt/Heimdall/database + cp -R public-backup/* /opt/Heimdall/public + sleep 1 + msg_ok "Restored Data" + + msg_info "Cleaning Up" + rm -rf {public-backup,database-backup} + sleep 1 + msg_ok "Cleaned Up" + + msg_info "Starting Service" + systemctl start heimdall.service + sleep 2 + 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}:7990${CL}" diff --git a/ct/homarr.sh b/ct/homarr.sh index 5b69526a0..151823f0b 100644 --- a/ct/homarr.sh +++ b/ct/homarr.sh @@ -29,7 +29,7 @@ function update_script() { exit fi - if check_for_gh_release "homarr" "homarr-labs/homarr"; then + if check_for_gh_release "homarr" "Meierschlumpf/homarr"; then msg_info "Stopping Services (Patience)" systemctl stop homarr systemctl stop redis-server @@ -38,7 +38,12 @@ function update_script() { if ! { grep -q '^REDIS_IS_EXTERNAL=' /opt/homarr/.env 2>/dev/null || grep -q '^REDIS_IS_EXTERNAL=' /opt/homarr.env 2>/dev/null; }; then msg_info "Fixing old structure" + systemctl disable -q --now nginx $STD apt install -y musl-dev + # Error: ec 15 21:05:23 homarr run.sh[330]: ⨯ Error: libc.musl-x86_64.so.1: cannot open shared object file: No such file or di> + # Dec 15 21:05:23 homarr run.sh[330]: at ignore-listed frames { + # Dec 15 21:05:23 homarr run.sh[330]: code: 'ERR_DLOPEN_FAILED' + # Dec 15 21:05:23 homarr run.sh[330]: } ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1 cp /opt/homarr/.env /opt/homarr.env echo "REDIS_IS_EXTERNAL='true'" >> /opt/homarr.env @@ -51,7 +56,6 @@ function update_script() { [Service] ReadWritePaths=-/appdata/redis -/var/lib/redis -/var/log/redis -/var/run/redis -/etc/redis EOF - # TODO: change in json systemctl daemon-reload rm /opt/run_homarr.sh msg_ok "Fixed old structure" @@ -62,9 +66,9 @@ EOF $STD apt upgrade nodejs -y msg_ok "Updated Nodejs" - NODE_VERSION=$(curl -s https://raw.githubusercontent.com/homarr-labs/homarr/dev/package.json | jq -r '.engines.node | split(">=")[1] | split(".")[0]') + NODE_VERSION=$(curl -s https://raw.githubusercontent.com/Meierschlumpf/homarr/dev/package.json | jq -r '.engines.node | split(">=")[1] | split(".")[0]') setup_nodejs - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "homarr" "homarr-labs/homarr" "prebuild" "latest" "/opt/homarr" "build-amd64.tar.gz" + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "homarr" "Meierschlumpf/homarr" "prebuild" "latest" "/opt/homarr" "source-amd64.tar.gz" msg_info "Updating Homarr" cp /opt/homarr/redis.conf /etc/redis/redis.conf diff --git a/ct/koel.sh b/ct/koel.sh deleted file mode 100644 index aa902d9d2..000000000 --- a/ct/koel.sh +++ /dev/null @@ -1,82 +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/ProxmoxVED/raw/main/LICENSE -# Source: https://koel.dev/ - -APP="Koel" -var_tags="${var_tags:-music;streaming}" -var_cpu="${var_cpu:-2}" -var_ram="${var_ram:-2048}" -var_disk="${var_disk:-8}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - - if [[ ! -d /opt/koel ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - if check_for_gh_release "koel" "koel/koel"; then - msg_info "Stopping Services" - systemctl stop nginx php8.4-fpm - msg_ok "Stopped Services" - - msg_info "Creating Backup" - mkdir -p /tmp/koel_backup - cp /opt/koel/.env /tmp/koel_backup/ - cp -r /opt/koel/storage /tmp/koel_backup/ 2>/dev/null || true - cp -r /opt/koel/public/img /tmp/koel_backup/ 2>/dev/null || true - msg_ok "Created Backup" - - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "koel" "koel/koel" "prebuild" "latest" "/opt/koel" "koel-*.tar.gz" - - msg_info "Restoring Data" - cp /tmp/koel_backup/.env /opt/koel/ - cp -r /tmp/koel_backup/storage/* /opt/koel/storage/ 2>/dev/null || true - cp -r /tmp/koel_backup/img/* /opt/koel/public/img/ 2>/dev/null || true - rm -rf /tmp/koel_backup - msg_ok "Restored Data" - - msg_info "Running Migrations" - cd /opt/koel - export COMPOSER_ALLOW_SUPERUSER=1 - $STD composer install --no-interaction --no-dev --optimize-autoloader - $STD php artisan migrate --force - $STD php artisan config:clear - $STD php artisan cache:clear - $STD php artisan view:clear - $STD php artisan koel:init --no-assets --no-interaction - chown -R www-data:www-data /opt/koel - chmod -R 775 /opt/koel/storage - msg_ok "Ran Migrations" - - msg_info "Starting Services" - systemctl start php8.4-fpm nginx - msg_ok "Started Services" - - 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/linkwarden.sh b/ct/linkwarden.sh new file mode 100644 index 000000000..1d8158e2c --- /dev/null +++ b/ct/linkwarden.sh @@ -0,0 +1,75 @@ +#!/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://linkwarden.app/ + +APP="Linkwarden" +var_tags="${var_tags:-bookmark}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-4096}" +var_disk="${var_disk:-12}" +var_os="${var_os:-ubuntu}" +var_version="${var_version:-24.04}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -d /opt/linkwarden ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + if check_for_gh_release "linkwarden" "linkwarden/linkwarden"; then + NODE_VERSION="22" NODE_MODULE="yarn@latest" setup_nodejs + msg_info "Stopping Service" + systemctl stop linkwarden + msg_ok "Stopped Service" + + RUST_CRATES="monolith" setup_rust + + msg_info "Backing up data" + mv /opt/linkwarden/.env /opt/.env + [ -d /opt/linkwarden/data ] && mv /opt/linkwarden/data /opt/data.bak + rm -rf /opt/linkwarden + msg_ok "Backed up data" + + fetch_and_deploy_gh_release "linkwarden" "linkwarden/linkwarden" + + msg_info "Updating ${APP}" + cd /opt/linkwarden + $STD yarn + $STD npx playwright install-deps + $STD yarn playwright install + mv /opt/.env /opt/linkwarden/.env + $STD yarn prisma:generate + $STD yarn web:build + $STD yarn prisma:deploy + [ -d /opt/data.bak ] && mv /opt/data.bak /opt/linkwarden/data + rm -rf ~/.cargo/registry ~/.cargo/git ~/.cargo/.package-cache + rm -rf /root/.cache/yarn + rm -rf /opt/linkwarden/.next/cache + msg_ok "Updated ${APP}" + + msg_info "Starting Service" + systemctl start linkwarden + 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/opencloud.sh b/ct/opencloud.sh index 78aadba47..37396bc75 100644 --- a/ct/opencloud.sh +++ b/ct/opencloud.sh @@ -29,7 +29,7 @@ function update_script() { exit fi - RELEASE="v4.0.0" + RELEASE="v4.1.0" if check_for_gh_release "opencloud" "opencloud-eu/opencloud" "${RELEASE}"; then msg_info "Stopping services" systemctl stop opencloud opencloud-wopi @@ -37,7 +37,7 @@ function update_script() { msg_info "Updating packages" $STD apt-get update - $STD apt-get dist-upgrade + $STD apt-get dist-upgrade -y msg_ok "Updated packages" CLEAN_INSTALL=1 fetch_and_deploy_gh_release "opencloud" "opencloud-eu/opencloud" "singlefile" "${RELEASE}" "/usr/bin" "opencloud-*-linux-amd64" diff --git a/ct/papra.sh b/ct/papra.sh new file mode 100644 index 000000000..c872bb0cb --- /dev/null +++ b/ct/papra.sh @@ -0,0 +1,48 @@ +#!/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: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/CorentinTh/papra + +APP="Papra" +var_tags="${var_tags:-document-management}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-2048}" +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/papra ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + msg_info "Updating $APP LXC" + systemctl stop papra + cd /opt/papra + git fetch + git pull + $STD pnpm install --frozen-lockfile + $STD pnpm --filter "@papra/app-client..." run build + $STD pnpm --filter "@papra/app-server..." run build + systemctl start papra + msg_ok "Updated $APP LXC" + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!" +msg_custom "🚀" "${GN}" "${APP} setup has been successfully initialized!" diff --git a/ct/piler.sh b/ct/piler.sh new file mode 100644 index 000000000..e592f2e3f --- /dev/null +++ b/ct/piler.sh @@ -0,0 +1,88 @@ +#!/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/ProxmoxVED/raw/main/LICENSE +# Source: https://www.mailpiler.org/ + +APP="Piler" +var_tags="${var_tags:-email;archive;smtp}" +var_cpu="${var_cpu:-4}" +var_ram="${var_ram:-4096}" +var_disk="${var_disk:-20}" +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/piler/piler.conf ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + RELEASE_NEW=$(curl -fsSL https://www.mailpiler.org/download.php | grep -oP 'piler-\K[0-9]+\.[0-9]+\.[0-9]+' | head -1) + RELEASE_OLD=$(pilerd -v 2>/dev/null | grep -oP 'version \K[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown") + + if [[ "${RELEASE_NEW}" != "${RELEASE_OLD}" ]]; then + msg_info "Stopping Piler Services" + $STD systemctl stop piler + $STD systemctl stop manticore + msg_ok "Stopped Piler Services" + + msg_info "Backing up Configuration" + cp /etc/piler/piler.conf /tmp/piler.conf.bak + msg_ok "Backed up Configuration" + + msg_info "Updating to v${RELEASE_NEW}" + cd /tmp + curl -fsSL "https://bitbucket.org/jsuto/piler/downloads/piler-${RELEASE_NEW}.tar.gz" -o piler.tar.gz + tar -xzf piler.tar.gz + cd "piler-${RELEASE_NEW}" + + $STD ./configure \ + --localstatedir=/var \ + --with-database=mysql \ + --sysconfdir=/etc/piler \ + --enable-memcached + + $STD make + $STD make install + $STD ldconfig + + cd /tmp && rm -rf "piler-${RELEASE_NEW}" piler.tar.gz + msg_ok "Updated to v${RELEASE_NEW}" + + msg_info "Restoring Configuration" + cp /tmp/piler.conf.bak /etc/piler/piler.conf + rm -f /tmp/piler.conf.bak + chown piler:piler /etc/piler/piler.conf + msg_ok "Restored Configuration" + + msg_info "Starting Piler Services" + $STD systemctl start manticore + $STD systemctl start piler + msg_ok "Started Piler Services" + msg_ok "Updated Successfully to v${RELEASE_NEW}" + else + msg_ok "No update available (current: v${RELEASE_OLD})" + 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/romm.sh b/ct/romm.sh index 129da9a34..f6d4119ca 100644 --- a/ct/romm.sh +++ b/ct/romm.sh @@ -2,6 +2,7 @@ source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) # Copyright (c) 2021-2025 community-scripts ORG # Author: MickLesk (CanbiZ) +# Co-author: AlphaLawless # License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE # Source: https://romm.app @@ -10,10 +11,9 @@ var_tags="${var_tags:-emulation}" var_cpu="${var_cpu:-2}" var_ram="${var_ram:-4096}" var_disk="${var_disk:-20}" -var_os="${var_os:-ubuntu}" -var_version="${var_version:-24.04}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" var_unprivileged="${var_unprivileged:-1}" -var_fuse="${var_fuse:-1}" header_info "$APP" variables @@ -30,35 +30,44 @@ function update_script() { exit fi - msg_info "Stopping $APP" - systemctl stop romm - systemctl stop nginx - msg_ok "Stopped $APP" + if check_for_gh_release "romm" "rommapp/romm"; then + msg_info "Stopping ${APP} services" + systemctl stop romm-backend romm-worker romm-scheduler romm-watcher + msg_ok "Stopped ${APP} services" - msg_info "Updating $APP" - cd /opt/romm/app - git pull + msg_info "Backing up configuration" + cp /opt/romm/.env /opt/romm/.env.backup + msg_ok "Backed up configuration" - # Update backend - cd /opt/romm/app - source /opt/romm/venv/bin/activate - pip install --upgrade pip - pip install poetry - poetry install + msg_info "Updating ${APP}" + fetch_and_deploy_gh_release "romm" "rommapp/romm" "tarball" "latest" "/opt/romm" - # Update frontend - cd /opt/romm/app/frontend - npm install - npm run build + cp /opt/romm/.env.backup /opt/romm/.env - echo "Updated on $(date)" >/opt/romm/version.txt - msg_ok "Updated $APP" + cd /opt/romm + $STD uv sync --all-extras - msg_info "Starting $APP" - systemctl start romm - systemctl start nginx - msg_ok "Started $APP" - msg_ok "Update Successful" + cd /opt/romm/backend + $STD uv run alembic upgrade head + + cd /opt/romm/frontend + $STD npm install + $STD npm run build + + # Merge static assets into dist folder + cp -rf /opt/romm/frontend/assets/* /opt/romm/frontend/dist/assets/ + + mkdir -p /opt/romm/frontend/dist/assets/romm + ln -sfn /var/lib/romm/resources /opt/romm/frontend/dist/assets/romm/resources + ln -sfn /var/lib/romm/assets /opt/romm/frontend/dist/assets/romm/assets + msg_ok "Updated ${APP}" + + msg_info "Starting ${APP} services" + systemctl start romm-backend romm-worker romm-scheduler romm-watcher + msg_ok "Started ${APP} services" + + msg_ok "Update Successful" + fi exit } @@ -69,4 +78,4 @@ 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}:8080${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}" diff --git a/ct/rustypaste.sh b/ct/rustypaste.sh new file mode 100644 index 000000000..527a7b6bd --- /dev/null +++ b/ct/rustypaste.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2025 community-scripts ORG +# Author: GoldenSpringness +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://github.com/orhun/rustypaste + +APP="rustypaste" +var_tags="${var_tags:-pastebin;storage}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-512}" +var_disk="${var_disk:-20}" +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/rustypaste/target/release/rustypaste" ]]; then + msg_error "No rustypaste Installation Found!" + exit + fi + + if check_for_gh_release "rustypaste" "orhun/rustypaste"; then + msg_info "Stopping rustypaste" + systemctl stop rustypaste + msg_ok "Stopped rustypaste" + + msg_info "Creating Backup" + tar -czf "/opt/rustypaste_backup_$(date +%F).tar.gz" "/opt/rustypaste/upload" + msg_ok "Backup Created" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "rustypaste" "orhun/rustypaste" "tarball" "latest" "/opt/rustypaste" + + msg_info "Updating rustypaste" + cd /opt/rustypaste + sed -i 's|^address = ".*"|address = "0.0.0.0:8000"|' config.toml + $STD cargo build --locked --release + msg_ok "Updated rustypaste" + + msg_info "Starting rustypaste" + systemctl start rustypaste + msg_ok "Started rustypaste" + msg_ok "Update Successful" + fi + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${CREATING}${GN}rustypaste 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/sportarr.sh b/ct/sportarr.sh new file mode 100644 index 000000000..6d11d4aef --- /dev/null +++ b/ct/sportarr.sh @@ -0,0 +1,53 @@ +#!/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/Sportarr/Sportarr + +APP="Sportarr" +var_tags="${var_tags:-arr}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-2048}" +var_disk="${var_disk:-8}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -d /opt/sportarr ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "sportarr" "Sportarr/Sportarr"; then + msg_info "Stopping Sportarr Service" + systemctl stop sportarr + msg_ok "Stopped Sportarr Service" + + fetch_and_deploy_gh_release "sportarr" "Sportarr/Sportarr" "prebuild" "latest" "/opt/sportarr" "Sportarr-linux-x64-*.tar.gz" + + msg_info "Starting Sportarr Service" + systemctl start sportarr + msg_ok "Started Sportarr 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}:1867${CL}" diff --git a/frontend/public/json/koel.json b/frontend/public/json/koel.json deleted file mode 100644 index 62530a284..000000000 --- a/frontend/public/json/koel.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "Koel", - "slug": "koel", - "categories": [ - 13 - ], - "date_created": "2025-12-10", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 80, - "documentation": "https://docs.koel.dev/", - "config_path": "/opt/koel/.env", - "website": "https://koel.dev/", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/koel-light.webp", - "description": "Koel is a simple web-based personal audio streaming service written in Vue and Laravel. It supports multiple users, audio visualization, smart playlists, YouTube integration, and Last.fm scrobbling.", - "install_methods": [ - { - "type": "default", - "script": "ct/koel.sh", - "resources": { - "cpu": 2, - "ram": 2048, - "hdd": 8, - "os": "Debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": "admin@koel.dev", - "password": "KoelIsCool" - }, - "notes": [ - { - "text": "Media files should be placed in /opt/koel_media", - "type": "info" - }, - { - "text": "Database credentials are stored in ~/koel.creds", - "type": "info" - }, - { - "text": "Music library is scanned hourly via cron job", - "type": "info" - } - ] -} diff --git a/frontend/public/json/nextexplorer.json b/frontend/public/json/nextexplorer.json index caa88b336..4e753b3cd 100644 --- a/frontend/public/json/nextexplorer.json +++ b/frontend/public/json/nextexplorer.json @@ -1,37 +1,41 @@ { - "name": "nextExplorer", - "slug": "nextexplorer", - "categories": [ - 11, - 12 - ], - "date_created": "2025-12-11", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 3000, - "documentation": "https://explorer.nxz.ai", - "website": "https://github.com/vikramsoni2/nextExplorer", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/nextexplorer.webp", - "config_path": "/etc/nextExplorer/.env", - "description": "", - "install_methods": [ - { - "type": "default", - "script": "ct/nextexplorer.sh", - "resources": { - "cpu": 2, - "ram": 3072, - "hdd": 8, - "os": "Debian", - "version": "13" - } - } - ], - "notes": [ - { - "text": "Bind mount your volume(s) in the `/mnt` directory", - "type": "info" - } - ] + "name": "nextExplorer", + "slug": "nextexplorer", + "categories": [ + 11, + 12 + ], + "date_created": "2025-12-11", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 3000, + "documentation": "https://explorer.nxz.ai", + "website": "https://github.com/vikramsoni2/nextExplorer", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/nextexplorer.webp", + "config_path": "/etc/nextExplorer/.env", + "description": "Self-hosted file access for teams, homelabs, and agencies with a refined interface.", + "install_methods": [ + { + "type": "default", + "script": "ct/nextexplorer.sh", + "resources": { + "cpu": 2, + "ram": 3072, + "hdd": 8, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Bind mount your volume(s) in the `/mnt` directory", + "type": "info" + } + ] } diff --git a/frontend/public/json/papra.json b/frontend/public/json/papra.json new file mode 100644 index 000000000..f3f613b2a --- /dev/null +++ b/frontend/public/json/papra.json @@ -0,0 +1,56 @@ +{ + "name": "Papra", + "slug": "papra", + "categories": [ + 12 + ], + "date_created": "2025-12-30", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 1221, + "documentation": "https://github.com/CorentinTh/papra", + "website": "https://github.com/CorentinTh/papra", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/papra.webp", + "config_path": "/opt/papra/.env", + "description": "Papra is a modern, self-hosted document management system with full-text search, OCR support, and automatic document processing. Built with Node.js and featuring a clean web interface for organizing and managing your documents.", + "install_methods": [ + { + "type": "default", + "script": "ct/papra.sh", + "resources": { + "cpu": 2, + "ram": 2048, + "hdd": 10, + "os": "debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "First visit will prompt you to create an account", + "type": "info" + }, + { + "text": "Tesseract OCR is pre-installed for all languages", + "type": "info" + }, + { + "text": "Documents are stored in /opt/papra/app-data/documents", + "type": "info" + }, + { + "text": "Ingestion folder available at /opt/papra/ingestion for automatic document import", + "type": "info" + }, + { + "text": "Email functionality runs in dry-run mode by default", + "type": "warning" + } + ] +} diff --git a/frontend/public/json/pihole-exporter.json b/frontend/public/json/pihole-exporter.json deleted file mode 100644 index c27945d03..000000000 --- a/frontend/public/json/pihole-exporter.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "Pi-Hole Exporter", - "slug": "pihole-exporter", - "categories": [ - 9 - ], - "date_created": "2025-12-08", - "type": "addon", - "updateable": true, - "privileged": false, - "interface_port": 9617, - "documentation": "https://github.com/eko/pihole-exporter", - "website": "https://github.com/eko/pihole-exporter", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/pi-hole.webp", - "config_path": "/opt/pihole-exporter.env", - "description": "A Prometheus exporter for PI-Hole's Raspberry PI ad blocker", - "install_methods": [ - { - "type": "default", - "script": "tools/addon/pihole-exporter.sh", - "resources": { - "cpu": null, - "ram": null, - "hdd": null, - "os": null, - "version": null - } - }, - { - "type": "alpine", - "script": "tools/addon/pihole-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/piler.json b/frontend/public/json/piler.json new file mode 100644 index 000000000..d463e9a80 --- /dev/null +++ b/frontend/public/json/piler.json @@ -0,0 +1,36 @@ +{ + "name": "Piler", + "slug": "piler", + "categories": [ + 7, + 18 + ], + "date_created": "2025-12-15", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 80, + "documentation": "https://www.mailpiler.org/", + "config_path": "", + "website": "https://www.mailpiler.org/", + "logo": "https://www.mailpiler.org/piler-logo.png", + "description": "Piler is a feature rich open source email archiving solution with support for legal hold, deduplication, full text search, and many more features.", + "install_methods": [ + { + "type": "default", + "script": "ct/piler.sh", + "resources": { + "cpu": 4, + "ram": 4096, + "hdd": 20, + "os": "Debian", + "version": "12" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [] +} diff --git a/frontend/public/json/qbittorrent-exporter.json b/frontend/public/json/qbittorrent-exporter.json deleted file mode 100644 index dba785b6f..000000000 --- a/frontend/public/json/qbittorrent-exporter.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "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@main/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/romm.json b/frontend/public/json/romm.json index 0e858bdb1..02a5b199a 100644 --- a/frontend/public/json/romm.json +++ b/frontend/public/json/romm.json @@ -8,10 +8,10 @@ "type": "ct", "updateable": true, "privileged": false, - "interface_port": 8080, + "interface_port": 80, "documentation": "https://docs.romm.app/latest/", "website": "https://romm.app/", - "config_path": "/opt", + "config_path": "/opt/romm/.env", "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/romm.webp", "description": "RomM (ROM Manager) allows you to scan, enrich, browse and play your game collection with a clean and responsive interface. Support for multiple platforms, various naming schemes, and custom tags.", "install_methods": [ @@ -22,14 +22,14 @@ "cpu": 2, "ram": 4096, "hdd": 20, - "os": "ubuntu", - "version": "24.04" + "os": "debian", + "version": "13" } } ], "default_credentials": { - "username": "romm", - "password": "changeme" + "username": null, + "password": null }, "notes": [] } diff --git a/frontend/public/json/rustypaste.json b/frontend/public/json/rustypaste.json new file mode 100644 index 000000000..ac81889d5 --- /dev/null +++ b/frontend/public/json/rustypaste.json @@ -0,0 +1,40 @@ +{ + "name": "RustyPaste", + "slug": "rustypaste", + "categories": [ + 12 + ], + "date_created": "2025-12-22", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 8000, + "documentation": "https://github.com/orhun/rustypaste", + "config_path": "/opt/rustypaste/config.toml", + "website": "https://github.com/orhun/rustypaste", + "logo": "https://github.com/orhun/rustypaste/raw/master/img/rustypaste_logo.png", + "description": "Rustypaste is a minimal file upload/pastebin service.", + "install_methods": [ + { + "type": "default", + "script": "ct/rustypaste.sh", + "resources": { + "cpu": 1, + "ram": 512, + "hdd": 20, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "When updating the script it will backup the whole project including all the uploaded files, make sure to extract it to a safe location or remove", + "type": "info" + } + ] +} diff --git a/frontend/public/json/sportarr.json b/frontend/public/json/sportarr.json new file mode 100644 index 000000000..fd0ec06fc --- /dev/null +++ b/frontend/public/json/sportarr.json @@ -0,0 +1,40 @@ +{ + "name": "Sportarr", + "slug": "sportarr", + "categories": [ + 14 + ], + "date_created": "2025-12-12", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 1867, + "documentation": "https://sportarr.net/docs", + "config_path": "/opt/sportarr/.env, /opt/sportarr-data/config/config.xml", + "website": "https://sportarr.net/", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/sportarr.webp", + "description": "Sportarr is an automated media management application for all sports. It works similar to Sonarr and Radarr but specifically designed for combat sports, basketball, football, hockey, motorsports, and hundreds of other sports worldwide.", + "install_methods": [ + { + "type": "default", + "script": "ct/sportarr.sh", + "resources": { + "cpu": 2, + "ram": 2048, + "hdd": 8, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "The resources assigned to LXC are considered baseline. Please adjust to match your workload.", + "type": "info" + } + ] +} diff --git a/install/alpine-valkey-install.sh b/install/alpine-valkey-install.sh new file mode 100644 index 000000000..bac37a357 --- /dev/null +++ b/install/alpine-valkey-install.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2025 community-scripts ORG +# Author: pshankinclarke (lazarillo) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://valkey.io/ + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Valkey" +$STD apk add valkey valkey-openrc valkey-cli +$STD sed -i 's/^bind .*/bind 0.0.0.0/' /etc/valkey/valkey.conf +$STD rc-update add valkey default +$STD rc-service valkey start +msg_ok "Installed Valkey" + +motd_ssh +customize diff --git a/install/cronmaster-install.sh b/install/cronmaster-install.sh index 738e05199..c2a4b651e 100644 --- a/install/cronmaster-install.sh +++ b/install/cronmaster-install.sh @@ -12,6 +12,7 @@ catch_errors setting_up_container network_check update_os +setup_hwaccel msg_info "Installing dependencies" $STD apt install -y pciutils diff --git a/install/debian-install.sh b/install/debian-install.sh index aedf72fc0..99453e489 100644 --- a/install/debian-install.sh +++ b/install/debian-install.sh @@ -13,6 +13,8 @@ setting_up_container network_check update_os +setup_hwaccel + msg_info "Installing Base Dependencies" $STD apt-get install -y curl wget ca-certificates msg_ok "Installed Base Dependencies" diff --git a/install/deferred/openwebui-install.sh b/install/deferred/openwebui-install.sh deleted file mode 100644 index 0e9384d8f..000000000 --- a/install/deferred/openwebui-install.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2025 tteck -# Author: tteck -# Co-Author: havardthom -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Source: https://openwebui.com/ - -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 \ - ffmpeg -msg_ok "Installed Dependencies" - -msg_info "Setup Python3" -$STD apt-get install -y --no-install-recommends \ - python3 \ - python3-pip -msg_ok "Setup Python3" - -setup_nodejs - -msg_info "Installing Open WebUI (Patience)" -fetch_and_deploy_gh_release "open-webui/open-webui" -cd /opt/openwebui/backend -$STD pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu -$STD pip3 install -r requirements.txt -U -cd /opt/openwebui -cat </opt/openwebui/.env -# Ollama URL for the backend to connect -# The path '/ollama' will be redirected to the specified backend URL -OLLAMA_BASE_URL=http://0.0.0.0:11434 -OPENAI_API_BASE_URL='' -OPENAI_API_KEY='' -# AUTOMATIC1111_BASE_URL="http://localhost:7860" -# DO NOT TRACK -SCARF_NO_ANALYTICS=true -DO_NOT_TRACK=true -ANONYMIZED_TELEMETRY=false -ENV=prod -ENABLE_OLLAMA_API=false -EOF -$STD npm install -export NODE_OPTIONS="--max-old-space-size=3584" -sed -i "s/git rev-parse HEAD/openssl rand -hex 20/g" /opt/openwebui/svelte.config.js -$STD npm run build -msg_ok "Installed Open WebUI" - -read -r -p "${TAB3}Would you like to add Ollama? " prompt -if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then - msg_info "Installing Ollama" - curl -fsSLO https://ollama.com/download/ollama-linux-amd64.tgz - tar -C /usr -xzf ollama-linux-amd64.tgz - rm -rf ollama-linux-amd64.tgz - cat </etc/systemd/system/ollama.service -[Unit] -Description=Ollama Service -After=network-online.target - -[Service] -Type=exec -ExecStart=/usr/bin/ollama serve -Environment=HOME=$HOME -Environment=OLLAMA_HOST=0.0.0.0 -Restart=always -RestartSec=3 - -[Install] -WantedBy=multi-user.target -EOF - systemctl enable -q --now ollama - sed -i 's/ENABLE_OLLAMA_API=false/ENABLE_OLLAMA_API=true/g' /opt/openwebui/.env - msg_ok "Installed Ollama" -fi - -msg_info "Creating Service" -cat </etc/systemd/system/open-webui.service -[Unit] -Description=Open WebUI Service -After=network.target - -[Service] -Type=exec -WorkingDirectory=/opt/openwebui -EnvironmentFile=/opt/openwebui/.env -ExecStart=/opt/openwebui/backend/start.sh - -[Install] -WantedBy=multi-user.target -EOF -systemctl enable -q --now open-webui -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/heimdall-dashboard-install.sh b/install/heimdall-dashboard-install.sh new file mode 100644 index 000000000..9346ea4fd --- /dev/null +++ b/install/heimdall-dashboard-install.sh @@ -0,0 +1,57 @@ +#!/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://heimdall.site/ + +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 apt-transport-https +msg_ok "Installed Dependencies" + +PHP_VERSION="8.4" PHP_MODULE="bz2,sqlite3" PHP_FPM="YES" setup_php +setup_composer +fetch_and_deploy_gh_release "Heimdall" "linuxserver/Heimdall" "tarball" + +msg_info "Setting up Heimdall-Dashboard" +cd /opt/Heimdall +cp .env.example .env +$STD php artisan key:generate +msg_ok "Setup Heimdall-Dashboard" + +msg_info "Creating Service" +cat </etc/systemd/system/heimdall.service +[Unit] +Description=Heimdall +After=network.target + +[Service] +Restart=always +RestartSec=5 +Type=simple +User=root +WorkingDirectory=/opt/Heimdall +ExecStart=/usr/bin/php artisan serve --port 7990 --host 0.0.0.0 +TimeoutStopSec=30 + +[Install] +WantedBy=multi-user.target" +EOF +systemctl enable -q --now heimdall +cd /opt/Heimdall +export COMPOSER_ALLOW_SUPERUSER=1 +$STD composer dump-autoload +systemctl restart heimdall.service +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/homarr-install.sh b/install/homarr-install.sh index e103494b6..85051c2f1 100644 --- a/install/homarr-install.sh +++ b/install/homarr-install.sh @@ -18,16 +18,14 @@ $STD apt install -y \ redis-server \ nginx \ gettext \ - openssl \ - musl-dev + openssl msg_ok "Installed Dependencies" -NODE_VERSION=$(curl -s https://raw.githubusercontent.com/homarr-labs/homarr/dev/package.json | jq -r '.engines.node | split(">=")[1] | split(".")[0]') +NODE_VERSION=$(curl -s https://raw.githubusercontent.com/Meierschlumpf/homarr/dev/package.json | jq -r '.engines.node | split(">=")[1] | split(".")[0]') setup_nodejs -fetch_and_deploy_gh_release "homarr" "homarr-labs/homarr" "prebuild" "latest" "/opt/homarr" "build-amd64.tar.gz" +fetch_and_deploy_gh_release "homarr" "Meierschlumpf/homarr" "prebuild" "latest" "/opt/homarr" "source-debian-amd64.tar.gz" msg_info "Installing Homarr" -ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1 mkdir -p /opt/homarr_db touch /opt/homarr_db/db.sqlite SECRET_ENCRYPTION_KEY="$(openssl rand -hex 32)" @@ -47,7 +45,7 @@ msg_ok "Installed Homarr" msg_info "Copying config files" mkdir -p /appdata/redis chown -R redis:redis /appdata/redis -chmod 755 /appdata/redis +chmod 744 /appdata/redis cp /opt/homarr/redis.conf /etc/redis/redis.conf rm /etc/nginx/nginx.conf mkdir -p /etc/nginx/templates @@ -64,6 +62,8 @@ ReadWritePaths=-/appdata/redis -/var/lib/redis -/var/log/redis -/var/run/redis - EOF cat </etc/systemd/system/homarr.service [Unit] +Requires=redis-server.service +After=redis-server.service Description=Homarr Service After=network.target @@ -80,6 +80,7 @@ chmod +x /opt/homarr/run.sh systemctl daemon-reload systemctl enable -q --now redis-server && sleep 5 systemctl enable -q --now homarr +systemctl disable -q --now nginx msg_ok "Created Services" motd_ssh diff --git a/install/hoodik-install.sh b/install/hoodik-install.sh index 31376dab3..1a88f54e9 100644 --- a/install/hoodik-install.sh +++ b/install/hoodik-install.sh @@ -13,46 +13,47 @@ setting_up_container network_check update_os -msg_info "Installing Dependencies" -$STD apt-get install -y \ - pkg-config \ - libssl-dev \ - libc6-dev \ - libpq-dev \ - clang \ - llvm \ - nettle-dev \ - build-essential \ - make -msg_ok "Installed Dependencies" +#msg_info "Installing Dependencies" +#$STD apt-get install -y \ +# pkg-config \ +# libssl-dev \ +# libc6-dev \ +# libpq-dev \ +# clang \ +# llvm \ +# nettle-dev \ +# build-essential \ +# make +#msg_ok "Installed Dependencies" -setup_rust -NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs -fetch_and_deploy_gh_release "hoodik" "hudikhq/hoodik" "tarball" "latest" "/opt/hoodik" +#setup_rust +#NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs +#fetch_and_deploy_gh_release "hoodik" "hudikhq/hoodik" "tarball" "latest" "/opt/hoodik" +fetch_and_deploy_gh_release "hoodik" "hudikhq/hoodik" "prebuild" "latest" "/opt/hoodik" "*x86_64.tar.gz" -msg_info "Installing wasm-pack" -$STD cargo install wasm-pack -msg_ok "Installed wasm-pack" +#msg_info "Installing wasm-pack" +#$STD cargo install wasm-pack +#msg_ok "Installed wasm-pack" -msg_info "Building Hoodik Frontend" -cd /opt/hoodik -$STD yarn install --frozen-lockfile -$STD yarn wasm-pack -$STD yarn web:build -msg_ok "Built Hoodik Frontend" +#msg_info "Building Hoodik Frontend" +#cd /opt/hoodik +#$STD yarn install --frozen-lockfile +#$STD yarn wasm-pack +#$STD yarn web:build +#msg_ok "Built Hoodik Frontend" -msg_info "Building Hoodik Backend" -cd /opt/hoodik -$STD cargo build --release -cp /opt/hoodik/target/release/hoodik /usr/local/bin/hoodik -chmod +x /usr/local/bin/hoodik -msg_ok "Built Hoodik Backend" +#msg_info "Building Hoodik Backend" +#cd /opt/hoodik +#$STD cargo build --release +#cp /opt/hoodik/target/release/hoodik /usr/local/bin/hoodik +#chmod +x /usr/local/bin/hoodik +#msg_ok "Built Hoodik Backend" -msg_info "Cleaning up build artifacts" -rm -rf /opt/hoodik/target -rm -rf /root/.cargo/registry -rm -rf /opt/hoodik/node_modules -msg_ok "Cleaned up build artifacts" +#msg_info "Cleaning up build artifacts" +#rm -rf /opt/hoodik/target +#rm -rf /root/.cargo/registry +#rm -rf /opt/hoodik/node_modules +#msg_ok "Cleaned up build artifacts" msg_info "Configuring Hoodik" mkdir -p /opt/hoodik_data @@ -80,7 +81,8 @@ Type=simple User=root WorkingDirectory=/opt/hoodik_data EnvironmentFile=/opt/hoodik/.env -ExecStart=/usr/local/bin/hoodik +#ExecStart=/usr/local/bin/hoodik +ExecStart=/opt/hoodik Restart=always RestartSec=5 diff --git a/install/koel-install.sh b/install/koel-install.sh deleted file mode 100644 index 487fc593f..000000000 --- a/install/koel-install.sh +++ /dev/null @@ -1,189 +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://koel.dev/ - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -msg_info "Installing Dependencies" -$STD apt install -y \ - nginx \ - ffmpeg \ - cron \ - locales -msg_ok "Installed Dependencies" - -import_local_ip -PG_VERSION="16" setup_postgresql -PG_DB_NAME="koel" PG_DB_USER="koel" setup_postgresql_db -PHP_VERSION="8.4" PHP_FPM="YES" PHP_MODULE="bz2,exif,imagick,pgsql,sqlite3" setup_php -NODE_VERSION="22" NODE_MODULE="pnpm" setup_nodejs -setup_composer - -fetch_and_deploy_gh_release "koel" "koel/koel" "prebuild" "latest" "/opt/koel" "koel-*.tar.gz" - -msg_info "Configuring Koel" -mkdir -p /opt/koel_media /opt/koel_sync -cd /opt/koel -cat </opt/koel/.env -APP_NAME=Koel -APP_ENV=production -APP_DEBUG=false -APP_URL=http://${LOCAL_IP} -APP_KEY= - -TRUSTED_HOSTS= - -DB_CONNECTION=pgsql -DB_HOST=127.0.0.1 -DB_PORT=5432 -DB_DATABASE=${PG_DB_NAME} -DB_USERNAME=${PG_DB_USER} -DB_PASSWORD=${PG_DB_PASS} - -STORAGE_DRIVER=local -MEDIA_PATH=/opt/koel_media -ARTIFACTS_PATH= - -IGNORE_DOT_FILES=true -APP_MAX_SCAN_TIME=600 -MEMORY_LIMIT= - -STREAMING_METHOD=php -SCOUT_DRIVER=tntsearch - -USE_MUSICBRAINZ=true -MUSICBRAINZ_USER_AGENT= - -LASTFM_API_KEY= -LASTFM_API_SECRET= - -SPOTIFY_CLIENT_ID= -SPOTIFY_CLIENT_SECRET= - -YOUTUBE_API_KEY= - -CDN_URL= - -TRANSCODE_FLAC=false -FFMPEG_PATH=/usr/bin/ffmpeg -TRANSCODE_BIT_RATE=128 - -ALLOW_DOWNLOAD=true -BACKUP_ON_DELETE=true - -MEDIA_BROWSER_ENABLED=false - -PROXY_AUTH_ENABLED=false - -SYNC_LOG_LEVEL=error -FORCE_HTTPS= - -MAIL_FROM_ADDRESS="noreply@localhost" -MAIL_FROM_NAME="Koel" -MAIL_MAILER=log -MAIL_HOST=null -MAIL_PORT=null -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null - -BROADCAST_CONNECTION=log -CACHE_DRIVER=file -FILESYSTEM_DISK=local -QUEUE_CONNECTION=sync -SESSION_DRIVER=file -SESSION_LIFETIME=120 -EOF - -mkdir -p /opt/koel/storage/{app/public,framework/{cache/data,sessions,views},logs} -chown -R www-data:www-data /opt/koel /opt/koel_media /opt/koel_sync -chmod -R 775 /opt/koel/storage /opt/koel/bootstrap/cache -msg_ok "Configured Koel" - -msg_info "Installing Koel (Patience)" -export COMPOSER_ALLOW_SUPERUSER=1 -cd /opt/koel -$STD composer install --no-interaction --no-dev --optimize-autoloader -$STD php artisan key:generate --force -$STD php artisan config:clear -$STD php artisan cache:clear -$STD php artisan koel:init --no-assets --no-interaction -chown -R www-data:www-data /opt/koel -msg_ok "Installed Koel" - -msg_info "Tuning PHP-FPM" -PHP_FPM_CONF="/etc/php/8.4/fpm/pool.d/www.conf" -sed -i 's/^pm.max_children = .*/pm.max_children = 15/' "$PHP_FPM_CONF" -sed -i 's/^pm.start_servers = .*/pm.start_servers = 4/' "$PHP_FPM_CONF" -sed -i 's/^pm.min_spare_servers = .*/pm.min_spare_servers = 2/' "$PHP_FPM_CONF" -sed -i 's/^pm.max_spare_servers = .*/pm.max_spare_servers = 8/' "$PHP_FPM_CONF" -$STD systemctl restart php8.4-fpm -msg_ok "Tuned PHP-FPM" - -msg_info "Configuring Nginx" -cat <<'EOF' >/etc/nginx/sites-available/koel -server { - listen 80; - server_name _; - root /opt/koel/public; - index index.php; - - client_max_body_size 50M; - charset utf-8; - - gzip on; - gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json; - gzip_comp_level 9; - - send_timeout 3600; - - location / { - try_files $uri $uri/ /index.php?$args; - } - - location /media/ { - internal; - alias $upstream_http_x_media_root; - } - - location ~ \.php$ { - try_files $uri $uri/ /index.php?$args; - fastcgi_pass unix:/run/php/php8.4-fpm.sock; - fastcgi_index index.php; - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_intercept_errors on; - fastcgi_param PATH_INFO $fastcgi_path_info; - fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include fastcgi_params; - } - - location ~ /\.(?!well-known).* { - deny all; - } -} -EOF -rm -f /etc/nginx/sites-enabled/default -ln -sf /etc/nginx/sites-available/koel /etc/nginx/sites-enabled/koel -$STD systemctl reload nginx -msg_ok "Configured Nginx" - -msg_info "Setting up Cron Job" -cat <<'EOF' >/etc/cron.d/koel -0 * * * * www-data cd /opt/koel && /usr/bin/php artisan koel:scan >/dev/null 2>&1 -EOF -chmod 644 /etc/cron.d/koel -msg_ok "Set up Cron Job" - -motd_ssh -customize -cleanup_lxc diff --git a/install/linkwarden-install.sh b/install/linkwarden-install.sh new file mode 100644 index 000000000..6596e2020 --- /dev/null +++ b/install/linkwarden-install.sh @@ -0,0 +1,76 @@ +#!/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://linkwarden.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 install -y build-essential +msg_ok "Installed Dependencies" + +NODE_VERSION="22" setup_nodejs +PG_VERSION="16" setup_postgresql +PG_DB_NAME="linkwardendb" PG_DB_USER="linkwarden" setup_postgresql_db +RUST_CRATES="monolith" setup_rust +fetch_and_deploy_gh_release "linkwarden" "linkwarden/linkwarden" +import_local_ip + +read -r -p "${TAB3}Would you like to add Adminer? " prompt +if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then + setup_adminer +fi + +msg_info "Installing Linkwarden (Patience)" +export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +export PRISMA_HIDE_UPDATE_MESSAGE=1 +export DEBIAN_FRONTEND=noninteractive +corepack enable +SECRET_KEY="$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)" +cd /opt/linkwarden +$STD yarn workspaces focus linkwarden @linkwarden/web @linkwarden/worker +# $STD npx playwright install-deps +# $STD yarn playwright install + +cat </opt/linkwarden/.env +NEXTAUTH_SECRET=${SECRET_KEY} +NEXTAUTH_URL=http://${LOCAL_IP}:3000 +DATABASE_URL=postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME} +EOF +$STD yarn prisma:generate +$STD yarn web:build +$STD yarn prisma:deploy +rm -rf ~/.cargo/registry ~/.cargo/git ~/.cargo/.package-cache +rm -rf /root/.cache/yarn +rm -rf /opt/linkwarden/.next/cache +msg_ok "Installed Linkwarden" + +msg_info "Creating Service" +cat </etc/systemd/system/linkwarden.service +[Unit] +Description=Linkwarden Service +After=network.target + +[Service] +Type=exec +Environment=PATH=$PATH +WorkingDirectory=/opt/linkwarden +ExecStart=/usr/bin/yarn concurrently:start + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now linkwarden +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/nextexplorer-install.sh b/install/nextexplorer-install.sh index a1f15a7d7..d39598f18 100644 --- a/install/nextexplorer-install.sh +++ b/install/nextexplorer-install.sh @@ -79,6 +79,7 @@ SESSION_SECRET="${SECRET}" # OIDC_CLIENT_SECRET= # OIDC_CALLBACK_URL= # OIDC_SCOPES= +# OIDC_AUTO_CREATE_USERS=true # SEARCH_DEEP= # SEARCH_RIPGREP= diff --git a/install/opencloud-install.sh b/install/opencloud-install.sh index 92b4e55c8..4e406c6f8 100644 --- a/install/opencloud-install.sh +++ b/install/opencloud-install.sh @@ -57,7 +57,7 @@ echo "$COOLPASS" >~/.coolpass msg_ok "Installed Collabora Online" # OpenCloud -fetch_and_deploy_gh_release "opencloud" "opencloud-eu/opencloud" "singlefile" "v4.0.0" "/usr/bin" "opencloud-*-linux-amd64" +fetch_and_deploy_gh_release "opencloud" "opencloud-eu/opencloud" "singlefile" "v4.1.0" "/usr/bin" "opencloud-*-linux-amd64" msg_info "Configuring OpenCloud" DATA_DIR="/var/lib/opencloud/" diff --git a/install/papra-install.sh b/install/papra-install.sh new file mode 100644 index 000000000..2c698030b --- /dev/null +++ b/install/papra-install.sh @@ -0,0 +1,104 @@ +#!/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://github.com/CorentinTh/papra + +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 \ + git \ + build-essential \ + tesseract-ocr \ + tesseract-ocr-all +msg_ok "Installed Dependencies" + +NODE_VERSION="24" setup_nodejs + +msg_info "Cloning Papra Repository" +cd /opt +RELEASE=$(curl -s https://api.github.com/repos/papra-hq/papra/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3)}') +$STD git clone --depth=1 --branch ${RELEASE} https://github.com/papra-hq/papra.git +cd papra +msg_ok "Cloned Papra Repository" + +msg_info "Setup Papra" +export COREPACK_ENABLE_NETWORK=1 +$STD corepack enable +$STD corepack prepare pnpm@10.19.0 --activate +$STD pnpm install --frozen-lockfile --ignore-scripts +$STD pnpm --filter "@papra/app-client..." run build +$STD pnpm --filter "@papra/app-server..." run build +msg_ok "Set up Papra" + +msg_info "Configuring Papra" +CONTAINER_IP=$(hostname -I | awk '{print $1}') +BETTER_AUTH_SECRET=$(openssl rand -hex 32) + +mkdir -p /opt/papra/app-data/db +mkdir -p /opt/papra/app-data/documents + +cat >/opt/papra/.env </etc/systemd/system/papra.service </etc/piler/piler.conf +hostid=piler.${LOCAL_IP}.nip.io +update_counters_to_memcached=1 + +mysql_hostname=localhost +mysql_database=${MARIADB_DB_NAME} +mysql_username=${MARIADB_DB_USER} +mysql_password=${MARIADB_DB_PASS} +mysql_socket=/var/run/mysqld/mysqld.sock + +archive_dir=/var/piler/store +data_dir=/var/piler +tmp_dir=/var/piler/tmp + +listen_addr=0.0.0.0 +listen_port=25 + +encrypt_messages=1 +key=${PILER_KEY} +iv=0123456789ABCDEF + +memcached_servers=127.0.0.1 + +enable_clamav=1 +clamd_socket=/var/run/clamav/clamd.ctl + +spam_header_line=X-Spam-Status: Yes + +verbosity=1 +EOF + +chown piler:piler /etc/piler/piler.conf +chmod 640 /etc/piler/piler.conf +chown -R piler:piler /var/piler +chmod 750 /var/piler +msg_ok "Configured Piler" + +msg_info "Configuring Manticore Search" +cat </etc/manticoresearch/manticore.conf +searchd { + listen = 9306:mysql + listen = 9312 + listen = 9308:http + log = /var/log/manticore/searchd.log + query_log = /var/log/manticore/query.log + pid_file = /var/run/manticore/searchd.pid + binlog_path = /var/lib/manticore/data +} + +source piler1 { + type = mysql + sql_host = localhost + sql_user = ${MARIADB_DB_USER} + sql_pass = ${MARIADB_DB_PASS} + sql_db = ${MARIADB_DB_NAME} + sql_port = 3306 + + sql_query = SELECT id, from_addr, to_addr, subject, body, sent FROM metadata + sql_attr_timestamp = sent +} + +index piler1 { + source = piler1 + path = /var/piler/manticore/piler1 + min_word_len = 1 + charset_table = 0..9, A..Z->a..z, a..z, U+00E1, U+00E9 +} + +index tag1 { + type = rt + path = /var/piler/manticore/tag1 + rt_field = tag + rt_attr_uint = uid +} + +index note1 { + type = rt + path = /var/piler/manticore/note1 + rt_field = note + rt_attr_uint = uid +} +EOF + +mkdir -p /var/log/manticore +chown -R manticore:manticore /var/log/manticore +chown -R piler:piler /var/piler/manticore +msg_ok "Configured Manticore Search" + +msg_info "Creating Piler Service" +cat </etc/systemd/system/piler.service +[Unit] +Description=Piler Email Archiving +After=network.target mysql.service manticore.service +Requires=mysql.service manticore.service + +[Service] +Type=forking +User=piler +Group=piler +ExecStart=/usr/local/sbin/pilerd -c /etc/piler/piler.conf +PIDFile=/var/piler/pilerd.pid +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +$STD systemctl daemon-reload +$STD systemctl enable --now manticore +$STD systemctl enable --now memcached +$STD systemctl enable --now clamav-daemon +$STD systemctl enable --now piler +msg_ok "Created Piler Service" + +msg_info "Configuring PHP-FPM Pool" +cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/piler.conf +sed -i 's/\[www\]/[piler]/' /etc/php/8.4/fpm/pool.d/piler.conf +sed -i 's/^user = www-data/user = piler/' /etc/php/8.4/fpm/pool.d/piler.conf +sed -i 's/^group = www-data/group = piler/' /etc/php/8.4/fpm/pool.d/piler.conf +sed -i 's|^listen = .*|listen = /run/php/php8.4-fpm-piler.sock|' /etc/php/8.4/fpm/pool.d/piler.conf +$STD systemctl restart php8.4-fpm +msg_ok "Configured PHP-FPM Pool" + +msg_info "Configuring Piler Web GUI" +cd /var/www/piler + +cat </var/www/piler/config-site.php + +EOF + +chown -R piler:piler /var/www/piler +chmod 755 /var/www/piler +msg_ok "Installed Piler Web GUI" + +msg_info "Configuring Nginx" +cat </etc/nginx/sites-available/piler +server { + listen 80; + server_name _; + root /var/www/piler; + index index.php; + + access_log /var/log/nginx/piler-access.log; + error_log /var/log/nginx/piler-error.log; + + charset utf-8; + + location / { + try_files \$uri \$uri/ /index.php?\$args; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php8.4-fpm-piler.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; + include fastcgi_params; + } + + location ~* \.(jpg|jpeg|gif|css|png|js|ico|html|woff|woff2)$ { + access_log off; + expires max; + } + + location ~ /\.ht { + deny all; + } +} +EOF + +ln -sf /etc/nginx/sites-available/piler /etc/nginx/sites-enabled/piler +rm -f /etc/nginx/sites-enabled/default +$STD nginx -t +$STD systemctl enable --now nginx +msg_ok "Configured Nginx" + +msg_info "Setting Up Cron Jobs" +cat </etc/cron.d/piler +30 6 * * * piler /usr/local/libexec/piler/indexer.delta.sh +30 7 * * * piler /usr/local/libexec/piler/indexer.main.sh +*/15 * * * * piler /usr/local/bin/pilerstat +30 2 * * * piler /usr/local/bin/pilerpurge +3 * * * * piler /usr/local/bin/pilerconf +EOF +msg_ok "Set Up Cron Jobs" + +motd_ssh +customize +cleanup_lxc diff --git a/install/pixelfed-install.sh b/install/pixelfed-install.sh index dcb4134da..a122fe356 100644 --- a/install/pixelfed-install.sh +++ b/install/pixelfed-install.sh @@ -43,13 +43,12 @@ systemctl restart redis-server msg_ok "Redis configured" msg_info "Configuring PHP-FPM Pool" -mkdir -p /run/php-fpm cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/pixelfed.conf sed -i 's/\[www\]/[pixelfed]/' /etc/php/8.4/fpm/pool.d/pixelfed.conf sed -i 's/^user = www-data/user = pixelfed/' /etc/php/8.4/fpm/pool.d/pixelfed.conf sed -i 's/^group = www-data/group = pixelfed/' /etc/php/8.4/fpm/pool.d/pixelfed.conf -sed -i 's|^listen = .*|listen = /run/php-fpm/pixelfed.sock|' /etc/php/8.4/fpm/pool.d/pixelfed.conf -sed -i 's/^listen.owner = .*/listen.owner = pixelfed/' /etc/php/8.4/fpm/pool.d/pixelfed.conf +sed -i 's|^listen = .*|listen = /run/php/php8.4-fpm-pixelfed.sock|' /etc/php/8.4/fpm/pool.d/pixelfed.conf +sed -i 's/^listen.owner = .*/listen.owner = www-data/' /etc/php/8.4/fpm/pool.d/pixelfed.conf sed -i 's/^listen.group = .*/listen.group = www-data/' /etc/php/8.4/fpm/pool.d/pixelfed.conf systemctl restart php8.4-fpm msg_ok "PHP-FPM Pool configured" @@ -75,6 +74,7 @@ sed -i "s|REDIS_PORT=.*|REDIS_PORT=6379|" .env sed -i "s|ACTIVITY_PUB=.*|ACTIVITY_PUB=true|" .env sed -i "s|AP_REMOTE_FOLLOW=.*|AP_REMOTE_FOLLOW=true|" .env sed -i "s|OAUTH_ENABLED=.*|OAUTH_ENABLED=true|" .env +echo "SESSION_SECURE_COOKIE=false" >>.env chown -R pixelfed:pixelfed /opt/pixelfed chmod -R 755 /opt/pixelfed @@ -122,7 +122,7 @@ server { location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass unix:/run/php-fpm/pixelfed.sock; + fastcgi_pass unix:/run/php/php8.4-fpm-pixelfed.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; diff --git a/install/postgresus-install.sh b/install/postgresus-install.sh index 812346647..7eaf731d8 100644 --- a/install/postgresus-install.sh +++ b/install/postgresus-install.sh @@ -26,25 +26,39 @@ NODE_VERSION="24" setup_nodejs fetch_and_deploy_gh_release "postgresus" "RostislavDugin/postgresus" "tarball" "latest" "/opt/postgresus" msg_info "Building Postgresus (Patience)" -# Build frontend cd /opt/postgresus/frontend $STD npm ci $STD npm run build - -# Build backend cd /opt/postgresus/backend $STD go mod tidy $STD go mod download -$STD CGO_ENABLED=0 go build -o /opt/postgresus/postgresus ./cmd/main.go -mkdir -p /opt/postgresus/{data,backups,logs} -cp -r /opt/postgresus/frontend/dist /opt/postgresus/ui +$STD go install github.com/swaggo/swag/cmd/swag@latest +$STD /root/go/bin/swag init -g cmd/main.go -o swagger +$STD env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o postgresus ./cmd/main.go +mv /opt/postgresus/backend/postgresus /opt/postgresus/postgresus +mkdir -p /opt/postgresus_data/{data,backups,logs} +mkdir -p /postgresus-data/temp +mkdir -p /opt/postgresus/ui/build +cp -r /opt/postgresus/frontend/dist/* /opt/postgresus/ui/build/ cp -r /opt/postgresus/backend/migrations /opt/postgresus/ chown -R postgres:postgres /opt/postgresus +chown -R postgres:postgres /opt/postgresus_data +chown -R postgres:postgres /postgresus-data msg_ok "Built Postgresus" msg_info "Configuring Postgresus" ADMIN_PASS=$(openssl rand -base64 12) JWT_SECRET=$(openssl rand -hex 32) + +# Create PostgreSQL version symlinks for compatibility +for v in 12 13 14 15 16 18; do + ln -sf /usr/lib/postgresql/17 /usr/lib/postgresql/$v +done + +# Install goose for migrations +$STD go install github.com/pressly/goose/v3/cmd/goose@latest +ln -sf /root/go/bin/goose /usr/local/bin/goose + cat </opt/postgresus/.env # Environment ENV_MODE=production @@ -54,8 +68,14 @@ SERVER_PORT=4005 SERVER_HOST=0.0.0.0 # Database (Internal PostgreSQL for app data) +DATABASE_DSN=host=localhost user=${PG_DB_USER} password=${PG_DB_PASS} dbname=${PG_DB_NAME} port=5432 sslmode=disable DATABASE_URL=postgres://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}?sslmode=disable +# Migrations +GOOSE_DRIVER=postgres +GOOSE_DBSTRING=postgres://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}?sslmode=disable +GOOSE_MIGRATION_DIR=/opt/postgresus/migrations + # Security JWT_SECRET=${JWT_SECRET} ENCRYPTION_KEY=$(openssl rand -hex 32) @@ -65,15 +85,16 @@ ADMIN_EMAIL=admin@localhost ADMIN_PASSWORD=${ADMIN_PASS} # Paths -DATA_DIR=/opt/postgresus/data -BACKUP_DIR=/opt/postgresus/backups -LOG_DIR=/opt/postgresus/logs +DATA_DIR=/opt/postgresus_data/data +BACKUP_DIR=/opt/postgresus_data/backups +LOG_DIR=/opt/postgresus_data/logs # PostgreSQL Tools (for creating backups) -PG_DUMP_PATH=/usr/bin/pg_dump -PG_RESTORE_PATH=/usr/bin/pg_restore -PSQL_PATH=/usr/bin/psql +PG_DUMP_PATH=/usr/lib/postgresql/17/bin/pg_dump +PG_RESTORE_PATH=/usr/lib/postgresql/17/bin/pg_restore +PSQL_PATH=/usr/lib/postgresql/17/bin/psql EOF +chown postgres:postgres /opt/postgresus/.env chmod 600 /opt/postgresus/.env msg_ok "Configured Postgresus" @@ -89,6 +110,7 @@ Type=simple User=postgres Group=postgres WorkingDirectory=/opt/postgresus +Environment="PATH=/usr/local/bin:/usr/bin:/bin" EnvironmentFile=/opt/postgresus/.env ExecStart=/opt/postgresus/postgresus Restart=always @@ -99,6 +121,7 @@ StandardError=journal [Install] WantedBy=multi-user.target EOF +$STD systemctl daemon-reload $STD systemctl enable -q --now postgresus msg_ok "Created Postgresus Service" diff --git a/install/romm-install.sh b/install/romm-install.sh index 438e4e5a7..2f88f52cb 100644 --- a/install/romm-install.sh +++ b/install/romm-install.sh @@ -2,9 +2,10 @@ # Copyright (c) 2021-2025 community-scripts ORG # Author: DevelopmentCats -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Co-author: AlphaLawless +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE # Source: https://romm.app -# Updated: 03/10/2025 +# Updated: 25/12/2025 source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" color @@ -18,65 +19,126 @@ msg_info "Installing dependencies" $STD apt-get install -y \ acl \ build-essential \ + gcc \ + g++ \ + make \ + git \ + curl \ libssl-dev \ libffi-dev \ + libmagic-dev \ python3-dev \ python3-pip \ python3-venv \ libmariadb3 \ libmariadb-dev \ libpq-dev \ + libbz2-dev \ + libreadline-dev \ + libsqlite3-dev \ + zlib1g-dev \ + liblzma-dev \ + libncurses5-dev \ + libncursesw5-dev \ + redis-server \ redis-tools \ - p7zip \ + p7zip-full \ tzdata \ - jq -msg_ok "Installed core dependencies" + jq \ + nginx +msg_ok "Installed dependencies" -PYTHON_VERSION="3.12" setup_uv -NODE_VERSION="22" NODE_MODULE="serve" setup_nodejs +UV_VERSION="0.7.19" PYTHON_VERSION="3.13" setup_uv +NODE_VERSION="22" setup_nodejs setup_mariadb +MARIADB_DB_NAME="romm" MARIADB_DB_USER="romm" setup_mariadb_db -msg_info "Configuring Database" -DB_NAME=romm -DB_USER=romm -DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13) -$STD mariadb -u root -e "CREATE DATABASE IF NOT EXISTS $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" -$STD mariadb -u root -e "CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';" -$STD mariadb -u root -e "GRANT ALL ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;" -{ - echo "RomM-Credentials" - echo "RomM Database User: $DB_USER" - echo "RomM Database Password: $DB_PASS" - echo "RomM Database Name: $DB_NAME" -} >~/romm.creds -chmod 600 ~/romm.creds -msg_ok "Configured Database" - -msg_info "Creating romm user and directories" -id -u romm &>/dev/null || useradd -r -m -d /var/lib/romm -s /bin/bash romm +msg_info "Creating directories" mkdir -p /opt/romm \ /var/lib/romm/config \ /var/lib/romm/resources \ /var/lib/romm/assets/{saves,states,screenshots} \ - /var/lib/romm/library/roms/{gba,gbc,ps} \ - /var/lib/romm/library/bios/{gba,ps} -chown -R romm:romm /opt/romm /var/lib/romm -msg_ok "Created romm user and directories" + /var/lib/romm/library/roms \ + /var/lib/romm/library/bios +msg_ok "Created directories" -msg_info "Configuring Database" -DB_NAME=romm -DB_USER=romm -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 "RomM-Credentials" - echo "RomM Database User: $DB_USER" - echo "RomM Database Password: $DB_PASS" - echo "RomM Database Name: $DB_NAME" -} >~/romm.creds -msg_ok "Configured Database" +msg_info "Creating configuration file" +cat >/var/lib/romm/config/config.yml <<'CONFIGEOF' +# RomM Configuration File +# Documentation: https://docs.romm.app/latest/Getting-Started/Configuration-File/ +# Only uncomment the lines you want to use/modify + +# exclude: +# platforms: +# - excluded_folder_a +# roms: +# single_file: +# extensions: +# - xml +# - txt +# names: +# - '._*' +# - '*.nfo' +# multi_file: +# names: +# - downloaded_media +# - media + +# system: +# platforms: +# gc: ngc +# ps1: psx + +# The folder name where your roms are located (relative to library path) +# filesystem: +# roms_folder: 'roms' + +# scan: +# priority: +# metadata: +# - "igdb" +# - "moby" +# - "ss" +# - "ra" +# artwork: +# - "igdb" +# - "moby" +# - "ss" +# region: +# - "us" +# - "eu" +# - "jp" +# language: +# - "en" +# media: +# - box2d +# - box3d +# - screenshot +# - manual + +# emulatorjs: +# debug: false +# cache_limit: null +CONFIGEOF +chmod 644 /var/lib/romm/config/config.yml +msg_ok "Created configuration file" + +msg_info "Building RAHasher (RetroAchievements)" +RAHASHER_VERSION="1.8.1" +cd /tmp +git clone --recursive --branch "$RAHASHER_VERSION" --depth 1 https://github.com/RetroAchievements/RALibretro.git +cd RALibretro +sed -i '22a #include ' ./src/Util.h +sed -i '6a #include ' \ + ./src/libchdr/deps/zlib-1.3.1/gzlib.c \ + ./src/libchdr/deps/zlib-1.3.1/gzread.c \ + ./src/libchdr/deps/zlib-1.3.1/gzwrite.c +$STD make HAVE_CHD=1 -f ./Makefile.RAHasher +cp ./bin64/RAHasher /usr/bin/RAHasher +chmod +x /usr/bin/RAHasher +cd /tmp +rm -rf /tmp/RALibretro +msg_ok "Built RAHasher" fetch_and_deploy_gh_release "romm" "rommapp/romm" @@ -88,13 +150,14 @@ AUTH_SECRET_KEY=$(openssl rand -hex 32) cat >/opt/romm/.env </etc/nginx/sites-available/romm <<'EOF' +upstream romm_backend { + server 127.0.0.1:5000; +} +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name _; + root /opt/romm/frontend/dist; + client_max_body_size 0; + + # Frontend SPA + location / { + try_files $uri $uri/ /index.html; + } + + # EmulatorJS player - requires COOP/COEP headers for SharedArrayBuffer + location ~ ^/rom/.*/ejs$ { + add_header Cross-Origin-Embedder-Policy "require-corp"; + add_header Cross-Origin-Opener-Policy "same-origin"; + try_files $uri /index.html; + } + + # Backend API + location /api { + proxy_pass http://romm_backend; + proxy_buffering off; + proxy_request_buffering off; + 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; + } + + # WebSocket and Netplay + location ~ ^/(ws|netplay) { + proxy_pass http://romm_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_read_timeout 86400; + } + + # OpenAPI docs + location = /openapi.json { + proxy_pass http://romm_backend; + } + + # Internal library file serving + location /library/ { + internal; + alias /var/lib/romm/library/; + } +} +EOF + +rm -f /etc/nginx/sites-enabled/default +ln -sf /etc/nginx/sites-available/romm /etc/nginx/sites-enabled/romm +$STD nginx -t +systemctl restart nginx +systemctl enable -q nginx +msg_ok "Configured nginx" + +msg_info "Creating services" cat >/etc/systemd/system/romm-backend.service </etc/systemd/system/romm-frontend.service </etc/systemd/system/romm-worker.service </etc/systemd/system/romm-scheduler.service </etc/systemd/system/romm-watcher.service </etc/systemd/system/rustypaste.service +[Unit] +Description=rustypaste Service +After=network.target + +[Service] +WorkingDirectory=/opt/rustypaste +ExecStart=/opt/rustypaste/target/release/rustypaste +Restart=always + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now rustypaste +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/sportarr-install.sh b/install/sportarr-install.sh new file mode 100644 index 000000000..26d9eb14d --- /dev/null +++ b/install/sportarr-install.sh @@ -0,0 +1,57 @@ +#!/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/Sportarr/Sportarr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os +setup_hwaccel + +msg_info "Installing Dependencies" +$STD apt install -y \ + ffmpeg \ + gosu \ + sqlite3 +msg_ok "Installed Dependencies" + +fetch_and_deploy_gh_release "sportarr" "Sportarr/Sportarr" "prebuild" "latest" "/opt/sportarr" "Sportarr-linux-x64-*.tar.gz" + +msg_info "Setting up Sportarr" +cat </opt/sportarr/.env +Sportarr__DataPath="/opt/sportarr-data/config" +ASPNETCORE_URLS="http://*:1867" +ASPNETCORE_ENVIRONMENT="Production" +DOTNET_CLI_TELEMETRY_OPTOUT=1 +DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +LIBVA_DRIVER_NAME=iHD +EOF +msg_ok "Setup Sportarr" + +msg_info "Creating Service" +cat </etc/systemd/system/sportarr.service +[Unit] +Description=Sportarr Service +After=network.target + +[Service] +EnvironmentFile=/opt/sportarr/.env +WorkingDirectory=/opt/sportarr +ExecStart=/opt/sportarr/Sportarr +Restart=always + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now sportarr +msg_info "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/misc/build.func b/misc/build.func index 7ce994d1e..c1674fb49 100644 --- a/misc/build.func +++ b/misc/build.func @@ -39,46 +39,46 @@ # - 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. - 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. - 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}}" + 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. + 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}}" - # Parse dev_mode early - parse_dev_mode + # 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 + # 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) + # 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 + # 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 } # ----------------------------------------------------------------------------- @@ -187,17 +187,17 @@ variables() { 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" + 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" + 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 # ============================================================================== @@ -215,46 +215,46 @@ fi # ------------------------------------------------------------------------------ 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) + # 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 + # 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) + # 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)) + # 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 + # 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 + # 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 - # Silent success - only show errors if they exist + # Silent success - only show errors if they exist } # ============================================================================== @@ -270,18 +270,18 @@ maxkeys_check() { # - Returns "Unknown" if OS type cannot be determined # ------------------------------------------------------------------------------ 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" + 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 - fi - echo "$CURRENT_IP" + echo "$CURRENT_IP" } # ------------------------------------------------------------------------------ @@ -291,16 +291,16 @@ get_current_ip() { # - Removes old IP entries to avoid duplicates # ------------------------------------------------------------------------------ update_motd_ip() { - MOTD_FILE="/etc/motd" + MOTD_FILE="/etc/motd" - if [ -f "$MOTD_FILE" ]; then - # Remove existing IP Address lines to prevent duplication - sed -i '/IP Address:/d' "$MOTD_FILE" + 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 + IP=$(get_current_ip) + # Add the new IP address + echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE" + fi } # ------------------------------------------------------------------------------ @@ -311,27 +311,27 @@ update_motd_ip() { # - Falls back to warning if no keys provided # ------------------------------------------------------------------------------ install_ssh_keys_into_ct() { - [[ "$SSH" != "yes" ]] && return 0 + [[ "$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}" + 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 - fi - - # Fallback: nichts ausgewählt - msg_warn "No SSH keys to install (skipping)." - return 0 } # ------------------------------------------------------------------------------ @@ -343,55 +343,55 @@ install_ssh_keys_into_ct() { # - 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 + 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 + 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 + 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 ' + # 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 + 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))) + # 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 - fi - FOUND_HOST_KEY_COUNT="$total" - ( - IFS=: - echo "${files[*]}" - ) + FOUND_HOST_KEY_COUNT="$total" + ( + IFS=: + echo "${files[*]}" + ) } # ============================================================================== @@ -406,53 +406,53 @@ find_host_ssh_keys() { # - 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" + # $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 + # $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") + 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 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 + 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" + _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 + # 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 + # Silent operation - no output message } # ============================================================================== @@ -469,83 +469,83 @@ choose_and_set_storage_for_file() { # - Sets up container type, resources, network, SSH, features, and tags # ------------------------------------------------------------------------------ base_settings() { - # Default Settings - CT_TYPE=${var_unprivileged:-"1"} + # 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}" + # 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}" + # 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 - 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}" + 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 - 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}" + 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 - 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:-""} + 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" + # 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 - 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_KEYCTL=${var_keyctl:-0} - ENABLE_MKNOD=${var_mknod:-0} - MOUNT_FS=${var_mount_fs:-""} + 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_KEYCTL=${var_keyctl:-0} + ENABLE_MKNOD=${var_mknod:-0} + MOUNT_FS=${var_mount_fs:-""} - # 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 + # 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 } # ------------------------------------------------------------------------------ @@ -556,49 +556,49 @@ base_settings() { # - Only loads whitelisted var_* keys # ------------------------------------------------------------------------------ load_vars_file() { - local file="$1" - [ -f "$file" ] || return 0 - msg_info "Loading defaults from ${file}" + local file="$1" + [ -f "$file" ] || return 0 + msg_info "Loading defaults from ${file}" - # Allowed var_* keys - local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu 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 - ) + # Allowed var_* keys + local VAR_WHITELIST=( + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu 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 + ) - # Whitelist check helper - _is_whitelisted() { - local k="$1" w - for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done - return 1 - } + # Whitelist check helper + _is_whitelisted() { + local k="$1" w + for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done + return 1 + } - 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]}" + 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 "$var_key" || continue + [[ "$var_key" != var_* ]] && continue + _is_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 + # Strip quotes + if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then + var_val="${BASH_REMATCH[1]}" + elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then + var_val="${BASH_REMATCH[1]}" + fi - # Set only if not already exported - [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" - fi - done <"$file" - msg_ok "Loaded ${file}" + # Set only if not already exported + [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" + fi + done <"$file" + msg_ok "Loaded ${file}" } # ------------------------------------------------------------------------------ @@ -611,56 +611,56 @@ load_vars_file() { # - 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_gpu 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 - ) + # 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_gpu 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 - } + # 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 - 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 + # 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" - local canonical="/usr/local/community-scripts/default.vars" - # Silent creation - no msg_info output - mkdir -p /usr/local/community-scripts + # 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 - # Pick storages before writing the file (always ask unless only one) - # Create a minimal temp file to write into - : >"$canonical" + local canonical="/usr/local/community-scripts/default.vars" + # Silent creation - no msg_info output + mkdir -p /usr/local/community-scripts - # Base content (no var_ctid / var_hostname here) - cat >"$canonical" <<'EOF' + # 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. @@ -709,47 +709,47 @@ var_verbose=no # 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 + # 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 - } + 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 - } + # 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 + } - # 1) Ensure file exists - _ensure_default_vars + # 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" + # 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 + # 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 User Defaults (default.vars) on node $PVEHOST_NAME${CL}" - echo_default + # 4) Apply base settings and show summary + METHOD="mydefaults-global" + base_settings "$VERBOSE" + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using User Defaults (default.vars) on node $PVEHOST_NAME${CL}" + echo_default } # ------------------------------------------------------------------------------ @@ -760,8 +760,8 @@ EOF # ------------------------------------------------------------------------------ get_app_defaults_path() { - local n="${NSAPP:-${APP,,}}" - echo "/usr/local/community-scripts/defaults/${n}.vars" + local n="${NSAPP:-${APP,,}}" + echo "/usr/local/community-scripts/defaults/${n}.vars" } # ------------------------------------------------------------------------------ @@ -774,32 +774,32 @@ get_app_defaults_path() { # - 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_gpu - 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 - ) + # 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_gpu + 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 # Global whitelist check function (used by _load_vars_file_to_map and others) _is_whitelisted_key() { - local k="$1" - local w - for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done - return 1 + 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" + # 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 @@ -807,190 +807,190 @@ _sanitize_value() { # 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" + 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 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 out + out+="# Diff for ${APP} (${NSAPP})\n" + out+="# Old: ${oldf}\n# New: ${newf}\n\n" - local found_change=0 + 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 + # 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 - 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" + 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)" + tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" - # NET/GW - _net="${NET:-}" - _gate="" - case "${GATE:-}" in - ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; - esac + # 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 + # 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 + # 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 + # 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}" + # 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}" + # 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:-}}" + # 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 "# 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")" + 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 "${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 "$_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 "$_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 "$_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 "$_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 "$_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" + [ -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" + echo "$tmpf" } # ------------------------------------------------------------------------------ @@ -1001,103 +1001,103 @@ _build_current_app_vars_tmp() { # - 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)" + 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")" + # 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}" + # 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" - 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" + 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-) + # 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 + 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 + choose_and_set_storage_for_file "$vf" template + choose_and_set_storage_for_file "$vf" container - # Silent operation - no output message + # 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" + 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" } # ============================================================================== @@ -1113,785 +1113,785 @@ ensure_global_default_vars_file() { # - Allows user to customize all container settings # ------------------------------------------------------------------------------ advanced_settings() { - # Enter alternate screen buffer to prevent flicker between dialogs - tput smcup 2>/dev/null || true - trap 'tput rmcup 2>/dev/null || true' RETURN + # Enter alternate screen buffer to prevent flicker between dialogs + tput smcup 2>/dev/null || true + trap 'tput rmcup 2>/dev/null || true' RETURN - # Initialize defaults - TAGS="community-script;${var_tags:-}" - local STEP=1 - local MAX_STEP=28 + # Initialize defaults + TAGS="community-script;${var_tags:-}" + local STEP=1 + local MAX_STEP=28 - # Store values for back navigation - inherit from var_* app defaults - local _ct_type="${var_unprivileged:-1}" - local _pw="" - local _pw_display="Automatic Login" - local _ct_id="$NEXTID" - local _hostname="$NSAPP" - local _disk_size="${var_disk:-4}" - local _core_count="${var_cpu:-1}" - local _ram_size="${var_ram:-1024}" - local _bridge="${var_brg:-vmbr0}" - local _net="${var_net:-dhcp}" - local _gate="${var_gateway:-}" - local _ipv6_method="${var_ipv6_method:-auto}" - local _ipv6_addr="" - local _ipv6_gate="" - local _apt_cacher="${var_apt_cacher:-no}" - local _apt_cacher_ip="${var_apt_cacher_ip:-}" - local _mtu="${var_mtu:-}" - local _sd="${var_searchdomain:-}" - local _ns="${var_ns:-}" - local _mac="${var_mac:-}" - local _vlan="${var_vlan:-}" - local _tags="$TAGS" - local _enable_fuse="${var_fuse:-no}" - local _enable_tun="${var_tun:-no}" - local _enable_gpu="${var_gpu:-no}" - local _enable_nesting="${var_nesting:-1}" - local _verbose="${var_verbose:-no}" - local _enable_keyctl="${var_keyctl:-0}" - local _enable_mknod="${var_mknod:-0}" - local _mount_fs="${var_mount_fs:-}" - local _protect_ct="${var_protection:-no}" + # Store values for back navigation - inherit from var_* app defaults + local _ct_type="${var_unprivileged:-1}" + local _pw="" + local _pw_display="Automatic Login" + local _ct_id="$NEXTID" + local _hostname="$NSAPP" + local _disk_size="${var_disk:-4}" + local _core_count="${var_cpu:-1}" + local _ram_size="${var_ram:-1024}" + local _bridge="${var_brg:-vmbr0}" + local _net="${var_net:-dhcp}" + local _gate="${var_gateway:-}" + local _ipv6_method="${var_ipv6_method:-auto}" + local _ipv6_addr="" + local _ipv6_gate="" + local _apt_cacher="${var_apt_cacher:-no}" + local _apt_cacher_ip="${var_apt_cacher_ip:-}" + local _mtu="${var_mtu:-}" + local _sd="${var_searchdomain:-}" + local _ns="${var_ns:-}" + local _mac="${var_mac:-}" + local _vlan="${var_vlan:-}" + local _tags="$TAGS" + local _enable_fuse="${var_fuse:-no}" + local _enable_tun="${var_tun:-no}" + local _enable_gpu="${var_gpu:-no}" + local _enable_nesting="${var_nesting:-1}" + local _verbose="${var_verbose:-no}" + local _enable_keyctl="${var_keyctl:-0}" + local _enable_mknod="${var_mknod:-0}" + local _mount_fs="${var_mount_fs:-}" + local _protect_ct="${var_protection:-no}" - # Detect host timezone for default (if not set via var_timezone) - local _host_timezone="" - if command -v timedatectl >/dev/null 2>&1; then - _host_timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "") - elif [ -f /etc/timezone ]; then - _host_timezone=$(cat /etc/timezone 2>/dev/null || echo "") - fi - local _ct_timezone="${var_timezone:-$_host_timezone}" - - # Helper to show current progress - show_progress() { - local current=$1 - local total=$MAX_STEP - echo -e "\n${INFO}${BOLD}${DGN}Step $current of $total${CL}" - } - - # Detect available bridges (do this once) - local BRIDGES="" - local BRIDGE_MENU_OPTIONS=() - _detect_bridges() { - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f 2>/dev/null) - BRIDGES="" - local OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - local iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" 2>/dev/null | cut -d':' -f1 && wc -l "${iface_filepath}" 2>/dev/null | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" 2>/dev/null || true - if [ -f "${iface_indexes_tmpfile}" ]; then - while read -r pair; do - local start=$(echo "${pair}" | cut -d':' -f1) - local end=$(echo "${pair}" | cut -d':' -f2) - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" 2>/dev/null | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - local 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) - - # Build bridge menu - BRIDGE_MENU_OPTIONS=() - if [[ -n "$BRIDGES" ]]; then - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//') - BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }") - fi - done <<<"$BRIDGES" + # Detect host timezone for default (if not set via var_timezone) + local _host_timezone="" + if command -v timedatectl >/dev/null 2>&1; then + _host_timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "") + elif [ -f /etc/timezone ]; then + _host_timezone=$(cat /etc/timezone 2>/dev/null || echo "") fi - } - _detect_bridges + local _ct_timezone="${var_timezone:-$_host_timezone}" - # Main wizard loop - while [ $STEP -le $MAX_STEP ]; do - case $STEP in + # Helper to show current progress + show_progress() { + local current=$1 + local total=$MAX_STEP + echo -e "\n${INFO}${BOLD}${DGN}Step $current of $total${CL}" + } - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 1: Container Type - # ═══════════════════════════════════════════════════════════════════════════ - 1) - local default_on="ON" - local default_off="OFF" - [[ "$_ct_type" == "0" ]] && { - default_on="OFF" - default_off="ON" - } - - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "CONTAINER TYPE" \ - --ok-button "Next" --cancel-button "Exit" \ - --radiolist "\nChoose container type:\n\nUse SPACE to select, ENTER to confirm." 14 58 2 \ - "1" "Unprivileged (recommended)" $default_on \ - "0" "Privileged" $default_off \ - 3>&1 1>&2 2>&3); then - [[ -n "$result" ]] && _ct_type="$result" - ((STEP++)) - else - exit_script - fi - ;; - - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 2: Root Password - # ═══════════════════════════════════════════════════════════════════════════ - 2) - if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "ROOT PASSWORD" \ - --ok-button "Next" --cancel-button "Back" \ - --passwordbox "\nSet Root Password (needed for root ssh access)\n\nLeave blank for automatic login (no password)" 12 58 \ - 3>&1 1>&2 2>&3); then - - if [[ -z "$PW1" ]]; then - _pw="" - _pw_display="Automatic Login" - ((STEP++)) - elif [[ "$PW1" == *" "* ]]; then - whiptail --msgbox "Password cannot contain spaces." 8 58 - elif ((${#PW1} < 5)); then - whiptail --msgbox "Password must be at least 5 characters." 8 58 - else - # Verify password - if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "PASSWORD VERIFICATION" \ - --ok-button "Confirm" --cancel-button "Back" \ - --passwordbox "\nVerify Root Password" 10 58 \ - 3>&1 1>&2 2>&3); then - if [[ "$PW1" == "$PW2" ]]; then - _pw="-password $PW1" - _pw_display="********" - ((STEP++)) - else - whiptail --msgbox "Passwords do not match. Please try again." 8 58 + # Detect available bridges (do this once) + local BRIDGES="" + local BRIDGE_MENU_OPTIONS=() + _detect_bridges() { + IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f 2>/dev/null) + BRIDGES="" + local OLD_IFS=$IFS + IFS=$'\n' + for iface_filepath in ${IFACE_FILEPATH_LIST}; do + local iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') + (grep -Pn '^\s*iface' "${iface_filepath}" 2>/dev/null | cut -d':' -f1 && wc -l "${iface_filepath}" 2>/dev/null | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" 2>/dev/null || true + if [ -f "${iface_indexes_tmpfile}" ]; then + while read -r pair; do + local start=$(echo "${pair}" | cut -d':' -f1) + local end=$(echo "${pair}" | cut -d':' -f2) + if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" 2>/dev/null | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then + local 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 - else - ((STEP--)) - fi - fi - else - ((STEP--)) - fi - ;; + done + IFS=$OLD_IFS + BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 3: Container ID - # ═══════════════════════════════════════════════════════════════════════════ - 3) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "CONTAINER ID" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Container ID" 10 58 "$_ct_id" \ - 3>&1 1>&2 2>&3); then - _ct_id="${result:-$NEXTID}" - ((STEP++)) - else - ((STEP--)) - fi - ;; - - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 4: Hostname - # ═══════════════════════════════════════════════════════════════════════════ - 4) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "HOSTNAME" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Hostname (lowercase, alphanumeric, hyphens only)" 10 58 "$_hostname" \ - 3>&1 1>&2 2>&3); then - local hn_test="${result:-$NSAPP}" - hn_test=$(echo "${hn_test,,}" | tr -d ' ') - if [[ "$hn_test" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - _hostname="$hn_test" - ((STEP++)) - else - whiptail --msgbox "Invalid hostname: '$hn_test'\n\nOnly lowercase letters, digits and hyphens are allowed." 10 58 - fi - else - ((STEP--)) - fi - ;; - - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 5: Disk Size - # ═══════════════════════════════════════════════════════════════════════════ - 5) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "DISK SIZE" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Disk Size in GB" 10 58 "$_disk_size" \ - 3>&1 1>&2 2>&3); then - local disk_test="${result:-$var_disk}" - if [[ "$disk_test" =~ ^[1-9][0-9]*$ ]]; then - _disk_size="$disk_test" - ((STEP++)) - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - else - ((STEP--)) - fi - ;; - - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 6: CPU Cores - # ═══════════════════════════════════════════════════════════════════════════ - 6) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "CPU CORES" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nAllocate CPU Cores" 10 58 "$_core_count" \ - 3>&1 1>&2 2>&3); then - local cpu_test="${result:-$var_cpu}" - if [[ "$cpu_test" =~ ^[1-9][0-9]*$ ]]; then - _core_count="$cpu_test" - ((STEP++)) - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - else - ((STEP--)) - fi - ;; - - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 7: RAM Size - # ═══════════════════════════════════════════════════════════════════════════ - 7) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "RAM SIZE" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nAllocate RAM in MiB" 10 58 "$_ram_size" \ - 3>&1 1>&2 2>&3); then - local ram_test="${result:-$var_ram}" - if [[ "$ram_test" =~ ^[1-9][0-9]*$ ]]; then - _ram_size="$ram_test" - ((STEP++)) - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - else - ((STEP--)) - fi - ;; - - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 8: Network Bridge - # ═══════════════════════════════════════════════════════════════════════════ - 8) - if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then - _bridge="vmbr0" - ((STEP++)) - else - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "NETWORK BRIDGE" \ - --ok-button "Next" --cancel-button "Back" \ - --menu "\nSelect network bridge:" 16 58 6 \ - "${BRIDGE_MENU_OPTIONS[@]}" \ - 3>&1 1>&2 2>&3); then - _bridge="${result:-vmbr0}" - ((STEP++)) - else - ((STEP--)) - fi - fi - ;; - - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 9: IPv4 Configuration - # ═══════════════════════════════════════════════════════════════════════════ - 9) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "IPv4 CONFIGURATION" \ - --ok-button "Next" --cancel-button "Back" \ - --menu "\nSelect IPv4 Address Assignment:" 14 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3); then - - if [[ "$result" == "static" ]]; then - # Get static IP - local static_ip - if static_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "STATIC IPv4 ADDRESS" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \ - 3>&1 1>&2 2>&3); then - if [[ "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - # Get gateway - local gateway_ip - if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "GATEWAY IP" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nEnter Gateway IP address" 10 58 "" \ - 3>&1 1>&2 2>&3); then - if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - _net="$static_ip" - _gate=",gw=$gateway_ip" - ((STEP++)) - else - whiptail --msgbox "Invalid Gateway IP format." 8 58 + # Build bridge menu + BRIDGE_MENU_OPTIONS=() + if [[ -n "$BRIDGES" ]]; then + while IFS= read -r bridge; do + if [[ -n "$bridge" ]]; then + local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//') + BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }") fi - fi + done <<<"$BRIDGES" + fi + } + _detect_bridges + + # Main wizard loop + while [ $STEP -le $MAX_STEP ]; do + case $STEP in + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 1: Container Type + # ═══════════════════════════════════════════════════════════════════════════ + 1) + local default_on="ON" + local default_off="OFF" + [[ "$_ct_type" == "0" ]] && { + default_on="OFF" + default_off="ON" + } + + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER TYPE" \ + --ok-button "Next" --cancel-button "Exit" \ + --radiolist "\nChoose container type:\n\nUse SPACE to select, ENTER to confirm." 14 58 2 \ + "1" "Unprivileged (recommended)" $default_on \ + "0" "Privileged" $default_off \ + 3>&1 1>&2 2>&3); then + [[ -n "$result" ]] && _ct_type="$result" + ((STEP++)) else - whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58 + exit_script fi - fi - else - _net="dhcp" - _gate="" - ((STEP++)) - fi - else - ((STEP--)) - fi - ;; + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 10: IPv6 Configuration - # ═══════════════════════════════════════════════════════════════════════════ - 10) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "IPv6 CONFIGURATION" \ - --ok-button "Next" --cancel-button "Back" \ - --menu "\nSelect IPv6 Address Management:" 16 70 5 \ - "auto" "SLAAC/AUTO (recommended) - Dynamic IPv6 from network" \ - "dhcp" "DHCPv6 - DHCP-assigned IPv6 address" \ - "static" "Static - Manual IPv6 address configuration" \ - "none" "None - No IPv6 assignment (most containers)" \ - "disable" "Fully Disabled - (breaks some services)" \ - 3>&1 1>&2 2>&3); then + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 2: Root Password + # ═══════════════════════════════════════════════════════════════════════════ + 2) + if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "ROOT PASSWORD" \ + --ok-button "Next" --cancel-button "Back" \ + --passwordbox "\nSet Root Password (needed for root ssh access)\n\nLeave blank for automatic login (no password)" 12 58 \ + 3>&1 1>&2 2>&3); then - _ipv6_method="$result" - case "$result" in - static) - local ipv6_addr - if ipv6_addr=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "STATIC IPv6 ADDRESS" \ - --inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \ - 3>&1 1>&2 2>&3); then - if [[ "$ipv6_addr" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - _ipv6_addr="$ipv6_addr" - # Optional gateway - _ipv6_gate=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "IPv6 GATEWAY" \ - --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ - 3>&1 1>&2 2>&3) || true - ((STEP++)) + if [[ -z "$PW1" ]]; then + _pw="" + _pw_display="Automatic Login" + ((STEP++)) + elif [[ "$PW1" == *" "* ]]; then + whiptail --msgbox "Password cannot contain spaces." 8 58 + elif ((${#PW1} < 5)); then + whiptail --msgbox "Password must be at least 5 characters." 8 58 + else + # Verify password + if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "PASSWORD VERIFICATION" \ + --ok-button "Confirm" --cancel-button "Back" \ + --passwordbox "\nVerify Root Password" 10 58 \ + 3>&1 1>&2 2>&3); then + if [[ "$PW1" == "$PW2" ]]; then + _pw="-password $PW1" + _pw_display="********" + ((STEP++)) + else + whiptail --msgbox "Passwords do not match. Please try again." 8 58 + fi + else + ((STEP--)) + fi + fi else - whiptail --msgbox "Invalid IPv6 CIDR format." 8 58 + ((STEP--)) fi - fi - ;; - dhcp) - _ipv6_addr="dhcp" - _ipv6_gate="" - ((STEP++)) - ;; - disable) - _ipv6_addr="" - _ipv6_gate="" - ((STEP++)) - ;; - none) - _ipv6_addr="none" - _ipv6_gate="" - ((STEP++)) - ;; - *) - _ipv6_addr="" - _ipv6_gate="" - ((STEP++)) - ;; - esac - else - ((STEP--)) - fi - ;; + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 11: MTU Size - # ═══════════════════════════════════════════════════════════════════════════ - 11) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "MTU SIZE" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500)" 12 58 "" \ - 3>&1 1>&2 2>&3); then - _mtu="$result" - ((STEP++)) - else - ((STEP--)) - fi - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 3: Container ID + # ═══════════════════════════════════════════════════════════════════════════ + 3) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER ID" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Container ID" 10 58 "$_ct_id" \ + 3>&1 1>&2 2>&3); then + _ct_id="${result:-$NEXTID}" + ((STEP++)) + else + ((STEP--)) + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 12: DNS Search Domain - # ═══════════════════════════════════════════════════════════════════════════ - 12) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "DNS SEARCH DOMAIN" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet DNS Search Domain\n(leave blank to use host setting)" 12 58 "" \ - 3>&1 1>&2 2>&3); then - _sd="$result" - ((STEP++)) - else - ((STEP--)) - fi - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 4: Hostname + # ═══════════════════════════════════════════════════════════════════════════ + 4) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "HOSTNAME" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Hostname (lowercase, alphanumeric, hyphens only)" 10 58 "$_hostname" \ + 3>&1 1>&2 2>&3); then + local hn_test="${result:-$NSAPP}" + hn_test=$(echo "${hn_test,,}" | tr -d ' ') + if [[ "$hn_test" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then + _hostname="$hn_test" + ((STEP++)) + else + whiptail --msgbox "Invalid hostname: '$hn_test'\n\nOnly lowercase letters, digits and hyphens are allowed." 10 58 + fi + else + ((STEP--)) + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 13: DNS Server - # ═══════════════════════════════════════════════════════════════════════════ - 13) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "DNS SERVER" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet DNS Server IP\n(leave blank to use host setting)" 12 58 "" \ - 3>&1 1>&2 2>&3); then - _ns="$result" - ((STEP++)) - else - ((STEP--)) - fi - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 5: Disk Size + # ═══════════════════════════════════════════════════════════════════════════ + 5) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "DISK SIZE" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Disk Size in GB" 10 58 "$_disk_size" \ + 3>&1 1>&2 2>&3); then + local disk_test="${result:-$var_disk}" + if [[ "$disk_test" =~ ^[1-9][0-9]*$ ]]; then + _disk_size="$disk_test" + ((STEP++)) + else + whiptail --msgbox "Disk size must be a positive integer!" 8 58 + fi + else + ((STEP--)) + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 14: MAC Address - # ═══════════════════════════════════════════════════════════════════════════ - 14) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "MAC ADDRESS" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet MAC Address\n(leave blank for auto-generated)" 12 58 "" \ - 3>&1 1>&2 2>&3); then - _mac="$result" - ((STEP++)) - else - ((STEP--)) - fi - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 6: CPU Cores + # ═══════════════════════════════════════════════════════════════════════════ + 6) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CPU CORES" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nAllocate CPU Cores" 10 58 "$_core_count" \ + 3>&1 1>&2 2>&3); then + local cpu_test="${result:-$var_cpu}" + if [[ "$cpu_test" =~ ^[1-9][0-9]*$ ]]; then + _core_count="$cpu_test" + ((STEP++)) + else + whiptail --msgbox "CPU core count must be a positive integer!" 8 58 + fi + else + ((STEP--)) + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 15: VLAN Tag - # ═══════════════════════════════════════════════════════════════════════════ - 15) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "VLAN TAG" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet VLAN Tag\n(leave blank for no VLAN)" 12 58 "" \ - 3>&1 1>&2 2>&3); then - _vlan="$result" - ((STEP++)) - else - ((STEP--)) - fi - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 7: RAM Size + # ═══════════════════════════════════════════════════════════════════════════ + 7) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "RAM SIZE" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nAllocate RAM in MiB" 10 58 "$_ram_size" \ + 3>&1 1>&2 2>&3); then + local ram_test="${result:-$var_ram}" + if [[ "$ram_test" =~ ^[1-9][0-9]*$ ]]; then + _ram_size="$ram_test" + ((STEP++)) + else + whiptail --msgbox "RAM size must be a positive integer!" 8 58 + fi + else + ((STEP--)) + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 16: Tags - # ═══════════════════════════════════════════════════════════════════════════ - 16) - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "CONTAINER TAGS" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Custom Tags (semicolon-separated)\n(remove all for no tags)" 12 58 "$_tags" \ - 3>&1 1>&2 2>&3); then - _tags="${result:-;}" - _tags=$(echo "$_tags" | tr -d '[:space:]') - ((STEP++)) - else - ((STEP--)) - fi - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 8: Network Bridge + # ═══════════════════════════════════════════════════════════════════════════ + 8) + if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then + _bridge="vmbr0" + ((STEP++)) + else + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "NETWORK BRIDGE" \ + --ok-button "Next" --cancel-button "Back" \ + --menu "\nSelect network bridge:" 16 58 6 \ + "${BRIDGE_MENU_OPTIONS[@]}" \ + 3>&1 1>&2 2>&3); then + _bridge="${result:-vmbr0}" + ((STEP++)) + else + ((STEP--)) + fi + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 17: SSH Settings - # ═══════════════════════════════════════════════════════════════════════════ - 17) - configure_ssh_settings "Step $STEP/$MAX_STEP" - # configure_ssh_settings handles its own flow, always advance - ((STEP++)) - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 9: IPv4 Configuration + # ═══════════════════════════════════════════════════════════════════════════ + 9) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "IPv4 CONFIGURATION" \ + --ok-button "Next" --cancel-button "Back" \ + --menu "\nSelect IPv4 Address Assignment:" 14 60 2 \ + "dhcp" "Automatic (DHCP, recommended)" \ + "static" "Static (manual entry)" \ + 3>&1 1>&2 2>&3); then - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 18: FUSE Support - # ═══════════════════════════════════════════════════════════════════════════ - 18) - local fuse_default_flag="--defaultno" - [[ "$_enable_fuse" == "yes" || "$_enable_fuse" == "1" ]] && fuse_default_flag="" + if [[ "$result" == "static" ]]; then + # Get static IP + local static_ip + if static_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "STATIC IPv4 ADDRESS" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + if [[ "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then + # Get gateway + local gateway_ip + if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "GATEWAY IP" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nEnter Gateway IP address" 10 58 "" \ + 3>&1 1>&2 2>&3); then + if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + _net="$static_ip" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Invalid Gateway IP format." 8 58 + fi + fi + else + whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58 + fi + fi + else + _net="dhcp" + _gate="" + ((STEP++)) + fi + else + ((STEP--)) + fi + ;; - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "FUSE SUPPORT" \ - --ok-button "Next" --cancel-button "Back" \ - $fuse_default_flag \ - --yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc.\n\n(App default: ${var_fuse:-no})" 14 58; then - _enable_fuse="yes" - else - if [ $? -eq 1 ]; then - _enable_fuse="no" - else - ((STEP--)) - continue - fi - fi - ((STEP++)) - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 10: IPv6 Configuration + # ═══════════════════════════════════════════════════════════════════════════ + 10) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "IPv6 CONFIGURATION" \ + --ok-button "Next" --cancel-button "Back" \ + --menu "\nSelect IPv6 Address Management:" 16 70 5 \ + "auto" "SLAAC/AUTO (recommended) - Dynamic IPv6 from network" \ + "dhcp" "DHCPv6 - DHCP-assigned IPv6 address" \ + "static" "Static - Manual IPv6 address configuration" \ + "none" "None - No IPv6 assignment (most containers)" \ + "disable" "Fully Disabled - (breaks some services)" \ + 3>&1 1>&2 2>&3); then - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 19: TUN/TAP Support - # ═══════════════════════════════════════════════════════════════════════════ - 19) - local tun_default_flag="--defaultno" - [[ "$_enable_tun" == "yes" || "$_enable_tun" == "1" ]] && tun_default_flag="" + _ipv6_method="$result" + case "$result" in + static) + local ipv6_addr + if ipv6_addr=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "STATIC IPv6 ADDRESS" \ + --inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + if [[ "$ipv6_addr" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then + _ipv6_addr="$ipv6_addr" + # Optional gateway + _ipv6_gate=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "IPv6 GATEWAY" \ + --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ + 3>&1 1>&2 2>&3) || true + ((STEP++)) + else + whiptail --msgbox "Invalid IPv6 CIDR format." 8 58 + fi + fi + ;; + dhcp) + _ipv6_addr="dhcp" + _ipv6_gate="" + ((STEP++)) + ;; + disable) + _ipv6_addr="" + _ipv6_gate="" + ((STEP++)) + ;; + none) + _ipv6_addr="none" + _ipv6_gate="" + ((STEP++)) + ;; + *) + _ipv6_addr="" + _ipv6_gate="" + ((STEP++)) + ;; + esac + else + ((STEP--)) + fi + ;; - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "TUN/TAP SUPPORT" \ - --ok-button "Next" --cancel-button "Back" \ - $tun_default_flag \ - --yesno "\nEnable TUN/TAP device support?\n\nRequired for: VPN apps (WireGuard, OpenVPN, Tailscale),\nnetwork tunneling, and containerized networking.\n\n(App default: ${var_tun:-no})" 14 62; then - _enable_tun="yes" - else - if [ $? -eq 1 ]; then - _enable_tun="no" - else - ((STEP--)) - continue - fi - fi - ((STEP++)) - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 11: MTU Size + # ═══════════════════════════════════════════════════════════════════════════ + 11) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "MTU SIZE" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _mtu="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 20: Nesting Support - # ═══════════════════════════════════════════════════════════════════════════ - 20) - local nesting_default_flag="" - [[ "$_enable_nesting" == "0" || "$_enable_nesting" == "no" ]] && nesting_default_flag="--defaultno" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 12: DNS Search Domain + # ═══════════════════════════════════════════════════════════════════════════ + 12) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "DNS SEARCH DOMAIN" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet DNS Search Domain\n(leave blank to use host setting)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _sd="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "NESTING SUPPORT" \ - --ok-button "Next" --cancel-button "Back" \ - $nesting_default_flag \ - --yesno "\nEnable Nesting?\n\nRequired for: Docker, LXC inside LXC, Podman,\nand other containerization tools.\n\n(App default: ${var_nesting:-1})" 14 58; then - _enable_nesting="1" - else - if [ $? -eq 1 ]; then - _enable_nesting="0" - else - ((STEP--)) - continue - fi - fi - ((STEP++)) - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 13: DNS Server + # ═══════════════════════════════════════════════════════════════════════════ + 13) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "DNS SERVER" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet DNS Server IP\n(leave blank to use host setting)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _ns="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 21: GPU Passthrough - # ═══════════════════════════════════════════════════════════════════════════ - 21) - local gpu_default_flag="--defaultno" - [[ "$_enable_gpu" == "yes" ]] && gpu_default_flag="" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 14: MAC Address + # ═══════════════════════════════════════════════════════════════════════════ + 14) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "MAC ADDRESS" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet MAC Address\n(leave blank for auto-generated)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _mac="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "GPU PASSTHROUGH" \ - --ok-button "Next" --cancel-button "Back" \ - $gpu_default_flag \ - --yesno "\nEnable GPU Passthrough?\n\nAutomatically detects and passes through available GPUs\n(Intel/AMD/NVIDIA) for hardware acceleration.\n\nRecommended for: Media servers, AI/ML, Transcoding\n\n(App default: ${var_gpu:-no})" 16 62; then - _enable_gpu="yes" - else - if [ $? -eq 1 ]; then - _enable_gpu="no" - else - ((STEP--)) - continue - fi - fi - ((STEP++)) - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 15: VLAN Tag + # ═══════════════════════════════════════════════════════════════════════════ + 15) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "VLAN TAG" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet VLAN Tag\n(leave blank for no VLAN)" 12 58 "" \ + 3>&1 1>&2 2>&3); then + _vlan="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 22: Keyctl Support (Docker/systemd) - # ═══════════════════════════════════════════════════════════════════════════ - 22) - local keyctl_default_flag="--defaultno" - [[ "$_enable_keyctl" == "1" ]] && keyctl_default_flag="" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 16: Tags + # ═══════════════════════════════════════════════════════════════════════════ + 16) + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER TAGS" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet Custom Tags (semicolon-separated)\n(remove all for no tags)" 12 58 "$_tags" \ + 3>&1 1>&2 2>&3); then + _tags="${result:-;}" + _tags=$(echo "$_tags" | tr -d '[:space:]') + ((STEP++)) + else + ((STEP--)) + fi + ;; - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "KEYCTL SUPPORT" \ - --ok-button "Next" --cancel-button "Back" \ - $keyctl_default_flag \ - --yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\nNote: Automatically enabled for unprivileged containers.\n\n(App default: ${var_keyctl:-0})" 16 62; then - _enable_keyctl="1" - else - if [ $? -eq 1 ]; then - _enable_keyctl="0" - else - ((STEP--)) - continue - fi - fi - ((STEP++)) - ;; + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 17: SSH Settings + # ═══════════════════════════════════════════════════════════════════════════ + 17) + configure_ssh_settings "Step $STEP/$MAX_STEP" + # configure_ssh_settings handles its own flow, always advance + ((STEP++)) + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 23: APT Cacher Proxy - # ═══════════════════════════════════════════════════════════════════════════ - 23) - local apt_cacher_default_flag="--defaultno" - [[ "$_apt_cacher" == "yes" ]] && apt_cacher_default_flag="" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 18: FUSE Support + # ═══════════════════════════════════════════════════════════════════════════ + 18) + local fuse_default_flag="--defaultno" + [[ "$_enable_fuse" == "yes" || "$_enable_fuse" == "1" ]] && fuse_default_flag="" - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "APT CACHER PROXY" \ - --ok-button "Next" --cancel-button "Back" \ - $apt_cacher_default_flag \ - --yesno "\nUse APT Cacher-NG proxy?\n\nSpeeds up package downloads by caching them locally.\nRequires apt-cacher-ng running on your network.\n\n(App default: ${var_apt_cacher:-no})" 14 62; then - _apt_cacher="yes" - # Ask for IP if enabled - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "APT CACHER IP" \ - --inputbox "\nEnter APT Cacher-NG server IP address:" 10 58 "$_apt_cacher_ip" \ - 3>&1 1>&2 2>&3); then - _apt_cacher_ip="$result" - fi - else - if [ $? -eq 1 ]; then - _apt_cacher="no" - _apt_cacher_ip="" - else - ((STEP--)) - continue - fi - fi - ((STEP++)) - ;; + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "FUSE SUPPORT" \ + --ok-button "Next" --cancel-button "Back" \ + $fuse_default_flag \ + --yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc.\n\n(App default: ${var_fuse:-no})" 14 58; then + _enable_fuse="yes" + else + if [ $? -eq 1 ]; then + _enable_fuse="no" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 24: Container Timezone - # ═══════════════════════════════════════════════════════════════════════════ - 24) - local tz_hint="$_ct_timezone" - [[ -z "$tz_hint" ]] && tz_hint="(empty - will use host timezone)" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 19: TUN/TAP Support + # ═══════════════════════════════════════════════════════════════════════════ + 19) + local tun_default_flag="--defaultno" + [[ "$_enable_tun" == "yes" || "$_enable_tun" == "1" ]] && tun_default_flag="" - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "CONTAINER TIMEZONE" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${_host_timezone:-unknown}\n\nLeave empty to inherit from host." 16 62 "$_ct_timezone" \ - 3>&1 1>&2 2>&3); then - _ct_timezone="$result" - ((STEP++)) - else - ((STEP--)) - fi - ;; + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "TUN/TAP SUPPORT" \ + --ok-button "Next" --cancel-button "Back" \ + $tun_default_flag \ + --yesno "\nEnable TUN/TAP device support?\n\nRequired for: VPN apps (WireGuard, OpenVPN, Tailscale),\nnetwork tunneling, and containerized networking.\n\n(App default: ${var_tun:-no})" 14 62; then + _enable_tun="yes" + else + if [ $? -eq 1 ]; then + _enable_tun="no" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 25: Container Protection - # ═══════════════════════════════════════════════════════════════════════════ - 25) - local protect_default_flag="--defaultno" - [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_default_flag="" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 20: Nesting Support + # ═══════════════════════════════════════════════════════════════════════════ + 20) + local nesting_default_flag="" + [[ "$_enable_nesting" == "0" || "$_enable_nesting" == "no" ]] && nesting_default_flag="--defaultno" - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "CONTAINER PROTECTION" \ - --ok-button "Next" --cancel-button "Back" \ - $protect_default_flag \ - --yesno "\nEnable Container Protection?\n\nPrevents accidental deletion of this container.\nYou must disable protection before removing.\n\n(App default: ${var_protection:-no})" 14 62; then - _protect_ct="yes" - else - if [ $? -eq 1 ]; then - _protect_ct="no" - else - ((STEP--)) - continue - fi - fi - ((STEP++)) - ;; + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "NESTING SUPPORT" \ + --ok-button "Next" --cancel-button "Back" \ + $nesting_default_flag \ + --yesno "\nEnable Nesting?\n\nRequired for: Docker, LXC inside LXC, Podman,\nand other containerization tools.\n\n(App default: ${var_nesting:-1})" 14 58; then + _enable_nesting="1" + else + if [ $? -eq 1 ]; then + _enable_nesting="0" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 26: Device Node Creation (mknod) - # ═══════════════════════════════════════════════════════════════════════════ - 26) - local mknod_default_flag="--defaultno" - [[ "$_enable_mknod" == "1" ]] && mknod_default_flag="" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 21: GPU Passthrough + # ═══════════════════════════════════════════════════════════════════════════ + 21) + local gpu_default_flag="--defaultno" + [[ "$_enable_gpu" == "yes" ]] && gpu_default_flag="" - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "DEVICE NODE CREATION" \ - --ok-button "Next" --cancel-button "Back" \ - $mknod_default_flag \ - --yesno "\nAllow device node creation (mknod)?\n\nRequired for: Creating device files inside container.\nExperimental feature (requires kernel 5.3+).\n\n(App default: ${var_mknod:-0})" 14 62; then - _enable_mknod="1" - else - if [ $? -eq 1 ]; then - _enable_mknod="0" - else - ((STEP--)) - continue - fi - fi - ((STEP++)) - ;; + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "GPU PASSTHROUGH" \ + --ok-button "Next" --cancel-button "Back" \ + $gpu_default_flag \ + --yesno "\nEnable GPU Passthrough?\n\nAutomatically detects and passes through available GPUs\n(Intel/AMD/NVIDIA) for hardware acceleration.\n\nRecommended for: Media servers, AI/ML, Transcoding\n\n(App default: ${var_gpu:-no})" 16 62; then + _enable_gpu="yes" + else + if [ $? -eq 1 ]; then + _enable_gpu="no" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 27: Mount Filesystems - # ═══════════════════════════════════════════════════════════════════════════ - 27) - local mount_hint="" - [[ -n "$_mount_fs" ]] && mount_hint="$_mount_fs" || mount_hint="(none)" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 22: Keyctl Support (Docker/systemd) + # ═══════════════════════════════════════════════════════════════════════════ + 22) + local keyctl_default_flag="--defaultno" + [[ "$_enable_keyctl" == "1" ]] && keyctl_default_flag="" - if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "MOUNT FILESYSTEMS" \ - --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint" 14 62 "$_mount_fs" \ - 3>&1 1>&2 2>&3); then - _mount_fs="$result" - ((STEP++)) - else - ((STEP--)) - fi - ;; + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "KEYCTL SUPPORT" \ + --ok-button "Next" --cancel-button "Back" \ + $keyctl_default_flag \ + --yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\nNote: Automatically enabled for unprivileged containers.\n\n(App default: ${var_keyctl:-0})" 16 62; then + _enable_keyctl="1" + else + if [ $? -eq 1 ]; then + _enable_keyctl="0" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; - # ═══════════════════════════════════════════════════════════════════════════ - # STEP 28: Verbose Mode & Confirmation - # ═══════════════════════════════════════════════════════════════════════════ - 28) - local verbose_default_flag="--defaultno" - [[ "$_verbose" == "yes" ]] && verbose_default_flag="" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 23: APT Cacher Proxy + # ═══════════════════════════════════════════════════════════════════════════ + 23) + local apt_cacher_default_flag="--defaultno" + [[ "$_apt_cacher" == "yes" ]] && apt_cacher_default_flag="" - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "VERBOSE MODE" \ - $verbose_default_flag \ - --yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then - _verbose="yes" - else - _verbose="no" - fi - # Build summary - local ct_type_desc="Unprivileged" - [[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged" + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "APT CACHER PROXY" \ + --ok-button "Next" --cancel-button "Back" \ + $apt_cacher_default_flag \ + --yesno "\nUse APT Cacher-NG proxy?\n\nSpeeds up package downloads by caching them locally.\nRequires apt-cacher-ng running on your network.\n\n(App default: ${var_apt_cacher:-no})" 14 62; then + _apt_cacher="yes" + # Ask for IP if enabled + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "APT CACHER IP" \ + --inputbox "\nEnter APT Cacher-NG server IP address:" 10 58 "$_apt_cacher_ip" \ + 3>&1 1>&2 2>&3); then + _apt_cacher_ip="$result" + fi + else + if [ $? -eq 1 ]; then + _apt_cacher="no" + _apt_cacher_ip="" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; - local nesting_desc="Disabled" - [[ "$_enable_nesting" == "1" ]] && nesting_desc="Enabled" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 24: Container Timezone + # ═══════════════════════════════════════════════════════════════════════════ + 24) + local tz_hint="$_ct_timezone" + [[ -z "$tz_hint" ]] && tz_hint="(empty - will use host timezone)" - local keyctl_desc="Disabled" - [[ "$_enable_keyctl" == "1" ]] && keyctl_desc="Enabled" + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER TIMEZONE" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${_host_timezone:-unknown}\n\nLeave empty to inherit from host." 16 62 "$_ct_timezone" \ + 3>&1 1>&2 2>&3); then + _ct_timezone="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; - local protect_desc="No" - [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_desc="Yes" + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 25: Container Protection + # ═══════════════════════════════════════════════════════════════════════════ + 25) + local protect_default_flag="--defaultno" + [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_default_flag="" - local tz_display="${_ct_timezone:-Host TZ}" - local apt_display="${_apt_cacher:-no}" - [[ "$_apt_cacher" == "yes" && -n "$_apt_cacher_ip" ]] && apt_display="$_apt_cacher_ip" + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONTAINER PROTECTION" \ + --ok-button "Next" --cancel-button "Back" \ + $protect_default_flag \ + --yesno "\nEnable Container Protection?\n\nPrevents accidental deletion of this container.\nYou must disable protection before removing.\n\n(App default: ${var_protection:-no})" 14 62; then + _protect_ct="yes" + else + if [ $? -eq 1 ]; then + _protect_ct="no" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; - local summary="Container Type: $ct_type_desc + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 26: Device Node Creation (mknod) + # ═══════════════════════════════════════════════════════════════════════════ + 26) + local mknod_default_flag="--defaultno" + [[ "$_enable_mknod" == "1" ]] && mknod_default_flag="" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "DEVICE NODE CREATION" \ + --ok-button "Next" --cancel-button "Back" \ + $mknod_default_flag \ + --yesno "\nAllow device node creation (mknod)?\n\nRequired for: Creating device files inside container.\nExperimental feature (requires kernel 5.3+).\n\n(App default: ${var_mknod:-0})" 14 62; then + _enable_mknod="1" + else + if [ $? -eq 1 ]; then + _enable_mknod="0" + else + ((STEP--)) + continue + fi + fi + ((STEP++)) + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 27: Mount Filesystems + # ═══════════════════════════════════════════════════════════════════════════ + 27) + local mount_hint="" + [[ -n "$_mount_fs" ]] && mount_hint="$_mount_fs" || mount_hint="(none)" + + if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "MOUNT FILESYSTEMS" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint" 14 62 "$_mount_fs" \ + 3>&1 1>&2 2>&3); then + _mount_fs="$result" + ((STEP++)) + else + ((STEP--)) + fi + ;; + + # ═══════════════════════════════════════════════════════════════════════════ + # STEP 28: Verbose Mode & Confirmation + # ═══════════════════════════════════════════════════════════════════════════ + 28) + local verbose_default_flag="--defaultno" + [[ "$_verbose" == "yes" ]] && verbose_default_flag="" + + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "VERBOSE MODE" \ + $verbose_default_flag \ + --yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then + _verbose="yes" + else + _verbose="no" + fi + # Build summary + local ct_type_desc="Unprivileged" + [[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged" + + local nesting_desc="Disabled" + [[ "$_enable_nesting" == "1" ]] && nesting_desc="Enabled" + + local keyctl_desc="Disabled" + [[ "$_enable_keyctl" == "1" ]] && keyctl_desc="Enabled" + + local protect_desc="No" + [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_desc="Yes" + + local tz_display="${_ct_timezone:-Host TZ}" + local apt_display="${_apt_cacher:-no}" + [[ "$_apt_cacher" == "yes" && -n "$_apt_cacher_ip" ]] && apt_display="$_apt_cacher_ip" + + local summary="Container Type: $ct_type_desc Container ID: $_ct_id Hostname: $_hostname @@ -1915,105 +1915,105 @@ Advanced: APT Cacher: $apt_display Verbose: $_verbose" - if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "CONFIRM SETTINGS" \ - --ok-button "Create LXC" --cancel-button "Back" \ - --yesno "$summary\n\nCreate ${APP} LXC with these settings?" 32 62; then - ((STEP++)) - else - ((STEP--)) - fi - ;; - esac - done + if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "CONFIRM SETTINGS" \ + --ok-button "Create LXC" --cancel-button "Back" \ + --yesno "$summary\n\nCreate ${APP} LXC with these settings?" 32 62; then + ((STEP++)) + else + ((STEP--)) + fi + ;; + esac + done - # ═══════════════════════════════════════════════════════════════════════════ - # Apply all collected values to global variables - # ═══════════════════════════════════════════════════════════════════════════ - CT_TYPE="$_ct_type" - PW="$_pw" - CT_ID="$_ct_id" - HN="$_hostname" - DISK_SIZE="$_disk_size" - CORE_COUNT="$_core_count" - RAM_SIZE="$_ram_size" - BRG="$_bridge" - NET="$_net" - GATE="$_gate" - IPV6_METHOD="$_ipv6_method" - IPV6_ADDR="$_ipv6_addr" - IPV6_GATE="$_ipv6_gate" - TAGS="$_tags" - ENABLE_FUSE="$_enable_fuse" - ENABLE_TUN="$_enable_tun" - ENABLE_GPU="$_enable_gpu" - ENABLE_NESTING="$_enable_nesting" - ENABLE_KEYCTL="$_enable_keyctl" - ENABLE_MKNOD="$_enable_mknod" - ALLOW_MOUNT_FS="$_mount_fs" - PROTECT_CT="$_protect_ct" - CT_TIMEZONE="$_ct_timezone" - APT_CACHER="$_apt_cacher" - APT_CACHER_IP="$_apt_cacher_ip" - VERBOSE="$_verbose" + # ═══════════════════════════════════════════════════════════════════════════ + # Apply all collected values to global variables + # ═══════════════════════════════════════════════════════════════════════════ + CT_TYPE="$_ct_type" + PW="$_pw" + CT_ID="$_ct_id" + HN="$_hostname" + DISK_SIZE="$_disk_size" + CORE_COUNT="$_core_count" + RAM_SIZE="$_ram_size" + BRG="$_bridge" + NET="$_net" + GATE="$_gate" + IPV6_METHOD="$_ipv6_method" + IPV6_ADDR="$_ipv6_addr" + IPV6_GATE="$_ipv6_gate" + TAGS="$_tags" + ENABLE_FUSE="$_enable_fuse" + ENABLE_TUN="$_enable_tun" + ENABLE_GPU="$_enable_gpu" + ENABLE_NESTING="$_enable_nesting" + ENABLE_KEYCTL="$_enable_keyctl" + ENABLE_MKNOD="$_enable_mknod" + ALLOW_MOUNT_FS="$_mount_fs" + PROTECT_CT="$_protect_ct" + CT_TIMEZONE="$_ct_timezone" + APT_CACHER="$_apt_cacher" + APT_CACHER_IP="$_apt_cacher_ip" + VERBOSE="$_verbose" - # Update var_* based on user choice (for functions that check these) - var_gpu="$_enable_gpu" - var_fuse="$_enable_fuse" - var_tun="$_enable_tun" - var_nesting="$_enable_nesting" - var_keyctl="$_enable_keyctl" - var_mknod="$_enable_mknod" - var_mount_fs="$_mount_fs" - var_protection="$_protect_ct" - var_timezone="$_ct_timezone" - var_apt_cacher="$_apt_cacher" - var_apt_cacher_ip="$_apt_cacher_ip" + # Update var_* based on user choice (for functions that check these) + var_gpu="$_enable_gpu" + var_fuse="$_enable_fuse" + var_tun="$_enable_tun" + var_nesting="$_enable_nesting" + var_keyctl="$_enable_keyctl" + var_mknod="$_enable_mknod" + var_mount_fs="$_mount_fs" + var_protection="$_protect_ct" + var_timezone="$_ct_timezone" + var_apt_cacher="$_apt_cacher" + var_apt_cacher_ip="$_apt_cacher_ip" - # Format optional values - [[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU="" - [[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD="" - [[ -n "$_ns" ]] && NS="-nameserver=$_ns" || NS="" - [[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC="" - [[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN="" + # Format optional values + [[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU="" + [[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD="" + [[ -n "$_ns" ]] && NS="-nameserver=$_ns" || NS="" + [[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC="" + [[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN="" - # Alpine UDHCPC fix - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ -n "$_ns" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - export SSH_KEYS_FILE + # Alpine UDHCPC fix + if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ -n "$_ns" ]; then + UDHCPC_FIX="yes" + else + UDHCPC_FIX="no" + fi + export UDHCPC_FIX + export SSH_KEYS_FILE - # Exit alternate screen buffer BEFORE displaying summary - # so the summary is visible in the main terminal - tput rmcup 2>/dev/null || true - trap - RETURN # Remove the trap since we already called rmcup + # Exit alternate screen buffer BEFORE displaying summary + # so the summary is visible in the main terminal + tput rmcup 2>/dev/null || true + trap - RETURN # Remove the trap since we already called rmcup - # Display final summary - echo -e "\n${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" == "1" ] && echo "Unprivileged" || echo "Privileged")${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${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}" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: ${BGN}$NET${CL}" - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}$IPV6_METHOD${CL}" - echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}$ENABLE_FUSE${CL}" - [[ "$ENABLE_TUN" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "$ENABLE_NESTING" == "1" ] && echo "Enabled" || echo "Disabled")${CL}" - [[ "$ENABLE_KEYCTL" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}" - echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}$ENABLE_GPU${CL}" - [[ "$PROTECT_CT" == "yes" || "$PROTECT_CT" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}" - [[ -n "$CT_TIMEZONE" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}" - [[ "$APT_CACHER" == "yes" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}" - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" - echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" + # Display final summary + echo -e "\n${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" == "1" ] && echo "Unprivileged" || echo "Privileged")${CL}" + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${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}" + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + echo -e "${NETWORK}${BOLD}${DGN}IPv4: ${BGN}$NET${CL}" + echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}$IPV6_METHOD${CL}" + echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}$ENABLE_FUSE${CL}" + [[ "$ENABLE_TUN" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "$ENABLE_NESTING" == "1" ] && echo "Enabled" || echo "Disabled")${CL}" + [[ "$ENABLE_KEYCTL" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}" + echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}$ENABLE_GPU${CL}" + [[ "$PROTECT_CT" == "yes" || "$PROTECT_CT" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}" + [[ -n "$CT_TIMEZONE" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}" + [[ "$APT_CACHER" == "yes" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}" + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" + echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" } # ============================================================================== @@ -2031,13 +2031,13 @@ Advanced: # - Sets global DIAGNOSTICS variable for API telemetry opt-in/out # ------------------------------------------------------------------------------ diagnostics_check() { - if ! [ -d "/usr/local/community-scripts" ]; then - mkdir -p /usr/local/community-scripts - fi + 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 + 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. @@ -2060,9 +2060,9 @@ DIAGNOSTICS=yes #"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="yes" + else + cat </usr/local/community-scripts/diagnostics DIAGNOSTICS=no #This file is used to store the diagnostics settings for the Community-Scripts API. @@ -2085,34 +2085,34 @@ DIAGNOSTICS=no #"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) + DIAGNOSTICS="no" + fi + else + DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics) - fi + fi } 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 + 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 - 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 } # ------------------------------------------------------------------------------ @@ -2123,25 +2123,25 @@ diagnostics_menu() { # - 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 [ "${var_gpu:-no}" == "yes" ]; then - echo -e "🎮${BOLD}${DGN} GPU Passthrough: ${BGN}Enabled${CL}" - fi - 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 " " + 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 [ "${var_gpu:-no}" == "yes" ]; then + echo -e "🎮${BOLD}${DGN} GPU Passthrough: ${BGN}Enabled${CL}" + fi + 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 " " } # ------------------------------------------------------------------------------ @@ -2153,196 +2153,196 @@ echo_default() { # - Applies chosen settings and triggers container build # ------------------------------------------------------------------------------ install_script() { - pve_check - shell_check - root_check - arch_check - ssh_check - maxkeys_check - diagnostics_check + 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) - - # 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 - - # --- 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" "User 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 - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # Main menu loop - allows returning from Settings - while true; do - 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" + if systemctl is-active -q ping-instances.service; then + systemctl -q stop ping-instances.service fi - # --- 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)" - break - ;; - 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" - break - ;; - 3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - break - ;; - "$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)" - break - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - # After settings menu, show main menu again - header_info - CHOICE="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - done + NEXTID=$(pvesh get /cluster/nextid) - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi + # 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 - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi + # 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" "User 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 + + APPDEFAULTS_OPTION="$appdefaults_option" + SETTINGS_OPTION="$settings_option" + + # Main menu loop - allows returning from Settings + while true; do + 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 + + # --- 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)" + break + ;; + 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" + break + ;; + 3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS) + default_var_settings || { + msg_error "Failed to apply default.vars" + exit 1 + } + defaults_target="/usr/local/community-scripts/default.vars" + break + ;; + "$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)" + break + else + msg_error "No App Defaults available for ${APP}" + exit 1 + fi + ;; + "$SETTINGS_OPTION" | settings | SETTINGS) + settings_menu + # After settings menu, show main menu again + header_info + CHOICE="" + ;; + *) + echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" + exit 1 + ;; + esac + done + + 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" + local vf="/usr/local/community-scripts/default.vars" - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi + # 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" + # 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" - ) - if [ -f "$(get_app_defaults_path)" ]; then - settings_items+=("3" "Edit App.vars for ${APP}") - settings_items+=("4" "Back to Main Menu") - else - settings_items+=("3" "Back to Main Menu") - fi + while true; do + local settings_items=( + "1" "Manage API-Diagnostic Setting" + "2" "Edit Default.vars" + ) + if [ -f "$(get_app_defaults_path)" ]; then + settings_items+=("3" "Edit App.vars for ${APP}") + settings_items+=("4" "Back to Main Menu") + else + settings_items+=("3" "Back to Main Menu") + fi - local choice - choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts SETTINGS Menu" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \ - "${settings_items[@]}" \ - 3>&1 1>&2 2>&3) || exit_script + local choice + choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "Community-Scripts SETTINGS Menu" \ + --ok-button "Select" --cancel-button "Exit Script" \ + --menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \ + "${settings_items[@]}" \ + 3>&1 1>&2 2>&3) || exit_script - case "$choice" in - 1) diagnostics_menu ;; - 2) nano /usr/local/community-scripts/default.vars ;; - 3) - if [ -f "$(get_app_defaults_path)" ]; then - nano "$(get_app_defaults_path)" - else - # Back was selected (no app.vars available) - return - fi - ;; - 4) - # Back to main menu - return - ;; - esac - done + case "$choice" in + 1) diagnostics_menu ;; + 2) nano /usr/local/community-scripts/default.vars ;; + 3) + if [ -f "$(get_app_defaults_path)" ]; then + nano "$(get_app_defaults_path)" + else + # Back was selected (no app.vars available) + return + fi + ;; + 4) + # Back to main menu + return + ;; + esac + done } # ------------------------------------------------------------------------------ @@ -2352,21 +2352,21 @@ settings_menu() { # - 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) + 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 + 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 - else - echo -e "" - fi } # ------------------------------------------------------------------------------ @@ -2376,18 +2376,18 @@ check_container_resources() { # - 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 + 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 - fi } # ------------------------------------------------------------------------------ @@ -2397,9 +2397,9 @@ check_container_storage() { # - 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 ' + local f="$1" + [[ -r "$f" ]] || return 0 + tr -d '\r' <"$f" | awk ' /^[[:space:]]*#/ {next} /^[[:space:]]*$/ {next} # nackt: typ base64 [comment] @@ -2419,45 +2419,45 @@ ssh_extract_keys_from_file() { # - 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 + 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 + 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 + # 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}..." + 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 + 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 } # ------------------------------------------------------------------------------ @@ -2467,105 +2467,105 @@ ssh_build_choices_from_files() { # - 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[@]}" + 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() { - local step_info="${1:-}" - local backtitle="[dev] Proxmox VE Helper Scripts" - [[ -n "$step_info" ]] && backtitle="[dev] Proxmox VE Helper Scripts [${step_info}]" + local step_info="${1:-}" + local backtitle="[dev] Proxmox VE Helper Scripts" + [[ -n "$step_info" ]] && backtitle="[dev] Proxmox VE Helper Scripts [${step_info}]" - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" + 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" + 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 "$backtitle" --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 "$backtitle" --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 + local ssh_key_mode + if [[ "$default_key_count" -gt 0 ]]; then + ssh_key_mode=$(whiptail --backtitle "$backtitle" --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 "$backtitle" --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 "$backtitle" --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 "$backtitle" \ - --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 "$backtitle" \ - --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 "$backtitle" --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 + case "$ssh_key_mode" in + found) + local selection + selection=$(whiptail --backtitle "$backtitle" --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 - else - whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60 + done + ;; + manual) + SSH_AUTHORIZED_KEY="$(whiptail --backtitle "$backtitle" \ + --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 "$backtitle" \ + --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 "$backtitle" --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 "$backtitle" --msgbox "No keys found in: $glob_path" 8 60 + fi + else + whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60 + fi fi - else - whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60 - fi + ;; + none) + : + ;; + esac + + if [[ -s "$SSH_KEYS_FILE" ]]; then + sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" + printf '\n' >>"$SSH_KEYS_FILE" fi - ;; - none) - : - ;; - esac - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - # Always show SSH access dialog - user should be able to enable SSH even without keys - if (whiptail --backtitle "$backtitle" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then - SSH="yes" - else - SSH="no" - fi + # Always show SSH access dialog - user should be able to enable SSH even without keys + if (whiptail --backtitle "$backtitle" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then + SSH="yes" + else + SSH="no" + fi } # ------------------------------------------------------------------------------ @@ -2577,39 +2577,39 @@ configure_ssh_settings() { # - 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) + 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 + case "$CHOICE" in + 1) + VERBOSE="no" + set_std_mode + ;; + 2) + VERBOSE="yes" + set_std_mode + ;; + 3) + clear + exit_script + exit + ;; + esac + update_script + fi } # ============================================================================== @@ -2630,256 +2630,256 @@ start() { # - Posts installation telemetry to API if diagnostics enabled # ------------------------------------------------------------------------------ build_container() { - # if [ "$VERBOSE" == "yes" ]; then set -x; fi + # if [ "$VERBOSE" == "yes" ]; then set -x; fi - NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" + NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" - # MAC - if [[ -n "$MAC" ]]; then - case "$MAC" in - ,hwaddr=*) NET_STRING+="$MAC" ;; - *) NET_STRING+=",hwaddr=$MAC" ;; + # 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 - fi - # IP (immer zwingend, Standard dhcp) - NET_STRING+=",ip=${NET:-dhcp}" + # Build FEATURES string based on container type and user choices + FEATURES="" - # Gateway - if [[ -n "$GATE" ]]; then - case "$GATE" in - ,gw=*) NET_STRING+="$GATE" ;; - *) NET_STRING+=",gw=$GATE" ;; - esac - fi + # Nesting support (user configurable, default enabled) + if [ "${ENABLE_NESTING:-1}" == "1" ]; then + FEATURES="nesting=1" + fi - # VLAN - if [[ -n "$VLAN" ]]; then - case "$VLAN" in - ,tag=*) NET_STRING+="$VLAN" ;; - *) NET_STRING+=",tag=$VLAN" ;; - esac - fi + # Keyctl for unprivileged containers (needed for Docker) + if [ "$CT_TYPE" == "1" ]; then + [ -n "$FEATURES" ] && FEATURES="$FEATURES," + FEATURES="${FEATURES}keyctl=1" + fi - # MTU - if [[ -n "$MTU" ]]; then - case "$MTU" in - ,mtu=*) NET_STRING+="$MTU" ;; - *) NET_STRING+=",mtu=$MTU" ;; - esac - fi + if [ "$ENABLE_FUSE" == "yes" ]; then + [ -n "$FEATURES" ] && FEATURES="$FEATURES," + FEATURES="${FEATURES}fuse=1" + 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 + # NEW IMPLEMENTATION (Fixed): Build PCT_OPTIONS properly + # Key insight: Bash cannot export arrays, so we build the options as a string - # Build FEATURES string based on container type and user choices - FEATURES="" + TEMP_DIR=$(mktemp -d) + pushd "$TEMP_DIR" >/dev/null - # Nesting support (user configurable, default enabled) - if [ "${ENABLE_NESTING:-1}" == "1" ]; then - FEATURES="nesting=1" - fi + # Unified install.func automatically detects OS type (debian, alpine, fedora, etc.) + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" - # Keyctl for unprivileged containers (needed for Docker) - if [ "$CT_TYPE" == "1" ]; then - [ -n "$FEATURES" ] && FEATURES="$FEATURES," - FEATURES="${FEATURES}keyctl=1" - fi + # Core exports for install.func + export DIAGNOSTICS="$DIAGNOSTICS" + export RANDOM_UUID="$RANDOM_UUID" + export SESSION_ID="$SESSION_ID" + 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" - if [ "$ENABLE_FUSE" == "yes" ]; then - [ -n "$FEATURES" ] && FEATURES="$FEATURES," - FEATURES="${FEATURES}fuse=1" - fi + # DEV_MODE exports (optional, for debugging) + 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}" - # NEW IMPLEMENTATION (Fixed): Build PCT_OPTIONS properly - # Key insight: Bash cannot export arrays, so we build the options as a string - - TEMP_DIR=$(mktemp -d) - pushd "$TEMP_DIR" >/dev/null - - # Unified install.func automatically detects OS type (debian, alpine, fedora, etc.) - export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" - - # Core exports for install.func - export DIAGNOSTICS="$DIAGNOSTICS" - export RANDOM_UUID="$RANDOM_UUID" - export SESSION_ID="$SESSION_ID" - 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" - - # DEV_MODE exports (optional, for debugging) - 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}" - - # Build PCT_OPTIONS as multi-line string - PCT_OPTIONS_STRING=" -features $FEATURES + # Build PCT_OPTIONS as multi-line string + PCT_OPTIONS_STRING=" -features $FEATURES -hostname $HN -tags $TAGS" - # Add storage if specified - if [ -n "$SD" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + # Add storage if specified + if [ -n "$SD" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING $SD" - fi + fi - # Add nameserver if specified - if [ -n "$NS" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + # Add nameserver if specified + if [ -n "$NS" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING $NS" - fi + fi - # Network configuration - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + # Network configuration + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING $NET_STRING -onboot 1 -cores $CORE_COUNT -memory $RAM_SIZE -unprivileged $CT_TYPE" - # Protection flag (if var_protection was set) - if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + # Protection flag (if var_protection was set) + if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING -protection 1" - fi + fi - # Timezone flag (if var_timezone was set) - if [ -n "${CT_TIMEZONE:-}" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + # Timezone flag (if var_timezone was set) + if [ -n "${CT_TIMEZONE:-}" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING -timezone $CT_TIMEZONE" - fi + fi - # Password (already formatted) - if [ -n "$PW" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + # Password (already formatted) + if [ -n "$PW" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING $PW" - fi - - # Export as string (this works, unlike arrays!) - export PCT_OPTIONS="$PCT_OPTIONS_STRING" - export TEMPLATE_STORAGE="${var_template_storage:-}" - export CONTAINER_STORAGE="${var_container_storage:-}" - - # # DEBUG: Show final PCT_OPTIONS being exported - # echo "[DEBUG] PCT_OPTIONS to be exported:" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Calling create_lxc_container..." - - create_lxc_container || exit $? - - LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" - - # ============================================================================ - # GPU/USB PASSTHROUGH CONFIGURATION - # ============================================================================ - - # Check if GPU passthrough is enabled - # Returns true only if var_gpu is explicitly set to "yes" - # Can be set via: - # - Environment variable: var_gpu=yes bash -c "..." - # - CT script default: var_gpu="${var_gpu:-no}" - # - Advanced settings wizard - # - App defaults file: /usr/local/community-scripts/defaults/.vars - is_gpu_app() { - [[ "${var_gpu:-no}" == "yes" ]] && return 0 - 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_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") - 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_custom "🎮" "${RD}" "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 + # Export as string (this works, unlike arrays!) + export PCT_OPTIONS="$PCT_OPTIONS_STRING" + export TEMPLATE_STORAGE="${var_template_storage:-}" + export CONTAINER_STORAGE="${var_container_storage:-}" + + # # DEBUG: Show final PCT_OPTIONS being exported + # echo "[DEBUG] PCT_OPTIONS to be exported:" + # echo "$PCT_OPTIONS" | sed 's/^/ /' + # echo "[DEBUG] Calling create_lxc_container..." + + create_lxc_container || exit $? + + LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" + + # ============================================================================ + # GPU/USB PASSTHROUGH CONFIGURATION + # ============================================================================ + + # Check if GPU passthrough is enabled + # Returns true only if var_gpu is explicitly set to "yes" + # Can be set via: + # - Environment variable: var_gpu=yes bash -c "..." + # - CT script default: var_gpu="${var_gpu:-no}" + # - Advanced settings wizard + # - App defaults file: /usr/local/community-scripts/defaults/.vars + is_gpu_app() { + [[ "${var_gpu:-no}" == "yes" ]] && return 0 + 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_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") + done + fi fi - fi - fi - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" + # 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_custom "🎮" "${RD}" "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 - # Simple passthrough - just bind /dev/nvidia* devices if they exist - # Skip directories like /dev/nvidia-caps (they need special handling) - for d in /dev/nvidia*; do - [[ -e "$d" ]] || continue - [[ -d "$d" ]] && continue # Skip directories - NVIDIA_DEVICES+=("$d") - done + # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] + if echo "$pci_vga_info" | grep -q "\[10de:"; then + msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough" - else - 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 + # Simple passthrough - just bind /dev/nvidia* devices if they exist + # Skip directories like /dev/nvidia-caps (they need special handling) + for d in /dev/nvidia*; do + [[ -e "$d" ]] || continue + [[ -d "$d" ]] && continue # Skip directories + NVIDIA_DEVICES+=("$d") + done - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } + if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then + msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough" + else + 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 - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi + # Debug output + msg_debug "Intel devices: ${INTEL_DEVICES[*]}" + msg_debug "AMD devices: ${AMD_DEVICES[*]}" + msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" + } - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" + # 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: @@ -2891,454 +2891,454 @@ lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create= 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" - } + msg_ok "USB passthrough configured" + } - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if: - # GPU passthrough is enabled when var_gpu="yes": - # - Set via environment variable: var_gpu=yes bash -c "..." - # - Set in CT script: var_gpu="${var_gpu:-no}" - # - Enabled in advanced_settings wizard - # - Configured in app defaults file - if ! is_gpu_app "$APP"; then - return 0 - fi + # Configure GPU passthrough + configure_gpu_passthrough() { + # Skip if: + # GPU passthrough is enabled when var_gpu="yes": + # - Set via environment variable: var_gpu=yes bash -c "..." + # - Set in CT script: var_gpu="${var_gpu:-no}" + # - Enabled in advanced_settings wizard + # - Configured in app defaults file + if ! is_gpu_app "$APP"; then + return 0 + fi - detect_gpu_devices + detect_gpu_devices - # Count available GPU types - local gpu_count=0 - local available_gpus=() + # 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 [[ ${#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 [[ ${#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 [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then + available_gpus+=("NVIDIA") + gpu_count=$((gpu_count + 1)) + fi - if [[ $gpu_count -eq 0 ]]; then - msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough" - return 0 - fi + if [[ $gpu_count -eq 0 ]]; then + msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough" + return 0 + fi - local selected_gpu="" + local selected_gpu="" - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_ok "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^^}" + if [[ $gpu_count -eq 1 ]]; then + # Automatic selection for single GPU + selected_gpu="${available_gpus[0]}" + msg_ok "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 + # 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 + 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 + # 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[@]}") + case "$selected_gpu" in + INTEL | AMD) + local devices=() + [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") + [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - # 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 - # 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 + # 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 + # 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 (${#devices[@]} devices)" - ;; + export GPU_TYPE="$selected_gpu" + msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)" + ;; - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "No NVIDIA devices available for passthrough" - return 0 - fi + NVIDIA) + if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then + 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 - echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG" - dev_index=$((dev_index + 1)) - done + # Use pct set for NVIDIA devices + local dev_index=0 + for dev in "${NVIDIA_DEVICES[@]}"; do + 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 (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed" - ;; - esac - } + export GPU_TYPE="NVIDIA" + msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed" + ;; + esac + } - # Additional device passthrough - configure_additional_devices() { - # TUN device passthrough - if [ "$ENABLE_TUN" == "yes" ]; then - cat <>"$LXC_CONFIG" + # 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 + fi - # Coral TPU passthrough - if [[ -e /dev/apex_0 ]]; then - 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 - } + # Coral TPU passthrough + if [[ -e /dev/apex_0 ]]; then + 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 + } - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices + # Execute pre-start configurations + configure_usb_passthrough + configure_gpu_passthrough + configure_additional_devices - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ + # ============================================================================ + # START CONTAINER AND INSTALL USERLAND + # ============================================================================ - msg_info "Starting LXC Container" - pct start "$CTID" + 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 assignment (IPv4 or IPv6) - local ip_in_lxc="" - for i in {1..20}; do - # Try IPv4 first - ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) - # Fallback to IPv6 if IPv4 not available - if [ -z "$ip_in_lxc" ]; then - ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1) - fi - [ -n "$ip_in_lxc" ] && break - sleep 1 + # 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 - if [ -z "$ip_in_lxc" ]; then - msg_error "No IP assigned to CT $CTID after 20s" - echo -e "${YW}Troubleshooting:${CL}" - echo " • Verify bridge ${BRG} exists and has connectivity" - echo " • Check if DHCP server is reachable (if using DHCP)" - echo " • Verify static IP configuration (if using static IP)" - echo " • Check Proxmox firewall rules" - echo " • If using Tailscale: Disable MagicDNS temporarily" - exit 1 + # Wait for network (skip for Alpine initially) + if [ "$var_os" != "alpine" ]; then + msg_info "Waiting for network in LXC container" + + # Wait for IP assignment (IPv4 or IPv6) + local ip_in_lxc="" + for i in {1..20}; do + # Try IPv4 first + ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) + # Fallback to IPv6 if IPv4 not available + if [ -z "$ip_in_lxc" ]; then + ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1) + fi + [ -n "$ip_in_lxc" ] && break + sleep 1 + done + + if [ -z "$ip_in_lxc" ]; then + msg_error "No IP assigned to CT $CTID after 20s" + echo -e "${YW}Troubleshooting:${CL}" + echo " • Verify bridge ${BRG} exists and has connectivity" + echo " • Check if DHCP server is reachable (if using DHCP)" + echo " • Verify static IP configuration (if using static IP)" + echo " • Check Proxmox firewall rules" + echo " • If using Tailscale: Disable MagicDNS temporarily" + exit 1 + fi + + # Verify basic connectivity (ping test) + local ping_success=false + for retry in {1..3}; do + if pct exec "$CTID" -- ping -c 1 -W 2 1.1.1.1 &>/dev/null || + pct exec "$CTID" -- ping -c 1 -W 2 8.8.8.8 &>/dev/null || + pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then + ping_success=true + break + fi + sleep 2 + done + + if [ "$ping_success" = false ]; then + msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed" + echo -e "${YW}Container may have limited internet access. Installation will continue...${CL}" + else + msg_ok "Network in LXC is reachable (ping)" + fi fi - # Verify basic connectivity (ping test) - local ping_success=false - for retry in {1..3}; do - if pct exec "$CTID" -- ping -c 1 -W 2 1.1.1.1 &>/dev/null || - pct exec "$CTID" -- ping -c 1 -W 2 8.8.8.8 &>/dev/null || - pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then - ping_success=true - break - fi - sleep 2 - done + # 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 + } - if [ "$ping_success" = false ]; then - msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed" - echo -e "${YW}Container may have limited internet access. Installation will continue...${CL}" - else - msg_ok "Network in LXC is reachable (ping)" - fi - fi + fix_gpu_gids - # 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 - } + # Continue with standard container setup + msg_info "Customizing LXC Container" - fix_gpu_gids + # # Install GPU userland if configured + # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then + # install_gpu_userland "VAAPI" + # fi - # Continue with standard container setup - msg_info "Customizing LXC Container" + # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then + # install_gpu_userland "NVIDIA" + # fi - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi + # Continue with standard container setup - install core dependencies based on OS + sleep 3 - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - install core dependencies based on OS - sleep 3 - - case "$var_os" in - alpine) - pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories + case "$var_os" in + alpine) + 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" - ;; + pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null" + ;; - debian | ubuntu | devuan) - # Locale setup for Debian-based - pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen 2>/dev/null || true" - pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen 2>/dev/null | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ + debian | ubuntu | devuan) + # Locale setup for Debian-based + pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen 2>/dev/null || true" + pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen 2>/dev/null | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ [[ -n \"\$locale_line\" ]] && echo LANG=\$locale_line >/etc/default/locale && \ locale-gen >/dev/null 2>&1 && \ export LANG=\$locale_line || true" - # Timezone setup - 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'; 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" + # Timezone setup + 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'; 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 + + # Core dependencies + 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 + } + ;; + + fedora | rockylinux | almalinux | centos) + # RHEL-based: Fedora, Rocky, AlmaLinux, CentOS + pct exec "$CTID" -- bash -c "dnf install -y curl sudo mc jq procps-ng >/dev/null 2>&1 || yum install -y curl sudo mc jq procps-ng >/dev/null 2>&1" || { + msg_error "dnf/yum base packages installation failed" + exit 1 + } + ;; + + opensuse) + # openSUSE + pct exec "$CTID" -- bash -c "zypper --non-interactive install curl sudo mc jq >/dev/null" || { + msg_error "zypper base packages installation failed" + exit 1 + } + ;; + + gentoo) + # Gentoo - emerge is slow, only install essentials + pct exec "$CTID" -- bash -c "emerge --quiet app-misc/jq net-misc/curl app-misc/mc >/dev/null 2>&1" || { + msg_warn "Gentoo base packages installation incomplete - may need manual setup" + } + ;; + + openeuler) + # openEuler (RHEL-compatible) + pct exec "$CTID" -- bash -c "dnf install -y curl sudo mc jq >/dev/null" || { + msg_error "dnf base packages installation failed" + exit 1 + } + ;; + + *) + msg_warn "Unknown OS '$var_os' - skipping core dependency installation" + ;; + esac + + msg_ok "Customized LXC Container" + + # Install SSH keys + install_ssh_keys_into_ct + + # Run application installer + # 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 - # Core dependencies - 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 - } - ;; - - fedora | rockylinux | almalinux | centos) - # RHEL-based: Fedora, Rocky, AlmaLinux, CentOS - pct exec "$CTID" -- bash -c "dnf install -y curl sudo mc jq procps-ng >/dev/null 2>&1 || yum install -y curl sudo mc jq procps-ng >/dev/null 2>&1" || { - msg_error "dnf/yum base packages installation failed" - exit 1 - } - ;; - - opensuse) - # openSUSE - pct exec "$CTID" -- bash -c "zypper --non-interactive install curl sudo mc jq >/dev/null" || { - msg_error "zypper base packages installation failed" - exit 1 - } - ;; - - gentoo) - # Gentoo - emerge is slow, only install essentials - pct exec "$CTID" -- bash -c "emerge --quiet app-misc/jq net-misc/curl app-misc/mc >/dev/null 2>&1" || { - msg_warn "Gentoo base packages installation incomplete - may need manual setup" - } - ;; - - openeuler) - # openEuler (RHEL-compatible) - pct exec "$CTID" -- bash -c "dnf install -y curl sudo mc jq >/dev/null" || { - msg_error "dnf base packages installation failed" - exit 1 - } - ;; - - *) - msg_warn "Unknown OS '$var_os' - skipping core dependency installation" - ;; - esac - - msg_ok "Customized LXC Container" - - # Install SSH keys - install_ssh_keys_into_ct - - # Run application installer - # 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}" + # 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 - # 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 + # Installation failed? + if [[ $install_exit_code -ne 0 ]]; then + msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})" - # Report failure to API before container cleanup - post_update_to_api "failed" "$install_exit_code" + # Copy both logs from container before potential deletion + local build_log_copied=false + local install_log_copied=false - # 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 [[ -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 - 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}" + # 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 - # 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 " + # 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 + + # Report failure to API before container cleanup + post_update_to_api "failed" "$install_exit_code" + + # 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 + 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 - 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 + exit $install_exit_code + 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 + if [[ -z "$CT_ID" ]]; then + msg_error "No CT_ID found. Nothing to remove." + return 1 fi - ;; - "" | n | no) - msg_custom "ℹ️" "${BL}" "Container was not removed." - ;; - *) - msg_warn "Invalid response. Container was not removed." - ;; - esac + + # 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_custom "ℹ️" "${BL}" "Container was not removed." + ;; + *) + msg_warn "Invalid response. Container was not removed." + ;; + esac } # ------------------------------------------------------------------------------ @@ -3346,81 +3346,81 @@ destroy_lxc() { # ------------------------------------------------------------------------------ # ===== 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}" + 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 - fi - STORAGE_RESULT="$preselect" - return 0 + + 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 + if [[ -z "${GPU_TYPE:-}" ]]; then + return 0 + fi - msg_info "Detecting and setting correct GPU group IDs" + msg_info "Detecting and setting correct GPU group IDs" - # 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") + # 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") - # Create groups if they don't exist - if [[ -z "$video_gid" ]]; then - 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" - fi + # Create groups if they don't exist + if [[ -z "$video_gid" ]]; then + 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" + fi - if [[ -z "$render_gid" ]]; then - 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" - fi + if [[ -z "$render_gid" ]]; then + 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" + fi - # Stop container to update config - pct stop "$CTID" >/dev/null 2>&1 - sleep 1 + # Stop container to update config + pct stop "$CTID" >/dev/null 2>&1 + sleep 1 - # 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" + # 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" - # Restart container - pct start "$CTID" >/dev/null 2>&1 - sleep 2 + # Restart container + pct start "$CTID" >/dev/null 2>&1 + sleep 2 - msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})" + msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})" - # For privileged containers: also fix permissions inside container - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " + # For privileged containers: also fix permissions inside container + 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 @@ -3434,740 +3434,740 @@ fix_gpu_gids() { done fi " >/dev/null 2>&1 - fi + fi } 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 ]] + 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 + 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 + 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') + 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 + if [[ ${#MENU[@]} -eq 0 ]]; then + msg_error "No storage found for content type '$CONTENT'." + return 2 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 + + 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 - return 0 - done } create_lxc_container() { - # ------------------------------------------------------------------------------ - # Optional verbose mode (debug tracing) - # ------------------------------------------------------------------------------ - if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi + # ------------------------------------------------------------------------------ + # 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}'; } + # ------------------------------------------------------------------------------ + # 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"; } + 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"; } + # 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 + # 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)" + _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 $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" - if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - msg_ok "Container created successfully after upgrade." + 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 - 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 - } + 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 $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" + 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 + } - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" + # ------------------------------------------------------------------------------ + # 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 + } - # 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 + msg_debug "CTID=$CTID" + msg_debug "PCT_OSTYPE=$PCT_OSTYPE" + msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - # 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 - } + # 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 - # 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 + # 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 + } - # 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 + # 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]" - fi + 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 - fi - msg_info "Validating storage '$CONTAINER_STORAGE'" - STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1) + msg_info "Validating storage '$CONTAINER_STORAGE'" + STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1) - case "$STORAGE_TYPE" in - iscsidirect) exit 212 ;; - iscsi | zfs) exit 213 ;; - cephfs) exit 219 ;; - pbs) exit 224 ;; - linstor | rbd | nfs | cifs) - pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null || exit 217 - ;; - esac + case "$STORAGE_TYPE" in + iscsidirect) exit 212 ;; + iscsi | zfs) exit 213 ;; + cephfs) exit 219 ;; + pbs) exit 224 ;; + linstor | rbd | nfs | cifs) + pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null || exit 217 + ;; + esac - pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE" || exit 213 - msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated" + pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE" || exit 213 + msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated" - msg_info "Validating template storage '$TEMPLATE_STORAGE'" - TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1) + msg_info "Validating template storage '$TEMPLATE_STORAGE'" + TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1) - if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then - msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'" - fi - msg_ok "Template storage '$TEMPLATE_STORAGE' validated" - - # 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 + if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then + msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'" fi - msg_ok "Cluster is quorate" - fi + msg_ok "Template storage '$TEMPLATE_STORAGE' validated" - # ------------------------------------------------------------------------------ - # Template discovery & validation - # Supported OS types (pveam available): alpine, almalinux, centos, debian, - # devuan, fedora, gentoo, openeuler, opensuse, rockylinux, ubuntu - # ------------------------------------------------------------------------------ - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - case "$PCT_OSTYPE" in - debian | ubuntu | devuan) TEMPLATE_PATTERN="-standard_" ;; - alpine | fedora | rocky | rockylinux | centos | almalinux | openeuler) TEMPLATE_PATTERN="-default_" ;; - gentoo) TEMPLATE_PATTERN="-current_" ;; - opensuse) TEMPLATE_PATTERN="-default_" ;; - *) TEMPLATE_PATTERN="" ;; - esac + # 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 + } - msg_info "Searching for template '$TEMPLATE_SEARCH'" + # 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 - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" + # ------------------------------------------------------------------------------ + # Template discovery & validation + # Supported OS types (pveam available): alpine, almalinux, centos, debian, + # devuan, fedora, gentoo, openeuler, opensuse, rockylinux, ubuntu + # ------------------------------------------------------------------------------ + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" + case "$PCT_OSTYPE" in + debian | ubuntu | devuan) TEMPLATE_PATTERN="-standard_" ;; + alpine | fedora | rocky | rockylinux | centos | almalinux | openeuler) TEMPLATE_PATTERN="-default_" ;; + gentoo) TEMPLATE_PATTERN="-current-openrc" ;; + opensuse) TEMPLATE_PATTERN="-default_" ;; + *) TEMPLATE_PATTERN="" ;; + esac - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" + msg_info "Searching for template '$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 - ) + # Build regex patterns outside awk/grep for clarity + SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." + #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" + #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" + #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - 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/^/ /' - - 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 - - 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 + 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 ) - 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 + pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - 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}-" + msg_ok "Template search completed" - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" + #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/^/ /' - 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 + 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 + + 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 [[ ${#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_custom "🚫" "${YW}" "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi + 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 - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" + 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}-" - 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 + #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - # 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 + 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 + ) - [[ -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_custom "🚫" "${YW}" "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_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 - - 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_custom "ℹ️" "${BL}" "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 - - # PCT_OPTIONS is now a string (exported from build_container) - # Add rootfs if not already specified - if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then - PCT_OPTIONS="$PCT_OPTIONS - -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}" - fi - - # 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}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" - - # # DEBUG: Show the actual command that will be executed - # echo "[DEBUG] ===== PCT CREATE COMMAND DETAILS =====" - # echo "[DEBUG] CTID: $CTID" - # echo "[DEBUG] Template: ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" - # echo "[DEBUG] PCT_OPTIONS (will be word-split):" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Full command line:" - # echo " pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - # echo "[DEBUG] ========================================" - - msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - msg_debug "Logfile: $LOGFILE" - - # First attempt (PCT_OPTIONS is a multi-line string, use it directly) - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then - 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 - 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 not already on local - if [[ "$TEMPLATE_STORAGE" != "local" ]]; then - 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 - # 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." - 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 - pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" - set +x + 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_custom "🚫" "${YW}" "Installation cancelled" + exit 0 fi - exit 209 - fi else - msg_ok "Container successfully created using local fallback." + msg_error "No ${PCT_OSTYPE} templates available at all" + exit 225 fi - else - # 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." - 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 - 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 - # 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 - } + #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" + #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - # Verify config rootfs - grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || { - msg_error "RootFS entry missing in container config. See $LOGFILE" - exit 216 - } + 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 - msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." + # 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 - # Report container creation to API - post_to_api + [[ -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_custom "🚫" "${YW}" "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_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 + + 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_custom "ℹ️" "${BL}" "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 + + # PCT_OPTIONS is now a string (exported from build_container) + # Add rootfs if not already specified + if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then + PCT_OPTIONS="$PCT_OPTIONS + -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}" + fi + + # 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}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" + + # # DEBUG: Show the actual command that will be executed + # echo "[DEBUG] ===== PCT CREATE COMMAND DETAILS =====" + # echo "[DEBUG] CTID: $CTID" + # echo "[DEBUG] Template: ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" + # echo "[DEBUG] PCT_OPTIONS (will be word-split):" + # echo "$PCT_OPTIONS" | sed 's/^/ /' + # echo "[DEBUG] Full command line:" + # echo " pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" + # echo "[DEBUG] ========================================" + + msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" + msg_debug "Logfile: $LOGFILE" + + # First attempt (PCT_OPTIONS is a multi-line string, use it directly) + if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then + 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 + 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 not already on local + if [[ "$TEMPLATE_STORAGE" != "local" ]]; then + 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 + # 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." + 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 + 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 + # 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." + 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 + 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 + + # 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." + + # Report container creation to API + post_to_api } # ============================================================================== @@ -4187,11 +4187,11 @@ create_lxc_container() { # - Posts final "done" status to API telemetry # ------------------------------------------------------------------------------ description() { - IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) + IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - # Generate LXC Description - DESCRIPTION=$( - cat < Logo @@ -4219,14 +4219,14 @@ description() { EOF - ) - pct set "$CTID" -description "$DESCRIPTION" + ) + pct set "$CTID" -description "$DESCRIPTION" - if [[ -f /etc/systemd/system/ping-instances.service ]]; then - systemctl start ping-instances.service - fi + if [[ -f /etc/systemd/system/ping-instances.service ]]; then + systemctl start ping-instances.service + fi - post_update_to_api "done" "none" + post_update_to_api "done" "none" } # ============================================================================== @@ -4243,14 +4243,14 @@ EOF # - Only executes on non-zero exit codes # ------------------------------------------------------------------------------ api_exit_script() { - exit_code=$? - if [ $exit_code -ne 0 ]; then - post_update_to_api "failed" "$exit_code" - fi + exit_code=$? + if [ $exit_code -ne 0 ]; then + post_update_to_api "failed" "$exit_code" + fi } if command -v pveversion >/dev/null 2>&1; then - trap 'api_exit_script' EXIT + trap 'api_exit_script' EXIT fi trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT diff --git a/misc/core.func b/misc/core.func index 39d655b0e..76339c87c 100644 --- a/misc/core.func +++ b/misc/core.func @@ -31,13 +31,13 @@ _CORE_FUNC_LOADED=1 # - Must be called at start of any script using these utilities # ------------------------------------------------------------------------------ load_functions() { - [[ -n "${__FUNCTIONS_LOADED:-}" ]] && return - __FUNCTIONS_LOADED=1 - color - formatting - icons - default_vars - set_std_mode + [[ -n "${__FUNCTIONS_LOADED:-}" ]] && return + __FUNCTIONS_LOADED=1 + color + formatting + icons + default_vars + set_std_mode } # ------------------------------------------------------------------------------ @@ -48,14 +48,14 @@ load_functions() { # GN (green), DGN (dark green), BGN (background green), CL (clear) # ------------------------------------------------------------------------------ color() { - YW=$(echo "\033[33m") - YWB=$'\e[93m' - 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") + YW=$(echo "\033[33m") + YWB=$'\e[93m' + 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") } # ------------------------------------------------------------------------------ @@ -67,9 +67,9 @@ color() { # - Used by spinner() function to avoid color conflicts # ------------------------------------------------------------------------------ color_spinner() { - CS_YW=$'\033[33m' - CS_YWB=$'\033[93m' - CS_CL=$'\033[m' + CS_YW=$'\033[33m' + CS_YWB=$'\033[93m' + CS_CL=$'\033[m' } # ------------------------------------------------------------------------------ @@ -81,11 +81,11 @@ color_spinner() { # - TAB/TAB3: Indentation spacing # ------------------------------------------------------------------------------ formatting() { - BFR="\\r\\033[K" - BOLD=$(echo "\033[1m") - HOLD=" " - TAB=" " - TAB3=" " + BFR="\\r\\033[K" + BOLD=$(echo "\033[1m") + HOLD=" " + TAB=" " + TAB3=" " } # ------------------------------------------------------------------------------ @@ -96,34 +96,34 @@ formatting() { # - Icons: CM (checkmark), CROSS (error), INFO (info), HOURGLASS (wait), etc. # ------------------------------------------------------------------------------ icons() { - CM="${TAB}✔️${TAB}" - CROSS="${TAB}✖️${TAB}" - DNSOK="✔️ " - DNSFAIL="${TAB}✖️${TAB}" - INFO="${TAB}💡${TAB}${CL}" - OS="${TAB}🖥️${TAB}${CL}" - OSVERSION="${TAB}🌟${TAB}${CL}" - CONTAINERTYPE="${TAB}📦${TAB}${CL}" - DISKSIZE="${TAB}💾${TAB}${CL}" - CPUCORE="${TAB}🧠${TAB}${CL}" - RAMSIZE="${TAB}🛠️${TAB}${CL}" - SEARCH="${TAB}🔍${TAB}${CL}" - VERBOSE_CROPPED="🔍${TAB}" - VERIFYPW="${TAB}🔐${TAB}${CL}" - CONTAINERID="${TAB}🆔${TAB}${CL}" - HOSTNAME="${TAB}🏠${TAB}${CL}" - BRIDGE="${TAB}🌉${TAB}${CL}" - NETWORK="${TAB}📡${TAB}${CL}" - GATEWAY="${TAB}🌐${TAB}${CL}" - ICON_DISABLEIPV6="${TAB}🚫${TAB}${CL}" - DEFAULT="${TAB}⚙️${TAB}${CL}" - MACADDRESS="${TAB}🔗${TAB}${CL}" - VLANTAG="${TAB}🏷️${TAB}${CL}" - ROOTSSH="${TAB}🔑${TAB}${CL}" - CREATING="${TAB}🚀${TAB}${CL}" - ADVANCED="${TAB}🧩${TAB}${CL}" - FUSE="${TAB}🗂️${TAB}${CL}" - HOURGLASS="${TAB}⏳${TAB}" + CM="${TAB}✔️${TAB}" + CROSS="${TAB}✖️${TAB}" + DNSOK="✔️ " + DNSFAIL="${TAB}✖️${TAB}" + INFO="${TAB}💡${TAB}${CL}" + OS="${TAB}🖥️${TAB}${CL}" + OSVERSION="${TAB}🌟${TAB}${CL}" + CONTAINERTYPE="${TAB}📦${TAB}${CL}" + DISKSIZE="${TAB}💾${TAB}${CL}" + CPUCORE="${TAB}🧠${TAB}${CL}" + RAMSIZE="${TAB}🛠️${TAB}${CL}" + SEARCH="${TAB}🔍${TAB}${CL}" + VERBOSE_CROPPED="🔍${TAB}" + VERIFYPW="${TAB}🔐${TAB}${CL}" + CONTAINERID="${TAB}🆔${TAB}${CL}" + HOSTNAME="${TAB}🏠${TAB}${CL}" + BRIDGE="${TAB}🌉${TAB}${CL}" + NETWORK="${TAB}📡${TAB}${CL}" + GATEWAY="${TAB}🌐${TAB}${CL}" + ICON_DISABLEIPV6="${TAB}🚫${TAB}${CL}" + DEFAULT="${TAB}⚙️${TAB}${CL}" + MACADDRESS="${TAB}🔗${TAB}${CL}" + VLANTAG="${TAB}🏷️${TAB}${CL}" + ROOTSSH="${TAB}🔑${TAB}${CL}" + CREATING="${TAB}🚀${TAB}${CL}" + ADVANCED="${TAB}🧩${TAB}${CL}" + FUSE="${TAB}🗂️${TAB}${CL}" + HOURGLASS="${TAB}⏳${TAB}" } # ------------------------------------------------------------------------------ @@ -135,9 +135,9 @@ icons() { # - i: Counter variable initialized to RETRY_NUM # ------------------------------------------------------------------------------ default_vars() { - RETRY_NUM=10 - RETRY_EVERY=3 - i=$RETRY_NUM + RETRY_NUM=10 + RETRY_EVERY=3 + i=$RETRY_NUM } # ------------------------------------------------------------------------------ @@ -149,17 +149,17 @@ default_vars() { # - If DEV_MODE_TRACE=true: Enables bash tracing (set -x) # ------------------------------------------------------------------------------ set_std_mode() { - if [ "${VERBOSE:-no}" = "yes" ]; then - STD="" - else - STD="silent" - fi + if [ "${VERBOSE:-no}" = "yes" ]; then + STD="" + else + STD="silent" + fi - # 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 + # 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 } # ------------------------------------------------------------------------------ @@ -177,57 +177,57 @@ set_std_mode() { # - 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 + 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 + # 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 - ;; - 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 } # ============================================================================== @@ -242,13 +242,13 @@ parse_dev_mode() { # - Required because scripts use Bash-specific features # ------------------------------------------------------------------------------ shell_check() { - if [[ "$(ps -p $$ -o comm=)" != "bash" ]]; then - clear - msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell." - echo -e "\nExiting..." - sleep 2 - exit - fi + if [[ "$(ps -p $$ -o comm=)" != "bash" ]]; then + clear + msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell." + echo -e "\nExiting..." + sleep 2 + exit + fi } # ------------------------------------------------------------------------------ @@ -259,13 +259,13 @@ shell_check() { # - Exits with error if not running as root directly # ------------------------------------------------------------------------------ root_check() { - 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 } # ------------------------------------------------------------------------------ @@ -276,35 +276,35 @@ root_check() { # - Exits with error message if unsupported version detected # ------------------------------------------------------------------------------ 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 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 + # 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.9 or 9.0 – 9.1" - 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.9 or 9.0 – 9.1" + exit 1 } # ------------------------------------------------------------------------------ @@ -315,13 +315,13 @@ pve_check() { # - 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" - 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 } # ------------------------------------------------------------------------------ @@ -333,29 +333,29 @@ arch_check() { # - 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 [ -n "$SSH_CLIENT" ]; then + local client_ip=$(awk '{print $1}' <<<"$SSH_CLIENT") + local host_ip=$(hostname -I | awk '{print $1}') - # 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 + # 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 - - # 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 } # ============================================================================== @@ -371,14 +371,14 @@ ssh_check() { # - 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 - # Fallback for legacy scripts - echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log" - fi + if [[ -n "${INSTALL_LOG:-}" ]]; then + echo "$INSTALL_LOG" + elif [[ -n "${BUILD_LOG:-}" ]]; then + echo "$BUILD_LOG" + else + # Fallback for legacy scripts + echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log" + fi } # Legacy compatibility: SILENT_LOGFILE points to active log @@ -393,56 +393,56 @@ SILENT_LOGFILE="$(get_active_logfile)" # - Sources explain_exit_code() for detailed error messages # ------------------------------------------------------------------------------ silent() { - local cmd="$*" - local caller_line="${BASH_LINENO[0]:-unknown}" - local logfile="$(get_active_logfile)" + 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) + # 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 - local explanation - explanation="$(explain_exit_code "$rc")" + set +Eeuo pipefail + trap - ERR - printf "\e[?25h" - msg_error "in line ${caller_line}: exit code ${rc} (${explanation})" - msg_custom "→" "${YWB}" "${cmd}" + "$@" >>"$logfile" 2>&1 + local rc=$? - if [[ -s "$logfile" ]]; then - local log_lines=$(wc -l <"$logfile") - echo "--- Last 10 lines of silent log ---" - tail -n 10 "$logfile" - echo "-----------------------------------" + set -Eeuo pipefail + trap 'error_handler' ERR - # 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 + 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 - - exit "$rc" - fi } # ------------------------------------------------------------------------------ @@ -454,14 +454,14 @@ silent() { # - 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}${msg}${CS_CL}" - sleep 0.1 - done + 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}${msg}${CS_CL}" + sleep 0.1 + done } # ------------------------------------------------------------------------------ @@ -473,8 +473,8 @@ spinner() { # - 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" + tput cr 2>/dev/null || echo -en "\r" + tput el 2>/dev/null || echo -en "\033[K" } # ------------------------------------------------------------------------------ @@ -487,20 +487,20 @@ clear_line() { # - Unsets SPINNER_PID and SPINNER_MSG variables # ------------------------------------------------------------------------------ stop_spinner() { - local pid="${SPINNER_PID:-}" - [[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(/dev/null; then - sleep 0.05 - kill -9 "$pid" 2>/dev/null || true - wait "$pid" 2>/dev/null || true + if [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]]; then + if kill "$pid" 2>/dev/null; then + sleep 0.05 + kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi + rm -f /tmp/.spinner.pid fi - rm -f /tmp/.spinner.pid - fi - unset SPINNER_PID SPINNER_MSG - stty sane 2>/dev/null || true + unset SPINNER_PID SPINNER_MSG + stty sane 2>/dev/null || true } # ============================================================================== @@ -517,42 +517,45 @@ stop_spinner() { # - Backgrounds spinner process and stores PID for later cleanup # ------------------------------------------------------------------------------ msg_info() { - local msg="$1" - [[ -z "$msg" ]] && return + local msg="$1" + [[ -z "$msg" ]] && return - if ! declare -p MSG_INFO_SHOWN &>/dev/null || ! declare -A MSG_INFO_SHOWN &>/dev/null; then - declare -gA MSG_INFO_SHOWN=() - fi - [[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return - MSG_INFO_SHOWN["$msg"]=1 - - stop_spinner - SPINNER_MSG="$msg" - - 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 + if ! declare -p MSG_INFO_SHOWN &>/dev/null || ! declare -A MSG_INFO_SHOWN &>/dev/null; then + declare -gA MSG_INFO_SHOWN=() fi - return - fi + # Sanitize message for use as associative array key (remove ANSI codes) + local sanitized_msg + sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g') + [[ -n "${MSG_INFO_SHOWN["$sanitized_msg"]+x}" ]] && return + MSG_INFO_SHOWN["$sanitized_msg"]=1 - color_spinner - spinner & - 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 + SPINNER_MSG="$msg" + + 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 + + color_spinner + spinner & + 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 } # ------------------------------------------------------------------------------ @@ -564,12 +567,15 @@ msg_info() { # - Uses green color for success indication # ------------------------------------------------------------------------------ msg_ok() { - local msg="$1" - [[ -z "$msg" ]] && return - stop_spinner - clear_line - echo -e "$CM ${GN}${msg}${CL}" - unset MSG_INFO_SHOWN["$msg"] + local msg="$1" + [[ -z "$msg" ]] && return + stop_spinner + clear_line + echo -e "$CM ${GN}${msg}${CL}" + # Sanitize message for use as associative array key (remove ANSI codes) + local sanitized_msg + sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g') + unset MSG_INFO_SHOWN["$sanitized_msg"] } # ------------------------------------------------------------------------------ @@ -581,9 +587,9 @@ msg_ok() { # - Outputs to stderr # ------------------------------------------------------------------------------ msg_error() { - stop_spinner - local msg="$1" - echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2 + stop_spinner + local msg="$1" + echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2 } # ------------------------------------------------------------------------------ @@ -595,9 +601,9 @@ msg_error() { # - Outputs to stderr # ------------------------------------------------------------------------------ msg_warn() { - stop_spinner - local msg="$1" - echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2 + stop_spinner + local msg="$1" + echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2 } # ------------------------------------------------------------------------------ @@ -609,12 +615,12 @@ msg_warn() { # - Useful for specialized status messages # ------------------------------------------------------------------------------ msg_custom() { - local symbol="${1:-"[*]"}" - local color="${2:-"\e[36m"}" - local msg="${3:-}" - [[ -z "$msg" ]] && return - stop_spinner - echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}" + local symbol="${1:-"[*]"}" + local color="${2:-"\e[36m"}" + local msg="${3:-}" + [[ -z "$msg" ]] && return + stop_spinner + echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}" } # ------------------------------------------------------------------------------ @@ -626,10 +632,10 @@ msg_custom() { # - 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 + if [[ "${var_full_verbose:-0}" == "1" ]]; then + [[ "${var_verbose:-0}" != "1" ]] && var_verbose=1 + echo -e "${YWB}[$(date '+%F %T')] [DEBUG]${CL} $*" + fi } # ------------------------------------------------------------------------------ @@ -642,9 +648,9 @@ msg_debug() { # - Usage: msg_dev "Container ready for debugging" # ------------------------------------------------------------------------------ msg_dev() { - if [[ -n "${dev_mode:-}" ]]; then - echo -e "${SEARCH}${BOLD}${DGN}🔧 [DEV]${CL} $*" - fi + if [[ -n "${dev_mode:-}" ]]; then + echo -e "${SEARCH}${BOLD}${DGN}🔧 [DEV]${CL} $*" + fi } # # - Displays error message and immediately terminates script @@ -652,8 +658,8 @@ msg_dev() { # - Use for unrecoverable errors that require immediate exit # ------------------------------------------------------------------------------ fatal() { - msg_error "$1" - kill -INT $$ + msg_error "$1" + kill -INT $$ } # ============================================================================== @@ -668,9 +674,9 @@ fatal() { # - Exits with default exit code # ------------------------------------------------------------------------------ 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 } # ------------------------------------------------------------------------------ @@ -682,20 +688,20 @@ exit_script() { # - 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}" + 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")" + mkdir -p "$(dirname "$local_header_path")" - if [ ! -s "$local_header_path" ]; then - if ! curl -fsSL "$header_url" -o "$local_header_path"; then - return 1 + if [ ! -s "$local_header_path" ]; then + if ! curl -fsSL "$header_url" -o "$local_header_path"; then + return 1 + fi fi - fi - cat "$local_header_path" 2>/dev/null || true + cat "$local_header_path" 2>/dev/null || true } # ------------------------------------------------------------------------------ @@ -707,18 +713,18 @@ get_header() { # - Returns silently if header not available # ------------------------------------------------------------------------------ header_info() { - local app_name=$(echo "${APP,,}" | tr -d ' ') - local header_content + local app_name=$(echo "${APP,,}" | tr -d ' ') + local header_content - header_content=$(get_header "$app_name") || header_content="" + header_content=$(get_header "$app_name") || header_content="" - clear - local term_width - term_width=$(tput cols 2>/dev/null || echo 120) + clear + local term_width + term_width=$(tput cols 2>/dev/null || echo 120) - if [ -n "$header_content" ]; then - echo "$header_content" - fi + if [ -n "$header_content" ]; then + echo "$header_content" + fi } # ------------------------------------------------------------------------------ @@ -729,14 +735,14 @@ header_info() { # - 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 + 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 - fi } # ------------------------------------------------------------------------------ @@ -748,16 +754,16 @@ ensure_tput() { # - Used to adjust behavior for Alpine-specific commands # ------------------------------------------------------------------------------ is_alpine() { - local os_id="${var_os:-${PCT_OSTYPE:-}}" + 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 + if [[ -z "$os_id" && -f /etc/os-release ]]; then + os_id="$( + . /etc/os-release 2>/dev/null + echo "${ID:-}" + )" + fi - [[ "$os_id" == "alpine" ]] + [[ "$os_id" == "alpine" ]] } # ------------------------------------------------------------------------------ @@ -769,14 +775,14 @@ is_alpine() { # - 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 ]] + 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 ]] } # ============================================================================== @@ -794,62 +800,62 @@ is_verbose_mode() { # - Run at end of container creation to minimize disk usage # ------------------------------------------------------------------------------ cleanup_lxc() { - msg_info "Cleaning up" - # OS-specific package manager cleanup - if is_alpine; then - $STD apk cache clean 2>/dev/null || true - rm -rf /var/cache/apk/* - elif command -v apt &>/dev/null; then - # Debian/Ubuntu/Devuan - $STD apt -y autoremove 2>/dev/null || true - $STD apt -y autoclean 2>/dev/null || true - $STD apt -y clean 2>/dev/null || true - elif command -v dnf &>/dev/null; then - # Fedora/Rocky/AlmaLinux/CentOS 8+ - $STD dnf clean all 2>/dev/null || true - $STD dnf autoremove -y 2>/dev/null || true - elif command -v yum &>/dev/null; then - # CentOS 7/older RHEL - $STD yum clean all 2>/dev/null || true - elif command -v zypper &>/dev/null; then - # openSUSE - $STD zypper clean --all 2>/dev/null || true - elif command -v emerge &>/dev/null; then - # Gentoo - $STD emerge --quiet --depclean 2>/dev/null || true - $STD eclean-dist -d 2>/dev/null || true - $STD eclean-pkg -d 2>/dev/null || true - fi + msg_info "Cleaning up" + # OS-specific package manager cleanup + if is_alpine; then + $STD apk cache clean 2>/dev/null || true + rm -rf /var/cache/apk/* + elif command -v apt &>/dev/null; then + # Debian/Ubuntu/Devuan + $STD apt -y autoremove 2>/dev/null || true + $STD apt -y autoclean 2>/dev/null || true + $STD apt -y clean 2>/dev/null || true + elif command -v dnf &>/dev/null; then + # Fedora/Rocky/AlmaLinux/CentOS 8+ + $STD dnf clean all 2>/dev/null || true + $STD dnf autoremove -y 2>/dev/null || true + elif command -v yum &>/dev/null; then + # CentOS 7/older RHEL + $STD yum clean all 2>/dev/null || true + elif command -v zypper &>/dev/null; then + # openSUSE + $STD zypper clean --all 2>/dev/null || true + elif command -v emerge &>/dev/null; then + # Gentoo + $STD emerge --quiet --depclean 2>/dev/null || true + $STD eclean-dist -d 2>/dev/null || true + $STD eclean-pkg -d 2>/dev/null || 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 + # 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 + # 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 - # Node.js npm - if command -v npm &>/dev/null; then $STD npm cache clean --force 2>/dev/null || true; fi - # Node.js yarn - if command -v yarn &>/dev/null; then $STD yarn cache clean 2>/dev/null || true; fi - # Node.js pnpm - if command -v pnpm &>/dev/null; then $STD pnpm store prune 2>/dev/null || true; fi - # Go - if command -v go &>/dev/null; then $STD go clean -cache -modcache 2>/dev/null || true; fi - # Rust cargo - if command -v cargo &>/dev/null; then $STD cargo clean 2>/dev/null || true; fi - # Ruby gem - if command -v gem &>/dev/null; then $STD gem cleanup 2>/dev/null || true; fi - # Composer (PHP) - if command -v composer &>/dev/null; then $STD composer clear-cache 2>/dev/null || true; fi + # Node.js npm + if command -v npm &>/dev/null; then $STD npm cache clean --force 2>/dev/null || true; fi + # Node.js yarn + if command -v yarn &>/dev/null; then $STD yarn cache clean 2>/dev/null || true; fi + # Node.js pnpm + if command -v pnpm &>/dev/null; then $STD pnpm store prune 2>/dev/null || true; fi + # Go + if command -v go &>/dev/null; then $STD go clean -cache -modcache 2>/dev/null || true; fi + # Rust cargo + if command -v cargo &>/dev/null; then $STD cargo clean 2>/dev/null || true; fi + # Ruby gem + if command -v gem &>/dev/null; then $STD gem cleanup 2>/dev/null || true; fi + # Composer (PHP) + if command -v composer &>/dev/null; then $STD composer clear-cache 2>/dev/null || true; fi - if command -v journalctl &>/dev/null; then - $STD journalctl --vacuum-time=10m 2>/dev/null || true - fi - msg_ok "Cleaned" + if command -v journalctl &>/dev/null; then + $STD journalctl --vacuum-time=10m 2>/dev/null || true + fi + msg_ok "Cleaned" } # ------------------------------------------------------------------------------ @@ -863,41 +869,41 @@ cleanup_lxc() { # - Returns 0 if swap active or successfully created, 1 if declined/failed # ------------------------------------------------------------------------------ check_or_create_swap() { - msg_info "Checking for active swap" + msg_info "Checking for active swap" - if swapon --noheadings --show | grep -q 'swap'; then - msg_ok "Swap is active" - return 0 - fi + if swapon --noheadings --show | grep -q 'swap'; then + msg_ok "Swap is active" + return 0 + fi - msg_error "No active swap detected" + msg_error "No active swap detected" - read -p "Do you want to create a swap file? [y/N]: " create_swap - create_swap="${create_swap,,}" # to lowercase + read -p "Do you want to create a swap file? [y/N]: " create_swap + create_swap="${create_swap,,}" # to lowercase - if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then - msg_info "Skipping swap file creation" - return 1 - fi + if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then + msg_info "Skipping swap file creation" + return 1 + fi - read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb - if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then - msg_error "Invalid size input. Aborting." - return 1 - fi + read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb + if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then + msg_error "Invalid size input. Aborting." + return 1 + fi - local swap_file="/swapfile" + local swap_file="/swapfile" - msg_info "Creating ${swap_size_mb}MB swap file at $swap_file" - if dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress && - chmod 600 "$swap_file" && - mkswap "$swap_file" && - swapon "$swap_file"; then - msg_ok "Swap file created and activated successfully" - else - msg_error "Failed to create or activate swap" - return 1 - fi + msg_info "Creating ${swap_size_mb}MB swap file at $swap_file" + if dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress && + chmod 600 "$swap_file" && + mkswap "$swap_file" && + swapon "$swap_file"; then + msg_ok "Swap file created and activated successfully" + else + msg_error "Failed to create or activate swap" + return 1 + fi } # ============================================================================== diff --git a/misc/install.func b/misc/install.func index f474b4062..484a3a1fc 100644 --- a/misc/install.func +++ b/misc/install.func @@ -49,134 +49,134 @@ INIT_SYSTEM="" # systemd, openrc, sysvinit # OS_TYPE, OS_FAMILY, OS_VERSION, PKG_MANAGER, INIT_SYSTEM # ------------------------------------------------------------------------------ detect_os() { - if [[ -f /etc/os-release ]]; then - # shellcheck disable=SC1091 - . /etc/os-release - OS_TYPE="${ID:-unknown}" - OS_VERSION="${VERSION_ID:-unknown}" - elif [[ -f /etc/alpine-release ]]; then - OS_TYPE="alpine" - OS_VERSION=$(cat /etc/alpine-release) - elif [[ -f /etc/debian_version ]]; then - OS_TYPE="debian" - OS_VERSION=$(cat /etc/debian_version) - elif [[ -f /etc/redhat-release ]]; then - OS_TYPE="centos" - OS_VERSION=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1) - elif [[ -f /etc/arch-release ]]; then - OS_TYPE="arch" - OS_VERSION="rolling" - elif [[ -f /etc/gentoo-release ]]; then - OS_TYPE="gentoo" - OS_VERSION=$(cat /etc/gentoo-release | grep -oE '[0-9.]+') - else - OS_TYPE="unknown" - OS_VERSION="unknown" - fi - - # Normalize OS type and determine family - case "$OS_TYPE" in - debian) - OS_FAMILY="debian" - PKG_MANAGER="apt" - ;; - ubuntu) - OS_FAMILY="debian" - PKG_MANAGER="apt" - ;; - devuan) - OS_FAMILY="debian" - PKG_MANAGER="apt" - ;; - alpine) - OS_FAMILY="alpine" - PKG_MANAGER="apk" - ;; - fedora) - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - rocky | rockylinux) - OS_TYPE="rocky" - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - alma | almalinux) - OS_TYPE="alma" - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - centos) - OS_FAMILY="rhel" - # CentOS 7 uses yum, 8+ uses dnf - if [[ "${OS_VERSION%%.*}" -ge 8 ]]; then - PKG_MANAGER="dnf" + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . /etc/os-release + OS_TYPE="${ID:-unknown}" + OS_VERSION="${VERSION_ID:-unknown}" + elif [[ -f /etc/alpine-release ]]; then + OS_TYPE="alpine" + OS_VERSION=$(cat /etc/alpine-release) + elif [[ -f /etc/debian_version ]]; then + OS_TYPE="debian" + OS_VERSION=$(cat /etc/debian_version) + elif [[ -f /etc/redhat-release ]]; then + OS_TYPE="centos" + OS_VERSION=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1) + elif [[ -f /etc/arch-release ]]; then + OS_TYPE="arch" + OS_VERSION="rolling" + elif [[ -f /etc/gentoo-release ]]; then + OS_TYPE="gentoo" + OS_VERSION=$(cat /etc/gentoo-release | grep -oE '[0-9.]+') else - PKG_MANAGER="yum" + OS_TYPE="unknown" + OS_VERSION="unknown" fi - ;; - rhel) - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - openeuler) - OS_FAMILY="rhel" - PKG_MANAGER="dnf" - ;; - opensuse* | sles) - OS_TYPE="opensuse" - OS_FAMILY="suse" - PKG_MANAGER="zypper" - ;; - gentoo) - OS_FAMILY="gentoo" - PKG_MANAGER="emerge" - ;; - *) - OS_FAMILY="unknown" - PKG_MANAGER="unknown" - ;; - esac - # Detect init system - if command -v systemctl &>/dev/null && [[ -d /run/systemd/system ]]; then - INIT_SYSTEM="systemd" - elif command -v rc-service &>/dev/null || [[ -d /etc/init.d && -f /sbin/openrc ]]; then - INIT_SYSTEM="openrc" - elif [[ -f /etc/inittab ]]; then - INIT_SYSTEM="sysvinit" - else - INIT_SYSTEM="unknown" - fi + # Normalize OS type and determine family + case "$OS_TYPE" in + debian) + OS_FAMILY="debian" + PKG_MANAGER="apt" + ;; + ubuntu) + OS_FAMILY="debian" + PKG_MANAGER="apt" + ;; + devuan) + OS_FAMILY="debian" + PKG_MANAGER="apt" + ;; + alpine) + OS_FAMILY="alpine" + PKG_MANAGER="apk" + ;; + fedora) + OS_FAMILY="rhel" + PKG_MANAGER="dnf" + ;; + rocky | rockylinux) + OS_TYPE="rocky" + OS_FAMILY="rhel" + PKG_MANAGER="dnf" + ;; + alma | almalinux) + OS_TYPE="alma" + OS_FAMILY="rhel" + PKG_MANAGER="dnf" + ;; + centos) + OS_FAMILY="rhel" + # CentOS 7 uses yum, 8+ uses dnf + if [[ "${OS_VERSION%%.*}" -ge 8 ]]; then + PKG_MANAGER="dnf" + else + PKG_MANAGER="yum" + fi + ;; + rhel) + OS_FAMILY="rhel" + PKG_MANAGER="dnf" + ;; + openeuler) + OS_FAMILY="rhel" + PKG_MANAGER="dnf" + ;; + opensuse* | sles) + OS_TYPE="opensuse" + OS_FAMILY="suse" + PKG_MANAGER="zypper" + ;; + gentoo) + OS_FAMILY="gentoo" + PKG_MANAGER="emerge" + ;; + *) + OS_FAMILY="unknown" + PKG_MANAGER="unknown" + ;; + esac + + # Detect init system + if command -v systemctl &>/dev/null && [[ -d /run/systemd/system ]]; then + INIT_SYSTEM="systemd" + elif command -v rc-service &>/dev/null || [[ -d /etc/init.d && -f /sbin/openrc ]]; then + INIT_SYSTEM="openrc" + elif [[ -f /etc/inittab ]]; then + INIT_SYSTEM="sysvinit" + else + INIT_SYSTEM="unknown" + fi } # ------------------------------------------------------------------------------ # Bootstrap: Ensure curl is available and source core functions # ------------------------------------------------------------------------------ _bootstrap() { - # Minimal bootstrap to get curl installed - if ! command -v curl &>/dev/null; then - printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 - if command -v apt-get &>/dev/null; then - apt-get update &>/dev/null && apt-get install -y curl &>/dev/null - elif command -v apk &>/dev/null; then - apk update &>/dev/null && apk add curl &>/dev/null - elif command -v dnf &>/dev/null; then - dnf install -y curl &>/dev/null - elif command -v yum &>/dev/null; then - yum install -y curl &>/dev/null - elif command -v zypper &>/dev/null; then - zypper install -y curl &>/dev/null - elif command -v emerge &>/dev/null; then - emerge --quiet net-misc/curl &>/dev/null + # Minimal bootstrap to get curl installed + if ! command -v curl &>/dev/null; then + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + if command -v apt-get &>/dev/null; then + apt-get update &>/dev/null && apt-get install -y curl &>/dev/null + elif command -v apk &>/dev/null; then + apk update &>/dev/null && apk add curl &>/dev/null + elif command -v dnf &>/dev/null; then + dnf install -y curl &>/dev/null + elif command -v yum &>/dev/null; then + yum install -y curl &>/dev/null + elif command -v zypper &>/dev/null; then + zypper install -y curl &>/dev/null + elif command -v emerge &>/dev/null; then + emerge --quiet net-misc/curl &>/dev/null + fi fi - fi - # Source core functions - 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 + # Source core functions + 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 } # Run bootstrap and OS detection @@ -193,30 +193,30 @@ detect_os # Updates package manager cache/database # ------------------------------------------------------------------------------ pkg_update() { - case "$PKG_MANAGER" in - apt) - $STD apt-get update - ;; - apk) - $STD apk update - ;; - dnf) - $STD dnf makecache - ;; - yum) - $STD yum makecache - ;; - zypper) - $STD zypper refresh - ;; - emerge) - $STD emerge --sync - ;; - *) - msg_error "Unknown package manager: $PKG_MANAGER" - return 1 - ;; - esac + case "$PKG_MANAGER" in + apt) + $STD apt-get update + ;; + apk) + $STD apk update + ;; + dnf) + $STD dnf makecache + ;; + yum) + $STD yum makecache + ;; + zypper) + $STD zypper refresh + ;; + emerge) + $STD emerge --sync + ;; + *) + msg_error "Unknown package manager: $PKG_MANAGER" + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -225,30 +225,30 @@ pkg_update() { # Upgrades all installed packages # ------------------------------------------------------------------------------ pkg_upgrade() { - case "$PKG_MANAGER" in - apt) - $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade - ;; - apk) - $STD apk -U upgrade - ;; - dnf) - $STD dnf -y upgrade - ;; - yum) - $STD yum -y update - ;; - zypper) - $STD zypper -n update - ;; - emerge) - $STD emerge --quiet --update --deep @world - ;; - *) - msg_error "Unknown package manager: $PKG_MANAGER" - return 1 - ;; - esac + case "$PKG_MANAGER" in + apt) + $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade + ;; + apk) + $STD apk -U upgrade + ;; + dnf) + $STD dnf -y upgrade + ;; + yum) + $STD yum -y update + ;; + zypper) + $STD zypper -n update + ;; + emerge) + $STD emerge --quiet --update --deep @world + ;; + *) + msg_error "Unknown package manager: $PKG_MANAGER" + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -259,33 +259,33 @@ pkg_upgrade() { # packages - List of packages to install # ------------------------------------------------------------------------------ pkg_install() { - local packages=("$@") - [[ ${#packages[@]} -eq 0 ]] && return 0 + local packages=("$@") + [[ ${#packages[@]} -eq 0 ]] && return 0 - case "$PKG_MANAGER" in - apt) - $STD apt-get install -y "${packages[@]}" - ;; - apk) - $STD apk add --no-cache "${packages[@]}" - ;; - dnf) - $STD dnf install -y "${packages[@]}" - ;; - yum) - $STD yum install -y "${packages[@]}" - ;; - zypper) - $STD zypper install -y "${packages[@]}" - ;; - emerge) - $STD emerge --quiet "${packages[@]}" - ;; - *) - msg_error "Unknown package manager: $PKG_MANAGER" - return 1 - ;; - esac + case "$PKG_MANAGER" in + apt) + $STD apt-get install -y "${packages[@]}" + ;; + apk) + $STD apk add --no-cache "${packages[@]}" + ;; + dnf) + $STD dnf install -y "${packages[@]}" + ;; + yum) + $STD yum install -y "${packages[@]}" + ;; + zypper) + $STD zypper install -y "${packages[@]}" + ;; + emerge) + $STD emerge --quiet "${packages[@]}" + ;; + *) + msg_error "Unknown package manager: $PKG_MANAGER" + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -294,33 +294,33 @@ pkg_install() { # Removes one or more packages # ------------------------------------------------------------------------------ pkg_remove() { - local packages=("$@") - [[ ${#packages[@]} -eq 0 ]] && return 0 + local packages=("$@") + [[ ${#packages[@]} -eq 0 ]] && return 0 - case "$PKG_MANAGER" in - apt) - $STD apt-get remove -y "${packages[@]}" - ;; - apk) - $STD apk del "${packages[@]}" - ;; - dnf) - $STD dnf remove -y "${packages[@]}" - ;; - yum) - $STD yum remove -y "${packages[@]}" - ;; - zypper) - $STD zypper remove -y "${packages[@]}" - ;; - emerge) - $STD emerge --quiet --unmerge "${packages[@]}" - ;; - *) - msg_error "Unknown package manager: $PKG_MANAGER" - return 1 - ;; - esac + case "$PKG_MANAGER" in + apt) + $STD apt-get remove -y "${packages[@]}" + ;; + apk) + $STD apk del "${packages[@]}" + ;; + dnf) + $STD dnf remove -y "${packages[@]}" + ;; + yum) + $STD yum remove -y "${packages[@]}" + ;; + zypper) + $STD zypper remove -y "${packages[@]}" + ;; + emerge) + $STD emerge --quiet --unmerge "${packages[@]}" + ;; + *) + msg_error "Unknown package manager: $PKG_MANAGER" + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -329,31 +329,31 @@ pkg_remove() { # Cleans package manager cache to free space # ------------------------------------------------------------------------------ pkg_clean() { - case "$PKG_MANAGER" in - apt) - $STD apt-get autoremove -y - $STD apt-get autoclean - ;; - apk) - $STD apk cache clean - ;; - dnf) - $STD dnf clean all - $STD dnf autoremove -y - ;; - yum) - $STD yum clean all - ;; - zypper) - $STD zypper clean - ;; - emerge) - $STD emerge --quiet --depclean - ;; - *) - return 0 - ;; - esac + case "$PKG_MANAGER" in + apt) + $STD apt-get autoremove -y + $STD apt-get autoclean + ;; + apk) + $STD apk cache clean + ;; + dnf) + $STD dnf clean all + $STD dnf autoremove -y + ;; + yum) + $STD yum clean all + ;; + zypper) + $STD zypper clean + ;; + emerge) + $STD emerge --quiet --depclean + ;; + *) + return 0 + ;; + esac } # ============================================================================== @@ -366,28 +366,28 @@ pkg_clean() { # Enables a service to start at boot # ------------------------------------------------------------------------------ svc_enable() { - local service="$1" - [[ -z "$service" ]] && return 1 + local service="$1" + [[ -z "$service" ]] && return 1 - case "$INIT_SYSTEM" in - systemd) - $STD systemctl enable "$service" - ;; - openrc) - $STD rc-update add "$service" default - ;; - sysvinit) - if command -v update-rc.d &>/dev/null; then - $STD update-rc.d "$service" defaults - elif command -v chkconfig &>/dev/null; then - $STD chkconfig "$service" on - fi - ;; - *) - msg_warn "Unknown init system, cannot enable $service" - return 1 - ;; - esac + case "$INIT_SYSTEM" in + systemd) + $STD systemctl enable "$service" + ;; + openrc) + $STD rc-update add "$service" default + ;; + sysvinit) + if command -v update-rc.d &>/dev/null; then + $STD update-rc.d "$service" defaults + elif command -v chkconfig &>/dev/null; then + $STD chkconfig "$service" on + fi + ;; + *) + msg_warn "Unknown init system, cannot enable $service" + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -396,27 +396,27 @@ svc_enable() { # Disables a service from starting at boot # ------------------------------------------------------------------------------ svc_disable() { - local service="$1" - [[ -z "$service" ]] && return 1 + local service="$1" + [[ -z "$service" ]] && return 1 - case "$INIT_SYSTEM" in - systemd) - $STD systemctl disable "$service" - ;; - openrc) - $STD rc-update del "$service" default 2>/dev/null || true - ;; - sysvinit) - if command -v update-rc.d &>/dev/null; then - $STD update-rc.d "$service" remove - elif command -v chkconfig &>/dev/null; then - $STD chkconfig "$service" off - fi - ;; - *) - return 1 - ;; - esac + case "$INIT_SYSTEM" in + systemd) + $STD systemctl disable "$service" + ;; + openrc) + $STD rc-update del "$service" default 2>/dev/null || true + ;; + sysvinit) + if command -v update-rc.d &>/dev/null; then + $STD update-rc.d "$service" remove + elif command -v chkconfig &>/dev/null; then + $STD chkconfig "$service" off + fi + ;; + *) + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -425,23 +425,23 @@ svc_disable() { # Starts a service immediately # ------------------------------------------------------------------------------ svc_start() { - local service="$1" - [[ -z "$service" ]] && return 1 + local service="$1" + [[ -z "$service" ]] && return 1 - case "$INIT_SYSTEM" in - systemd) - $STD systemctl start "$service" - ;; - openrc) - $STD rc-service "$service" start - ;; - sysvinit) - $STD /etc/init.d/"$service" start - ;; - *) - return 1 - ;; - esac + case "$INIT_SYSTEM" in + systemd) + $STD systemctl start "$service" + ;; + openrc) + $STD rc-service "$service" start + ;; + sysvinit) + $STD /etc/init.d/"$service" start + ;; + *) + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -450,23 +450,23 @@ svc_start() { # Stops a running service # ------------------------------------------------------------------------------ svc_stop() { - local service="$1" - [[ -z "$service" ]] && return 1 + local service="$1" + [[ -z "$service" ]] && return 1 - case "$INIT_SYSTEM" in - systemd) - $STD systemctl stop "$service" - ;; - openrc) - $STD rc-service "$service" stop - ;; - sysvinit) - $STD /etc/init.d/"$service" stop - ;; - *) - return 1 - ;; - esac + case "$INIT_SYSTEM" in + systemd) + $STD systemctl stop "$service" + ;; + openrc) + $STD rc-service "$service" stop + ;; + sysvinit) + $STD /etc/init.d/"$service" stop + ;; + *) + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -475,23 +475,23 @@ svc_stop() { # Restarts a service # ------------------------------------------------------------------------------ svc_restart() { - local service="$1" - [[ -z "$service" ]] && return 1 + local service="$1" + [[ -z "$service" ]] && return 1 - case "$INIT_SYSTEM" in - systemd) - $STD systemctl restart "$service" - ;; - openrc) - $STD rc-service "$service" restart - ;; - sysvinit) - $STD /etc/init.d/"$service" restart - ;; - *) - return 1 - ;; - esac + case "$INIT_SYSTEM" in + systemd) + $STD systemctl restart "$service" + ;; + openrc) + $STD rc-service "$service" restart + ;; + sysvinit) + $STD /etc/init.d/"$service" restart + ;; + *) + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -500,23 +500,23 @@ svc_restart() { # Gets service status (returns 0 if running) # ------------------------------------------------------------------------------ svc_status() { - local service="$1" - [[ -z "$service" ]] && return 1 + local service="$1" + [[ -z "$service" ]] && return 1 - case "$INIT_SYSTEM" in - systemd) - systemctl is-active --quiet "$service" - ;; - openrc) - rc-service "$service" status &>/dev/null - ;; - sysvinit) - /etc/init.d/"$service" status &>/dev/null - ;; - *) - return 1 - ;; - esac + case "$INIT_SYSTEM" in + systemd) + systemctl is-active --quiet "$service" + ;; + openrc) + rc-service "$service" status &>/dev/null + ;; + sysvinit) + /etc/init.d/"$service" status &>/dev/null + ;; + *) + return 1 + ;; + esac } # ------------------------------------------------------------------------------ @@ -525,15 +525,15 @@ svc_status() { # Reloads init system daemon configuration (for systemd) # ------------------------------------------------------------------------------ svc_reload_daemon() { - case "$INIT_SYSTEM" in - systemd) - $STD systemctl daemon-reload - ;; - *) - # Other init systems don't need this - return 0 - ;; - esac + case "$INIT_SYSTEM" in + systemd) + $STD systemctl daemon-reload + ;; + *) + # Other init systems don't need this + return 0 + ;; + esac } # ============================================================================== @@ -547,24 +547,24 @@ svc_reload_daemon() { # Returns: IP address string # ------------------------------------------------------------------------------ get_ip() { - local ip="" + local ip="" - # Try hostname -I first (most common) - if command -v hostname &>/dev/null; then - ip=$(hostname -I 2>/dev/null | awk '{print $1}') - fi + # Try hostname -I first (most common) + if command -v hostname &>/dev/null; then + ip=$(hostname -I 2>/dev/null | awk '{print $1}') + fi - # Fallback to ip command - if [[ -z "$ip" ]] && command -v ip &>/dev/null; then - ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1) - fi + # Fallback to ip command + if [[ -z "$ip" ]] && command -v ip &>/dev/null; then + ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1) + fi - # Fallback to ifconfig - if [[ -z "$ip" ]] && command -v ifconfig &>/dev/null; then - ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1) - fi + # Fallback to ifconfig + if [[ -z "$ip" ]] && command -v ifconfig &>/dev/null; then + ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1) + fi - echo "$ip" + echo "$ip" } # ------------------------------------------------------------------------------ @@ -574,25 +574,25 @@ get_ip() { # If IPV6_METHOD=disable: disables IPv6 via sysctl # ------------------------------------------------------------------------------ verb_ip6() { - set_std_mode # Set STD mode based on VERBOSE + set_std_mode # Set STD mode based on VERBOSE - if [[ "${IPV6_METHOD:-}" == "disable" ]]; then - msg_info "Disabling IPv6 (this may affect some services)" - mkdir -p /etc/sysctl.d - cat >/etc/sysctl.d/99-disable-ipv6.conf </etc/sysctl.d/99-disable-ipv6.conf </dev/null || true + # For OpenRC, ensure sysctl runs at boot + if [[ "$INIT_SYSTEM" == "openrc" ]]; then + $STD rc-update add sysctl default 2>/dev/null || true + fi + msg_ok "Disabled IPv6" fi - msg_ok "Disabled IPv6" - fi } # ------------------------------------------------------------------------------ @@ -604,36 +604,36 @@ EOF # - Disables network wait services # ------------------------------------------------------------------------------ setting_up_container() { - msg_info "Setting up Container OS" + msg_info "Setting up Container OS" - # Wait for network - local i - for ((i = RETRY_NUM; i > 0; i--)); do - if [[ -n "$(get_ip)" ]]; then - break + # Wait for network + local i + for ((i = RETRY_NUM; i > 0; i--)); do + if [[ -n "$(get_ip)" ]]; then + break + fi + echo 1>&2 -en "${CROSS}${RD} No Network! " + sleep "$RETRY_EVERY" + done + + if [[ -z "$(get_ip)" ]]; then + echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}" + echo -e "${NETWORK}Check Network Settings" + exit 1 fi - echo 1>&2 -en "${CROSS}${RD} No Network! " - sleep "$RETRY_EVERY" - done - if [[ -z "$(get_ip)" ]]; then - echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}" - echo -e "${NETWORK}Check Network Settings" - exit 1 - fi + # Remove Python EXTERNALLY-MANAGED restriction (Debian 12+, Ubuntu 23.04+) + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true - # Remove Python EXTERNALLY-MANAGED restriction (Debian 12+, Ubuntu 23.04+) - rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true + # Disable network wait services for faster boot + case "$INIT_SYSTEM" in + systemd) + systemctl disable -q --now systemd-networkd-wait-online.service 2>/dev/null || true + ;; + esac - # Disable network wait services for faster boot - case "$INIT_SYSTEM" in - systemd) - systemctl disable -q --now systemd-networkd-wait-online.service 2>/dev/null || true - ;; - esac - - msg_ok "Set up Container OS" - msg_ok "Network Connected: ${BL}$(get_ip)" + msg_ok "Set up Container OS" + msg_ok "Network Connected: ${BL}$(get_ip)" } # ------------------------------------------------------------------------------ @@ -643,65 +643,65 @@ setting_up_container() { # Tests connectivity to DNS servers and verifies DNS resolution # ------------------------------------------------------------------------------ network_check() { - set +e - trap - ERR - local ipv4_connected=false - local ipv6_connected=false - sleep 1 + set +e + trap - ERR + local ipv4_connected=false + local ipv6_connected=false + sleep 1 - # Check IPv4 connectivity - 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 - else - msg_error "IPv4 Internet Not Connected" - fi - - # Check IPv6 connectivity (if ping6 exists) - if command -v ping6 &>/dev/null; then - if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null; then - msg_ok "IPv6 Internet Connected" - ipv6_connected=true + # Check IPv4 connectivity + 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 else - msg_error "IPv6 Internet Not Connected" + msg_error "IPv4 Internet Not Connected" fi - fi - # Prompt if both fail - if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then - read -r -p "No Internet detected, would you like to continue anyway? " prompt - if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then - echo -e "${INFO}${RD}Expect Issues Without Internet${CL}" + # Check IPv6 connectivity (if ping6 exists) + if command -v ping6 &>/dev/null; then + if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null; then + msg_ok "IPv6 Internet Connected" + ipv6_connected=true + else + msg_error "IPv6 Internet Not Connected" + fi + fi + + # Prompt if both fail + if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then + read -r -p "No Internet detected, 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 + fi + + # DNS resolution checks + local GIT_HOSTS=("github.com" "raw.githubusercontent.com" "git.community-scripts.org") + local GIT_STATUS="Git DNS:" + local DNS_FAILED=false + + for HOST in "${GIT_HOSTS[@]}"; do + local RESOLVEDIP + RESOLVEDIP=$(getent hosts "$HOST" 2>/dev/null | awk '{ print $1 }' | head -n1) + if [[ -z "$RESOLVEDIP" ]]; then + GIT_STATUS+=" $HOST:(${DNSFAIL:-FAIL})" + DNS_FAILED=true + else + GIT_STATUS+=" $HOST:(${DNSOK:-OK})" + fi + done + + if [[ "$DNS_FAILED" == true ]]; then + fatal "$GIT_STATUS" else - echo -e "${NETWORK}Check Network Settings" - exit 1 + msg_ok "$GIT_STATUS" fi - fi - # DNS resolution checks - local GIT_HOSTS=("github.com" "raw.githubusercontent.com" "git.community-scripts.org") - local GIT_STATUS="Git DNS:" - local DNS_FAILED=false - - for HOST in "${GIT_HOSTS[@]}"; do - local RESOLVEDIP - RESOLVEDIP=$(getent hosts "$HOST" 2>/dev/null | awk '{ print $1 }' | head -n1) - if [[ -z "$RESOLVEDIP" ]]; then - GIT_STATUS+=" $HOST:(${DNSFAIL:-FAIL})" - DNS_FAILED=true - else - GIT_STATUS+=" $HOST:(${DNSOK:-OK})" - fi - done - - if [[ "$DNS_FAILED" == true ]]; then - fatal "$GIT_STATUS" - else - msg_ok "$GIT_STATUS" - fi - - set -e - trap 'error_handler $LINENO "$BASH_COMMAND"' ERR + set -e + trap 'error_handler $LINENO "$BASH_COMMAND"' ERR } # ============================================================================== @@ -714,12 +714,12 @@ network_check() { # Updates container OS and sources appropriate tools.func # ------------------------------------------------------------------------------ update_os() { - msg_info "Updating Container OS" + msg_info "Updating Container OS" - # Configure APT cacher proxy if enabled (Debian/Ubuntu only) - if [[ "$PKG_MANAGER" == "apt" && "${CACHER:-}" == "yes" ]]; then - echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy - cat </usr/local/bin/apt-proxy-detect.sh + # Configure APT cacher proxy if enabled (Debian/Ubuntu only) + if [[ "$PKG_MANAGER" == "apt" && "${CACHER:-}" == "yes" ]]; then + echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy + cat </usr/local/bin/apt-proxy-detect.sh #!/bin/bash if nc -w1 -z "${CACHER_IP}" 3142; then echo -n "http://${CACHER_IP}:3142" @@ -727,27 +727,27 @@ else echo -n "DIRECT" fi EOF - chmod +x /usr/local/bin/apt-proxy-detect.sh - fi + chmod +x /usr/local/bin/apt-proxy-detect.sh + fi - # Update and upgrade - pkg_update - pkg_upgrade + # Update and upgrade + pkg_update + pkg_upgrade - # Remove Python EXTERNALLY-MANAGED restriction - rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true + # Remove Python EXTERNALLY-MANAGED restriction + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true - msg_ok "Updated Container OS" + msg_ok "Updated Container OS" - # Source appropriate tools.func based on OS - case "$OS_FAMILY" in - alpine) - source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-tools.func) - ;; - *) - source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func) - ;; - esac + # Source appropriate tools.func based on OS + case "$OS_FAMILY" in + alpine) + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-tools.func) + ;; + *) + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func) + ;; + esac } # ============================================================================== @@ -760,21 +760,21 @@ EOF # Configures Message of the Day and SSH settings # ------------------------------------------------------------------------------ motd_ssh() { - # Set terminal to 256-color mode - grep -qxF "export TERM='xterm-256color'" /root/.bashrc 2>/dev/null || echo "export TERM='xterm-256color'" >>/root/.bashrc + # Set terminal to 256-color mode + grep -qxF "export TERM='xterm-256color'" /root/.bashrc 2>/dev/null || echo "export TERM='xterm-256color'" >>/root/.bashrc - # Get OS information - local os_name="$OS_TYPE" - local os_version="$OS_VERSION" + # Get OS information + local os_name="$OS_TYPE" + local os_version="$OS_VERSION" - 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 '"') - 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 '"') + fi - # Create MOTD profile script - local PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" - cat >"$PROFILE_FILE" <"$PROFILE_FILE" </dev/null || true + # Disable default MOTD scripts (Debian/Ubuntu) + [[ -d /etc/update-motd.d ]] && chmod -x /etc/update-motd.d/* 2>/dev/null || true - # Configure SSH root access if requested - if [[ "${SSH_ROOT:-}" == "yes" ]]; then - # Ensure SSH server is installed - if [[ ! -f /etc/ssh/sshd_config ]]; then - msg_info "Installing SSH server" - case "$PKG_MANAGER" in - apt) - pkg_install openssh-server - ;; - apk) - pkg_install openssh - rc-update add sshd default 2>/dev/null || true - ;; - dnf | yum) - pkg_install openssh-server - ;; - zypper) - pkg_install openssh - ;; - emerge) - pkg_install net-misc/openssh - ;; - esac - msg_ok "Installed SSH server" + # Configure SSH root access if requested + if [[ "${SSH_ROOT:-}" == "yes" ]]; then + # Ensure SSH server is installed + if [[ ! -f /etc/ssh/sshd_config ]]; then + msg_info "Installing SSH server" + case "$PKG_MANAGER" in + apt) + pkg_install openssh-server + ;; + apk) + pkg_install openssh + rc-update add sshd default 2>/dev/null || true + ;; + dnf | yum) + pkg_install openssh-server + ;; + zypper) + pkg_install openssh + ;; + emerge) + pkg_install net-misc/openssh + ;; + esac + msg_ok "Installed SSH server" + fi + + local sshd_config="/etc/ssh/sshd_config" + if [[ -f "$sshd_config" ]]; then + sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" "$sshd_config" + sed -i "s/PermitRootLogin prohibit-password/PermitRootLogin yes/g" "$sshd_config" + + case "$INIT_SYSTEM" in + systemd) + svc_restart sshd 2>/dev/null || svc_restart ssh 2>/dev/null || true + ;; + openrc) + svc_enable sshd 2>/dev/null || true + svc_start sshd 2>/dev/null || true + ;; + *) + svc_restart sshd 2>/dev/null || true + ;; + esac + fi fi - - local sshd_config="/etc/ssh/sshd_config" - if [[ -f "$sshd_config" ]]; then - sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" "$sshd_config" - sed -i "s/PermitRootLogin prohibit-password/PermitRootLogin yes/g" "$sshd_config" - - case "$INIT_SYSTEM" in - systemd) - svc_restart sshd 2>/dev/null || svc_restart ssh 2>/dev/null || true - ;; - openrc) - svc_enable sshd 2>/dev/null || true - svc_start sshd 2>/dev/null || true - ;; - *) - svc_restart sshd 2>/dev/null || true - ;; - esac - fi - fi } # ============================================================================== @@ -845,82 +845,83 @@ EOF # Customizes container for passwordless login and creates update script # ------------------------------------------------------------------------------ customize() { - if [[ "${PASSWORD:-}" == "" ]]; then - msg_info "Customizing Container" + if [[ "${PASSWORD:-}" == "" ]]; then + msg_info "Customizing Container" - # Remove root password for auto-login - passwd -d root &>/dev/null || true + # Remove root password for auto-login + passwd -d root &>/dev/null || true - case "$INIT_SYSTEM" in - systemd) - # Mask services that block boot in LXC containers - # systemd-homed-firstboot.service hangs waiting for user input on Fedora - systemctl mask systemd-homed-firstboot.service &>/dev/null || true - systemctl mask systemd-homed.service &>/dev/null || true + case "$INIT_SYSTEM" in + systemd) + # Mask services that block boot in LXC containers + # systemd-homed-firstboot.service hangs waiting for user input on Fedora + systemctl mask systemd-homed-firstboot.service &>/dev/null || true + systemctl mask systemd-homed.service &>/dev/null || true - # Configure console-getty for auto-login in LXC containers - # console-getty.service is THE service that handles /dev/console in LXC - # It's present on all systemd distros but not enabled by default on Fedora/RHEL + # Configure console-getty for auto-login in LXC containers + # console-getty.service is THE service that handles /dev/console in LXC + # It's present on all systemd distros but not enabled by default on Fedora/RHEL - if [[ -f /usr/lib/systemd/system/console-getty.service ]]; then - mkdir -p /etc/systemd/system/console-getty.service.d - cat >/etc/systemd/system/console-getty.service.d/override.conf <<'EOF' + if [[ -f /usr/lib/systemd/system/console-getty.service ]]; then + mkdir -p /etc/systemd/system/console-getty.service.d + cat >/etc/systemd/system/console-getty.service.d/override.conf <<'EOF' [Service] ExecStart= -ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud 115200,38400,9600 $TERM +ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud 115200,38400,9600 - $TERM EOF - # Enable console-getty for LXC web console (required on Fedora/RHEL) - systemctl enable console-getty.service &>/dev/null || true - fi + # Enable console-getty for LXC web console (required on Fedora/RHEL) + systemctl enable console-getty.service &>/dev/null || true + fi - # Also configure container-getty@1 (Debian/Ubuntu default in LXC) - if [[ -f /usr/lib/systemd/system/container-getty@.service ]]; then - mkdir -p /etc/systemd/system/container-getty@1.service.d - cat >/etc/systemd/system/container-getty@1.service.d/override.conf <<'EOF' + # Also configure container-getty@1 (Debian/Ubuntu default in LXC) + if [[ -f /usr/lib/systemd/system/container-getty@.service ]]; then + mkdir -p /etc/systemd/system/container-getty@1.service.d + cat >/etc/systemd/system/container-getty@1.service.d/override.conf <<'EOF' [Service] ExecStart= -ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 $TERM +ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 - $TERM EOF - fi + fi - # Reload systemd and restart getty services to apply auto-login - systemctl daemon-reload - systemctl restart console-getty.service &>/dev/null || true - systemctl restart container-getty@1.service &>/dev/null || true - ;; + # Reload systemd and restart getty services to apply auto-login + systemctl daemon-reload + systemctl restart console-getty.service &>/dev/null || true + systemctl restart container-getty@1.service &>/dev/null || true + ;; - openrc) - # Alpine/Gentoo: modify inittab for auto-login - if [[ -f /etc/inittab ]]; then - sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab - fi - touch /root/.hushlogin - ;; + openrc) + # Alpine/Gentoo: modify inittab for auto-login + if [[ -f /etc/inittab ]]; then + sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab + fi + touch /root/.hushlogin + ;; - sysvinit) - # Devuan/older systems - just modify inittab, no telinit needed during install - if [[ -f /etc/inittab ]]; then - sed -i 's|^1:2345:respawn:/sbin/getty.*|1:2345:respawn:/sbin/agetty --autologin root tty1 38400 linux|' /etc/inittab - fi - ;; - esac + sysvinit) + # Devuan/older systems - modify inittab with flexible runlevel matching + if [[ -f /etc/inittab ]]; then + # Match various runlevel patterns (23, 2345, 12345, etc.) and both getty/agetty + sed -i 's|^1:[0-9]*:respawn:/sbin/a\?getty.*|1:2345:respawn:/sbin/agetty --autologin root tty1 38400 linux|' /etc/inittab + fi + ;; + esac - msg_ok "Customized Container" - fi + msg_ok "Customized Container" + fi - # Create update script - # Use var_os for OS-based containers, otherwise use app name - local update_script_name="${var_os:-$app}" - echo "bash -c \"\$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/ct/${update_script_name}.sh)\"" >/usr/bin/update - chmod +x /usr/bin/update + # Create update script + # Use var_os for OS-based containers, otherwise use app name + local update_script_name="${var_os:-$app}" + echo "bash -c \"\$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/ct/${update_script_name}.sh)\"" >/usr/bin/update + chmod +x /usr/bin/update - # Inject SSH authorized keys if provided - if [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then - mkdir -p /root/.ssh - echo "${SSH_AUTHORIZED_KEY}" >/root/.ssh/authorized_keys - chmod 700 /root/.ssh - chmod 600 /root/.ssh/authorized_keys - fi + # Inject SSH authorized keys if provided + if [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then + mkdir -p /root/.ssh + echo "${SSH_AUTHORIZED_KEY}" >/root/.ssh/authorized_keys + chmod 700 /root/.ssh + chmod 600 /root/.ssh/authorized_keys + fi } # ============================================================================== @@ -934,8 +935,8 @@ EOF # Returns: 0 if valid, 1 if invalid # ------------------------------------------------------------------------------ validate_tz() { - local tz="$1" - [[ -f "/usr/share/zoneinfo/$tz" ]] + local tz="$1" + [[ -f "/usr/share/zoneinfo/$tz" ]] } # ------------------------------------------------------------------------------ @@ -944,21 +945,21 @@ validate_tz() { # Sets container timezone # ------------------------------------------------------------------------------ set_timezone() { - local tz="$1" - if validate_tz "$tz"; then - ln -sf "/usr/share/zoneinfo/$tz" /etc/localtime - echo "$tz" >/etc/timezone 2>/dev/null || true + local tz="$1" + if validate_tz "$tz"; then + ln -sf "/usr/share/zoneinfo/$tz" /etc/localtime + echo "$tz" >/etc/timezone 2>/dev/null || true - # Update tzdata if available - case "$PKG_MANAGER" in - apt) - dpkg-reconfigure -f noninteractive tzdata 2>/dev/null || true - ;; - esac - msg_ok "Timezone set to $tz" - else - msg_warn "Invalid timezone: $tz" - fi + # Update tzdata if available + case "$PKG_MANAGER" in + apt) + dpkg-reconfigure -f noninteractive tzdata 2>/dev/null || true + ;; + esac + msg_ok "Timezone set to $tz" + else + msg_warn "Invalid timezone: $tz" + fi } # ------------------------------------------------------------------------------ @@ -967,9 +968,9 @@ set_timezone() { # Prints detected OS information (for debugging) # ------------------------------------------------------------------------------ os_info() { - echo "OS Type: $OS_TYPE" - echo "OS Family: $OS_FAMILY" - echo "OS Version: $OS_VERSION" - echo "Pkg Manager: $PKG_MANAGER" - echo "Init System: $INIT_SYSTEM" + echo "OS Type: $OS_TYPE" + echo "OS Family: $OS_FAMILY" + echo "OS Version: $OS_VERSION" + echo "Pkg Manager: $PKG_MANAGER" + echo "Init System: $INIT_SYSTEM" } diff --git a/misc/tools.func b/misc/tools.func index 716a74d8a..f7c13fa7c 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -72,15 +72,19 @@ 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 | + # Find all matching services (grep || true to handle no matches) + local services + services=$(systemctl list-units --type=service --all 2>/dev/null | + grep -oE "${pattern}[^ ]*\.service" 2>/dev/null | sort -u) || true + + if [[ -n "$services" ]]; then while read -r service; do $STD systemctl stop "$service" 2>/dev/null || true $STD systemctl disable "$service" 2>/dev/null || true - done + done <<<"$services" + fi done + } # ------------------------------------------------------------------------------ @@ -188,6 +192,8 @@ install_packages_with_retry() { if [[ $retry -le $max_retries ]]; then msg_warn "Package installation failed, retrying ($retry/$max_retries)..." sleep 2 + # Fix any interrupted dpkg operations before retry + $STD dpkg --configure -a 2>/dev/null || true $STD apt update 2>/dev/null || true fi done @@ -213,6 +219,8 @@ upgrade_packages_with_retry() { if [[ $retry -le $max_retries ]]; then msg_warn "Package upgrade failed, retrying ($retry/$max_retries)..." sleep 2 + # Fix any interrupted dpkg operations before retry + $STD dpkg --configure -a 2>/dev/null || true $STD apt update 2>/dev/null || true fi done @@ -1178,6 +1186,12 @@ cleanup_orphaned_sources() { # This should be called at the start of any setup function # ------------------------------------------------------------------------------ ensure_apt_working() { + # Fix interrupted dpkg operations first + # This can happen if a previous installation was interrupted (e.g., by script error) + if [[ -f /var/lib/dpkg/lock-frontend ]] || dpkg --audit 2>&1 | grep -q "interrupted"; then + $STD dpkg --configure -a 2>/dev/null || true + fi + # Clean up orphaned sources first cleanup_orphaned_sources @@ -1208,6 +1222,7 @@ setup_deb822_repo() { local suite="$4" local component="${5:-main}" local architectures="${6-}" # optional + local enabled="${7-}" # optional: "true" or "false" # Validate required parameters if [[ -z "$name" || -z "$gpg_url" || -z "$repo_url" || -z "$suite" ]]; then @@ -1235,9 +1250,13 @@ setup_deb822_repo() { echo "Types: deb" echo "URIs: $repo_url" echo "Suites: $suite" - echo "Components: $component" + # Flat repositories (suite="./" or absolute path) must not have Components + if [[ "$suite" != "./" && -n "$component" ]]; then + echo "Components: $component" + fi [[ -n "$architectures" ]] && echo "Architectures: $architectures" echo "Signed-By: /etc/apt/keyrings/${name}.gpg" + [[ -n "$enabled" ]] && echo "Enabled: $enabled" } >/etc/apt/sources.list.d/${name}.sources $STD apt update @@ -1439,15 +1458,32 @@ check_for_gh_release() { ensure_dependencies jq - # Fetch releases and exclude drafts/prereleases - local releases_json - releases_json=$(curl -fsSL --max-time 20 \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "https://api.github.com/repos/${source}/releases") || { - msg_error "Unable to fetch releases for ${app}" - return 1 - } + # Try /latest endpoint for non-pinned versions (most efficient) + local releases_json="" + + if [[ -z "$pinned_version_in" ]]; then + releases_json=$(curl -fsSL --max-time 20 \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "https://api.github.com/repos/${source}/releases/latest" 2>/dev/null) + + if [[ $? -eq 0 ]] && [[ -n "$releases_json" ]]; then + # Wrap single release in array for consistent processing + releases_json="[$releases_json]" + fi + fi + + # If no releases yet (pinned version OR /latest failed), fetch up to 100 + if [[ -z "$releases_json" ]]; then + # Fetch releases and exclude drafts/prereleases + releases_json=$(curl -fsSL --max-time 20 \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "https://api.github.com/repos/${source}/releases?per_page=100") || { + msg_error "Unable to fetch releases for ${app}" + return 1 + } + fi mapfile -t raw_tags < <(jq -r '.[] | select(.draft==false and .prerelease==false) | .tag_name' <<<"$releases_json") if ((${#raw_tags[@]} == 0)); then @@ -1721,12 +1757,13 @@ function fetch_and_deploy_gh_release() { ### Tarball Mode ### if [[ "$mode" == "tarball" || "$mode" == "source" ]]; then - url=$(echo "$json" | jq -r '.tarball_url // empty') - [[ -z "$url" ]] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz" + # GitHub API's tarball_url/zipball_url can return HTTP 300 Multiple Choices + # when a branch and tag share the same name. Use explicit refs/tags/ URL instead. + local direct_tarball_url="https://github.com/$repo/archive/refs/tags/$tag_name.tar.gz" filename="${app_lc}-${version}.tar.gz" - curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url" || { - msg_error "Download failed: $url" + curl $download_timeout -fsSL -o "$tmpdir/$filename" "$direct_tarball_url" || { + msg_error "Download failed: $direct_tarball_url" rm -rf "$tmpdir" return 1 } @@ -2548,93 +2585,203 @@ function setup_hwaccel() { fi # Detect GPU vendor (Intel, AMD, NVIDIA) - local gpu_vendor - gpu_vendor=$(lspci 2>/dev/null | grep -Ei 'vga|3d|display' | grep -Eo 'Intel|AMD|NVIDIA' | head -n1 || echo "") + local gpu_vendor gpu_info + gpu_info=$(lspci 2>/dev/null | grep -Ei 'vga|3d|display' || echo "") + gpu_vendor=$(echo "$gpu_info" | grep -Eo 'Intel|AMD|NVIDIA' | head -n1 || echo "") # Detect CPU vendor (relevant for AMD APUs) local cpu_vendor cpu_vendor=$(lscpu 2>/dev/null | grep -i 'Vendor ID' | awk '{print $3}' || echo "") if [[ -z "$gpu_vendor" && -z "$cpu_vendor" ]]; then - msg_error "No GPU or CPU vendor detected (missing lspci/lscpu output)" - return 1 + msg_warn "No GPU or CPU vendor detected - skipping hardware acceleration setup" + msg_ok "Setup Hardware Acceleration (skipped - no GPU detected)" + return 0 fi # Detect OS with fallbacks - local os_id os_codename - os_id=$(grep -oP '(?<=^ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || grep '^ID=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "debian") - os_codename=$(grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || grep '^VERSION_CODENAME=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "unknown") + local os_id os_codename os_version + os_id=$(grep -oP '(?<=^ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "debian") + os_codename=$(grep -oP '(?<=^VERSION_CODENAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "unknown") + os_version=$(grep -oP '(?<=^VERSION_ID=).+' /etc/os-release 2>/dev/null | tr -d '"' || echo "") - # Validate os_id - if [[ -z "$os_id" ]]; then - os_id="debian" - fi + [[ -z "$os_id" ]] && os_id="debian" - # Determine if we are on a VM or LXC + # Determine if we are in a privileged LXC container local in_ct="${CTTYPE:-0}" case "$gpu_vendor" in Intel) - if [[ "$os_id" == "ubuntu" ]]; then - $STD apt -y install intel-opencl-icd || { - msg_error "Failed to install intel-opencl-icd" - return 1 - } - else - # For Debian: fetch Intel GPU drivers from GitHub - fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb" || { - msg_warn "Failed to deploy Intel IGC core 2" - } - fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb" || { - msg_warn "Failed to deploy Intel IGC OpenCL 2" - } - fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || { - msg_warn "Failed to deploy Intel GDGMM12" - } - fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || { - msg_warn "Failed to deploy Intel OpenCL ICD" - } + # Detect Intel GPU generation for driver selection + # Gen 9+ (Skylake and newer) benefit from non-free drivers + local intel_gen="" + local needs_nonfree=false + + # Check for specific Intel GPU models that need non-free drivers + if echo "$gpu_info" | grep -Ei 'HD Graphics [56][0-9]{2}|UHD Graphics|Iris|Arc|DG[12]' &>/dev/null; then + needs_nonfree=true + intel_gen="gen9+" fi - $STD apt -y install va-driver-all ocl-icd-libopencl1 vainfo intel-gpu-tools || { - msg_error "Failed to install Intel GPU dependencies" - return 1 - } + if [[ "$os_id" == "ubuntu" ]]; then + # Ubuntu: Use packages from Ubuntu repos + $STD apt -y install \ + va-driver-all \ + ocl-icd-libopencl1 \ + intel-opencl-icd \ + vainfo \ + libmfx-gen1.2 \ + intel-gpu-tools || { + msg_error "Failed to install Intel GPU dependencies" + return 1 + } + # Try to install intel-media-va-driver for newer GPUs + $STD apt -y install intel-media-va-driver 2>/dev/null || true + + elif [[ "$os_id" == "debian" ]]; then + # Debian: Check version and install appropriate drivers + if [[ "$needs_nonfree" == true ]]; then + # Add non-free repo for intel-media-va-driver-non-free + if [[ "$os_codename" == "bookworm" ]]; then + # Debian 12 Bookworm + if [[ ! -f /etc/apt/sources.list.d/non-free.list && ! -f /etc/apt/sources.list.d/non-free.sources ]]; then + cat </etc/apt/sources.list.d/non-free.sources +Types: deb +URIs: http://deb.debian.org/debian +Suites: bookworm bookworm-updates +Components: non-free non-free-firmware +EOF + $STD apt update + fi + $STD apt -y install \ + intel-media-va-driver-non-free \ + ocl-icd-libopencl1 \ + intel-opencl-icd \ + vainfo \ + libmfx-gen1.2 \ + intel-gpu-tools || { + msg_warn "Non-free driver install failed, falling back to open drivers" + needs_nonfree=false + } + + elif [[ "$os_codename" == "trixie" || "$os_codename" == "sid" ]]; then + # Debian 13 Trixie / Sid + if [[ ! -f /etc/apt/sources.list.d/non-free.sources ]]; then + cat <<'EOF' >/etc/apt/sources.list.d/non-free.sources +Types: deb +URIs: http://deb.debian.org/debian +Suites: trixie trixie-updates +Components: non-free non-free-firmware + +Types: deb +URIs: http://deb.debian.org/debian-security +Suites: trixie-security +Components: non-free non-free-firmware +EOF + $STD apt update + fi + $STD apt -y install \ + intel-media-va-driver-non-free \ + ocl-icd-libopencl1 \ + mesa-opencl-icd \ + mesa-va-drivers \ + libvpl2 \ + vainfo \ + libmfx-gen1.2 \ + intel-gpu-tools 2>/dev/null || { + msg_warn "Non-free driver install failed, falling back to open drivers" + needs_nonfree=false + } + fi + fi + + # Fallback to open drivers or older Intel GPUs + if [[ "$needs_nonfree" == false ]]; then + # Fetch latest Intel drivers from GitHub for Debian + fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-core-2_*_amd64.deb" || { + msg_warn "Failed to deploy Intel IGC core 2" + } + fetch_and_deploy_gh_release "" "intel/intel-graphics-compiler" "binary" "latest" "" "intel-igc-opencl-2_*_amd64.deb" || { + msg_warn "Failed to deploy Intel IGC OpenCL 2" + } + fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "libigdgmm12_*_amd64.deb" || { + msg_warn "Failed to deploy Intel GDGMM12" + } + fetch_and_deploy_gh_release "" "intel/compute-runtime" "binary" "latest" "" "intel-opencl-icd_*_amd64.deb" || { + msg_warn "Failed to deploy Intel OpenCL ICD" + } + + $STD apt -y install \ + va-driver-all \ + ocl-icd-libopencl1 \ + mesa-opencl-icd \ + mesa-va-drivers \ + vainfo \ + intel-gpu-tools || { + msg_error "Failed to install Intel GPU dependencies" + return 1 + } + fi + fi ;; + AMD) - $STD apt -y install mesa-va-drivers mesa-vdpau-drivers mesa-opencl-icd vainfo clinfo || { + $STD apt -y install \ + mesa-va-drivers \ + mesa-vdpau-drivers \ + mesa-opencl-icd \ + ocl-icd-libopencl1 \ + vainfo \ + clinfo 2>/dev/null || { msg_error "Failed to install AMD GPU dependencies" return 1 } - # For AMD CPUs without discrete GPU (APUs) - if [[ "$cpu_vendor" == "AuthenticAMD" && -n "$gpu_vendor" ]]; then - $STD apt -y install libdrm-amdgpu1 firmware-amd-graphics || true + # AMD firmware for better GPU support + if [[ "$os_id" == "debian" ]]; then + $STD apt -y install firmware-amd-graphics 2>/dev/null || true fi + $STD apt -y install libdrm-amdgpu1 2>/dev/null || true ;; + NVIDIA) - # NVIDIA needs manual driver setup - skip for now - msg_info "NVIDIA GPU detected - manual driver setup required" + # NVIDIA needs manual driver setup or passthrough from host + msg_warn "NVIDIA GPU detected - driver must be installed manually or passed through from host" + # Install basic VA-API support for potential hybrid setups + $STD apt -y install va-driver-all vainfo 2>/dev/null || true ;; + *) - # If no discrete GPU, but AMD CPU (e.g., Ryzen APU) + # No discrete GPU detected - check for AMD APU if [[ "$cpu_vendor" == "AuthenticAMD" ]]; then - $STD apt -y install mesa-opencl-icd ocl-icd-libopencl1 clinfo || { - msg_error "Failed to install Mesa OpenCL stack" - return 1 - } + $STD apt -y install \ + mesa-va-drivers \ + mesa-vdpau-drivers \ + mesa-opencl-icd \ + ocl-icd-libopencl1 \ + vainfo 2>/dev/null || true else - msg_warn "No supported GPU vendor detected - skipping GPU acceleration" + msg_warn "No supported GPU vendor detected - skipping GPU driver installation" fi ;; esac - if [[ -d /dev/dri ]]; then + # Set permissions for /dev/dri (only in privileged containers and if /dev/dri exists) + if [[ "$in_ct" == "0" && -d /dev/dri ]]; then chgrp video /dev/dri 2>/dev/null || true chmod 755 /dev/dri 2>/dev/null || true chmod 660 /dev/dri/* 2>/dev/null || true - $STD adduser "$(id -u -n)" video - $STD adduser "$(id -u -n)" render + $STD adduser "$(id -u -n)" video 2>/dev/null || true + $STD adduser "$(id -u -n)" render 2>/dev/null || true + + # Sync GID for video/render groups between host and container + local host_video_gid host_render_gid + host_video_gid=$(getent group video | cut -d: -f3) + host_render_gid=$(getent group render | cut -d: -f3) + if [[ -n "$host_video_gid" && -n "$host_render_gid" ]]; then + sed -i "s/^video:x:[0-9]*:/video:x:$host_video_gid:/" /etc/group 2>/dev/null || true + sed -i "s/^render:x:[0-9]*:/render:x:$host_render_gid:/" /etc/group 2>/dev/null || true + fi fi cache_installed_version "hwaccel" "1.0" @@ -2780,12 +2927,19 @@ function setup_java() { INSTALLED_VERSION=$(dpkg -l 2>/dev/null | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+' | head -n1 || echo "") fi - # Validate INSTALLED_VERSION is not empty if matched + # Validate INSTALLED_VERSION is not empty if JDK package found local JDK_COUNT=0 JDK_COUNT=$(dpkg -l 2>/dev/null | grep -c "temurin-.*-jdk" || true) if [[ -z "$INSTALLED_VERSION" && "${JDK_COUNT:-0}" -gt 0 ]]; then - msg_warn "Found Temurin JDK but cannot determine version" - INSTALLED_VERSION="0" + msg_warn "Found Temurin JDK but cannot determine version - attempting reinstall" + # Try to get actual package name for purge + local OLD_PACKAGE + OLD_PACKAGE=$(dpkg -l 2>/dev/null | awk '/temurin-.*-jdk/{print $2}' | head -n1 || echo "") + if [[ -n "$OLD_PACKAGE" ]]; then + msg_info "Removing existing package: $OLD_PACKAGE" + $STD apt purge -y "$OLD_PACKAGE" || true + fi + INSTALLED_VERSION="" # Reset to trigger fresh install fi # Scenario 1: Already at correct version @@ -3234,7 +3388,6 @@ function setup_mongodb() { 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 @@ -3410,12 +3563,12 @@ EOF # - Optionally installs or updates global npm modules # # Variables: -# NODE_VERSION - Node.js version to install (default: 22) +# NODE_VERSION - Node.js version to install (default: 24 LTS) # NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0") # ------------------------------------------------------------------------------ function setup_nodejs() { - local NODE_VERSION="${NODE_VERSION:-22}" + local NODE_VERSION="${NODE_VERSION:-24}" local NODE_MODULE="${NODE_MODULE:-}" # ALWAYS clean up legacy installations first (nvm, etc.) to prevent conflicts @@ -3477,14 +3630,11 @@ function setup_nodejs() { return 1 } - # CRITICAL: Force APT cache refresh AFTER repository setup - # This ensures NodeSource is the only nodejs source in APT cache + # Force APT cache refresh after repository setup $STD apt update - # Install dependencies (NodeSource is now the only nodejs source) ensure_dependencies curl ca-certificates gnupg - # Install Node.js from NodeSource install_packages_with_retry "nodejs" || { msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" return 1 @@ -3635,7 +3785,7 @@ function setup_php() { local CURRENT_PHP="" CURRENT_PHP=$(is_tool_installed "php" 2>/dev/null) || true - # CRITICAL: If wrong version is installed, remove it FIRST before any pinning + # Remove conflicting PHP version before pinning if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then msg_info "Removing conflicting PHP ${CURRENT_PHP} (need ${PHP_VERSION})" stop_all_services "php.*-fpm" @@ -3782,7 +3932,6 @@ EOF local INSTALLED_VERSION=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) - # Critical: if major.minor doesn't match, fail and cleanup if [[ "$INSTALLED_VERSION" != "$PHP_VERSION" ]]; then msg_error "PHP version mismatch: requested ${PHP_VERSION} but got ${INSTALLED_VERSION}" msg_error "This indicates a critical package installation issue" @@ -3862,11 +4011,14 @@ function setup_postgresql() { local SUITE case "$DISTRO_CODENAME" in trixie | forky | sid) + if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then SUITE="trixie-pgdg" + else SUITE="bookworm-pgdg" fi + ;; *) SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") @@ -4387,7 +4539,7 @@ function setup_rust() { # Get currently installed version local CURRENT_VERSION="" if command -v rustc &>/dev/null; then - CURRENT_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + CURRENT_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true fi # Scenario 1: Rustup not installed - fresh install @@ -4406,7 +4558,8 @@ function setup_rust() { return 1 fi - local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + local RUST_VERSION + RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true if [[ -z "$RUST_VERSION" ]]; then msg_error "Failed to determine Rust version" return 1 @@ -4437,7 +4590,8 @@ function setup_rust() { # Ensure PATH is updated for current shell session export PATH="$CARGO_BIN:$PATH" - local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') + local RUST_VERSION + RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}' 2>/dev/null) || true if [[ -z "$RUST_VERSION" ]]; then msg_error "Failed to determine Rust version after update" return 1 @@ -4524,6 +4678,8 @@ function setup_uv() { local UVX_BIN="/usr/local/bin/uvx" local TMP_DIR=$(mktemp -d) local CACHED_VERSION + local TARGET_VERSION="" + local USE_PINNED_VERSION=false # trap for TMP Cleanup trap "rm -rf '$TMP_DIR'" EXIT @@ -4559,22 +4715,27 @@ function setup_uv() { ensure_dependencies jq - # 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 "") + # Check if specific version is requested via UV_VERSION environment variable + if [[ -n "${UV_VERSION:-}" ]]; then + TARGET_VERSION="${UV_VERSION}" + USE_PINNED_VERSION=true + else + # Fetch latest version from GitHub API + local releases_json + 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" - return 1 - fi + if [[ -z "$releases_json" ]]; then + msg_error "Could not fetch latest uv version from GitHub API" + return 1 + fi - local LATEST_VERSION - LATEST_VERSION=$(echo "$releases_json" | jq -r '.tag_name' 2>/dev/null | sed 's/^v//') + TARGET_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" - return 1 + if [[ -z "$TARGET_VERSION" ]]; then + msg_error "Could not parse uv version from GitHub API response" + return 1 + fi fi # Get currently installed version @@ -4583,9 +4744,9 @@ function setup_uv() { 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" + # Scenario 1: Already at target version + if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" == "$TARGET_VERSION" ]]; then + cache_installed_version "uv" "$TARGET_VERSION" # Check if uvx is needed and missing if [[ "${USE_UVX:-NO}" == "YES" ]] && [[ ! -x "$UVX_BIN" ]]; then @@ -4597,14 +4758,22 @@ function setup_uv() { return 0 fi - # Scenario 2: New install or upgrade - if [[ -n "$INSTALLED_VERSION" && "$INSTALLED_VERSION" != "$LATEST_VERSION" ]]; then - msg_info "Upgrade uv from $INSTALLED_VERSION to $LATEST_VERSION" + # Scenario 2: New install or upgrade/downgrade + if [[ -n "$INSTALLED_VERSION" ]]; then + if [[ "$USE_PINNED_VERSION" == true ]]; then + msg_info "Switching uv from $INSTALLED_VERSION to pinned version $TARGET_VERSION" + else + msg_info "Upgrade uv from $INSTALLED_VERSION to $TARGET_VERSION" + fi else - msg_info "Setup uv $LATEST_VERSION" + if [[ "$USE_PINNED_VERSION" == true ]]; then + msg_info "Setup uv $TARGET_VERSION (pinned)" + else + msg_info "Setup uv $TARGET_VERSION" + fi fi - local UV_URL="https://github.com/astral-sh/uv/releases/download/${LATEST_VERSION}/${UV_TAR}" + local UV_URL="https://github.com/astral-sh/uv/releases/download/${TARGET_VERSION}/${UV_TAR}" $STD curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || { msg_error "Failed to download uv from $UV_URL" @@ -4647,6 +4816,7 @@ function setup_uv() { if [[ -d /usr/share/zsh/site-functions ]]; then $STD uv generate-shell-completion zsh >/usr/share/zsh/site-functions/_uv 2>/dev/null || true fi + # Optional: Install specific Python version if requested if [[ -n "${PYTHON_VERSION:-}" ]]; then msg_info "Installing Python $PYTHON_VERSION via uv" @@ -4657,8 +4827,8 @@ function setup_uv() { msg_ok "Python $PYTHON_VERSION installed" fi - cache_installed_version "uv" "$LATEST_VERSION" - msg_ok "Setup uv $LATEST_VERSION" + cache_installed_version "uv" "$TARGET_VERSION" + msg_ok "Setup uv $TARGET_VERSION" } # Helper function to install uvx wrapper @@ -4805,6 +4975,7 @@ function setup_docker() { # 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 @@ -4818,7 +4989,6 @@ function setup_docker() { "$(get_os_info codename)" \ "stable" \ "$(dpkg --print-architecture)" - msg_ok "Set up Docker Repository" # Install or upgrade Docker if [ "$docker_installed" = true ]; then @@ -4826,8 +4996,7 @@ function setup_docker() { 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_ok "Docker update available ($DOCKER_CURRENT_VERSION → $DOCKER_LATEST_VERSION)" - msg_info "Updating Docker" + msg_info "Updating Docker $DOCKER_CURRENT_VERSION → $DOCKER_LATEST_VERSION" $STD apt install -y --only-upgrade \ docker-ce \ docker-ce-cli \ @@ -4873,8 +5042,7 @@ EOF 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 - msg_ok "Portainer update available ($PORTAINER_CURRENT → $PORTAINER_LATEST)" - read -r -p "${TAB3}Update Portainer? " prompt + read -r -p "${TAB3}Update Portainer $PORTAINER_CURRENT → $PORTAINER_LATEST? " prompt if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then msg_info "Updating Portainer" docker stop portainer @@ -4889,8 +5057,6 @@ EOF -v portainer_data:/data \ portainer/portainer-ce:latest msg_ok "Updated Portainer to $PORTAINER_LATEST" - else - msg_ok "Skipped Portainer update" fi else msg_ok "Portainer is up-to-date ($PORTAINER_CURRENT)" @@ -4938,7 +5104,6 @@ EOF done < <(docker ps --format '{{.Names}} {{.Image}}') if [ ${#containers_with_updates[@]} -gt 0 ]; then - msg_ok "Found ${#containers_with_updates[@]} container(s) with updates" echo "" echo "${TAB3}Container updates available:" for info in "${container_info[@]}"; do @@ -4967,8 +5132,6 @@ EOF msg_ok "Stopped and removed $container (please recreate with updated image)" fi done - else - msg_ok "Skipped container updates" fi else msg_ok "All containers are up-to-date" diff --git a/tools/addon/pihole-exporter.sh b/tools/addon/pihole-exporter.sh deleted file mode 100644 index e79713588..000000000 --- a/tools/addon/pihole-exporter.sh +++ /dev/null @@ -1,249 +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: https://github.com/eko/pihole-exporter/ - -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) -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/error_handler.func) -load_functions - -# Enable error handling -set -Eeuo pipefail -trap 'error_handler' ERR - -# ============================================================================== -# CONFIGURATION -# ============================================================================== -VERBOSE=${var_verbose:-no} -APP="pihole-exporter" -APP_TYPE="tools" -INSTALL_PATH="/opt/pihole-exporter" -CONFIG_PATH="/opt/pihole-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/pihole-exporter" -elif grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then - OS="Debian" - SERVICE_PATH="/etc/systemd/system/pihole-exporter.service" -else - echo -e "${CROSS} Unsupported OS detected. Exiting." - exit 1 -fi - -# ============================================================================== -# UNINSTALL -# ============================================================================== -function uninstall() { - msg_info "Uninstalling Pihole-Exporter" - if [[ "$OS" == "Alpine" ]]; then - rc-service pihole-exporter stop &>/dev/null - rc-update del pihole-exporter &>/dev/null - rm -f "$SERVICE_PATH" - else - systemctl disable -q --now pihole-exporter - rm -f "$SERVICE_PATH" - fi - rm -rf "$INSTALL_PATH" "$CONFIG_PATH" - rm -f "/usr/local/bin/update_pihole-exporter" - rm -f "$HOME/.pihole-exporter" - msg_ok "Pihole-Exporter has been uninstalled" -} - -# ============================================================================== -# UPDATE -# ============================================================================== -function update() { - if check_for_gh_release "pihole-exporter" "eko/pihole-exporter"; then - msg_info "Stopping service" - if [[ "$OS" == "Alpine" ]]; then - rc-service pihole-exporter stop &>/dev/null - else - systemctl stop pihole-exporter - fi - msg_ok "Stopped service" - - fetch_and_deploy_gh_release "pihole-exporter" "eko/pihole-exporter" "tarball" "latest" - setup_go - - msg_info "Building Pihole-Exporter" - cd /opt/pihole-exporter/ - $STD /usr/local/bin/go build -o ./pihole-exporter - msg_ok "Built Pihole-Exporter" - - msg_info "Starting service" - if [[ "$OS" == "Alpine" ]]; then - rc-service pihole-exporter start - else - systemctl start pihole-exporter - fi - msg_ok "Started service" - msg_ok "Updated successfully" - exit - fi -} - -# ============================================================================== -# INSTALL -# ============================================================================== -function install() { - read -erp "Enter the protocol to use (http/https), default https: " pihole_PROTOCOL - read -erp "Enter the hostname of Pihole, example: (127.0.0.1): " pihole_HOSTNAME - read -erp "Enter the port of Pihole, default 443: " pihole_PORT - read -rsp "Enter Pihole password: " pihole_PASSWORD - printf "\n" - read -erp "Do you want to skip TLS-Verification (if using a self-signed Certificate on Pi-Hole) [y/N]: " SKIP_TLS - if [[ "${SKIP_TLS,,}" =~ ^(y|yes)$ ]]; then - pihole_SKIP_TLS="true" - fi - - fetch_and_deploy_gh_release "pihole-exporter" "eko/pihole-exporter" "tarball" "latest" - setup_go - msg_info "Building Pihole-Exporter on ${OS}" - cd /opt/pihole-exporter/ - $STD /usr/local/bin/go build -o ./pihole-exporter - msg_ok "Built Pihole-Exporter" - - msg_info "Creating configuration" - cat <"$CONFIG_PATH" -# https://github.com/eko/pihole-exporter/?tab=readme-ov-file#available-cli-options -PIHOLE_PASSWORD="${pihole_PASSWORD}" -PIHOLE_HOSTNAME="${pihole_HOSTNAME}" -PIHOLE_PORT="${pihole_PORT:-443}" -SKIP_TLS_VERIFICATION="${pihole_SKIP_TLS:-false}" -PIHOLE_PROTOCOL="${pihole_PROTOCOL:-https}" -EOF - msg_ok "Created configuration" - - msg_info "Creating service" - if [[ "$OS" == "Debian" ]]; then - cat <"$SERVICE_PATH" -[Unit] -Description=pihole-exporter -After=network.target - -[Service] -User=root -WorkingDirectory=/opt/pihole-exporter -EnvironmentFile=$CONFIG_PATH -ExecStart=/opt/pihole-exporter/pihole-exporter -Restart=always - -[Install] -WantedBy=multi-user.target -EOF - systemctl daemon-reload - systemctl enable -q --now pihole-exporter - else - cat <"$SERVICE_PATH" -#!/sbin/openrc-run - -name="pihole-exporter" -description="Pi-hole Exporter for Prometheus" -command="${INSTALL_PATH}/pihole-exporter" -command_background=true -directory="/opt/pihole-exporter" -pidfile="/run/\${RC_SVCNAME}.pid" -output_log="/var/log/pihole-exporter.log" -error_log="/var/log/pihole-exporter.log" - -depend() { - need net - after firewall -} - -start_pre() { - if [ -f "$CONFIG_PATH" ]; then - export \$(grep -v '^#' $CONFIG_PATH | xargs) - fi -} -EOF - chmod +x "$SERVICE_PATH" - rc-update add pihole-exporter default - rc-service pihole-exporter start - fi - msg_ok "Created and started service" - - # Create update script - msg_info "Creating update script" - cat <<'UPDATEEOF' >/usr/local/bin/update_pihole-exporter -#!/usr/bin/env bash -# pihole-exporter Update Script -type=update bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/tools/addon/pihole-exporter.sh)" -UPDATEEOF - chmod +x /usr/local/bin/update_pihole-exporter - msg_ok "Created update script (/usr/local/bin/update_pihole-exporter)" - - echo "" - msg_ok "Pihole-Exporter installed successfully" - msg_ok "Metrics: ${BL}http://${CURRENT_IP}:${DEFAULT_PORT}/metrics${CL}" - msg_ok "Config: ${BL}${CONFIG_PATH}${CL}" -} - -# ============================================================================== -# MAIN -# ============================================================================== -header_info -ensure_usr_local_bin_persist -get_current_ip &>/dev/null - -# Handle type=update (called from update script) -if [[ "${type:-}" == "update" ]]; then - if [[ -d "$INSTALL_PATH" && -f "$INSTALL_PATH/pihole-exporter" ]]; then - update - else - msg_error "Pihole-Exporter is not installed. Nothing to update." - exit 1 - fi - exit 0 -fi - -# Check if already installed -if [[ -d "$INSTALL_PATH" && -f "$INSTALL_PATH/pihole-exporter" ]]; then - msg_warn "Pihole-Exporter is already installed." - echo "" - - echo -n "${TAB}Uninstall Pihole-Exporter? (y/N): " - read -r uninstall_prompt - if [[ "${uninstall_prompt,,}" =~ ^(y|yes)$ ]]; then - uninstall - exit 0 - fi - - echo -n "${TAB}Update Pihole-Exporter? (y/N): " - read -r update_prompt - if [[ "${update_prompt,,}" =~ ^(y|yes)$ ]]; then - update - exit 0 - fi - - msg_warn "No action selected. Exiting." - exit 0 -fi - -# Fresh installation -msg_warn "Pihole-Exporter is not installed." -echo "" -echo -e "${TAB}${INFO} This will install:" -echo -e "${TAB} - Pi-hole Exporter (Go binary)" -echo -e "${TAB} - Systemd/OpenRC service" -echo "" - -echo -n "${TAB}Install Pihole-Exporter? (y/N): " -read -r install_prompt -if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then - install -else - msg_warn "Installation cancelled. Exiting." - exit 0 -fi diff --git a/tools/addon/qbittorrent-exporter.sh b/tools/addon/qbittorrent-exporter.sh deleted file mode 100644 index 03e459803..000000000 --- a/tools/addon/qbittorrent-exporter.sh +++ /dev/null @@ -1,149 +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: https://github.com/martabal/qbittorrent-exporter - -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 - $STD /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://127.0.0.1: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" "latest" -setup_go -msg_info "Installing qbittorrent-exporter on ${OS}" -cd /opt/qbittorrent-exporter/src -$STD /usr/local/bin/go build -o ./qbittorrent-exporter -msg_ok "Installed qbittorrent-exporter" - -msg_info "Creating configuration" -cat <"$CONFIG_PATH" -# https://github.com/martabal/qbittorrent-exporter?tab=readme-ov-file#parameters -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 -q --now qbittorrent-exporter -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/pve/oci-deploy.sh b/tools/pve/oci-deploy.sh new file mode 100644 index 000000000..705595a1c --- /dev/null +++ b/tools/pve/oci-deploy.sh @@ -0,0 +1,352 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2025 community-scripts ORG +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://www.proxmox.com/ + +function header_info { + clear + cat <<"EOF" + ____ ________ ______ __ _ + / __ \/ ____/ / / ____/___ ____ / /_____ _(_)___ ___ _____ + / / / / / / / / / / __ \/ __ \/ __/ __ `/ / __ \/ _ \/ ___/ +/ /_/ / /___/ / / /___/ /_/ / / / / /_/ /_/ / / / / / __/ / +\____/\____/_/ \____/\____/_/ /_/\__/\__,_/_/_/ /_/\___/_/ + +EOF +} + +YW=$(echo "\033[33m") +GN=$(echo "\033[1;92m") +RD=$(echo "\033[01;31m") +BL=$(echo "\033[36m") +CL=$(echo "\033[m") +CM="${GN}✔️${CL}" +CROSS="${RD}✖️${CL}" +INFO="${BL}ℹ️${CL}" + +APP="OCI-Container" + +header_info + +function msg_info() { + local msg="$1" + echo -e "${INFO} ${YW}${msg}...${CL}" +} + +function msg_ok() { + local msg="$1" + echo -e "${CM} ${GN}${msg}${CL}" +} + +function msg_error() { + local msg="$1" + echo -e "${CROSS} ${RD}${msg}${CL}" +} + +# Check Proxmox version +if ! command -v pveversion &>/dev/null; then + msg_error "This script must be run on Proxmox VE" + exit 1 +fi + +PVE_VER=$(pveversion | grep -oP 'pve-manager/\K[0-9.]+' | cut -d. -f1,2) +MAJOR=$(echo "$PVE_VER" | cut -d. -f1) +MINOR=$(echo "$PVE_VER" | cut -d. -f2) + +if [[ "$MAJOR" -lt 9 ]] || { [[ "$MAJOR" -eq 9 ]] && [[ "$MINOR" -lt 1 ]]; }; then + msg_error "Proxmox VE 9.1+ required (current: $PVE_VER)" + exit 1 +fi + +msg_ok "Proxmox VE $PVE_VER detected" + +# Parse OCI image +parse_image() { + local input="$1" + if [[ "$input" =~ ^([^/]+\.[^/]+)/ ]]; then + echo "$input" + elif [[ "$input" =~ / ]]; then + echo "docker.io/$input" + else + echo "docker.io/library/$input" + fi +} + +# Interactive image selection +if [[ -z "${OCI_IMAGE:-}" ]]; then + echo "" + echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e "${BL}Select OCI Image:${CL}" + echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e " ${BL}1)${CL} nginx:alpine - Lightweight web server" + echo -e " ${BL}2)${CL} postgres:16-alpine - PostgreSQL database" + echo -e " ${BL}3)${CL} redis:alpine - Redis cache" + echo -e " ${BL}4)${CL} mariadb:latest - MariaDB database" + echo -e " ${BL}5)${CL} ghcr.io/linkwarden/linkwarden:latest" + echo -e " ${BL}6)${CL} Custom image" + echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo "" + + read -r -p "Select option (1-6): " IMAGE_CHOICE + + case $IMAGE_CHOICE in + 1) OCI_IMAGE="nginx:alpine" ;; + 2) OCI_IMAGE="postgres:16-alpine" ;; + 3) OCI_IMAGE="redis:alpine" ;; + 4) OCI_IMAGE="mariadb:latest" ;; + 5) OCI_IMAGE="ghcr.io/linkwarden/linkwarden:latest" ;; + 6) + read -r -p "Enter OCI image (e.g., ghcr.io/user/repo:tag): " OCI_IMAGE + [[ -z "$OCI_IMAGE" ]] && { + msg_error "No image specified" + exit 1 + } + ;; + *) + msg_error "Invalid choice" + exit 1 + ;; + esac +fi + +FULL_IMAGE=$(parse_image "$OCI_IMAGE") +msg_ok "Selected: $FULL_IMAGE" + +# Derive container name +if [[ -z "${CT_NAME:-}" ]]; then + DEFAULT_NAME=$(echo "$OCI_IMAGE" | sed 's|.*/||; s/:.*//; s/[^a-zA-Z0-9-]/-/g' | cut -c1-60) + read -r -p "Container name [${DEFAULT_NAME}]: " CT_NAME + CT_NAME=${CT_NAME:-$DEFAULT_NAME} +fi + +# Get next VMID +if [[ -z "${VMID:-}" ]]; then + NEXT_ID=$(pvesh get /cluster/nextid) + read -r -p "Container ID [${NEXT_ID}]: " VMID + VMID=${VMID:-$NEXT_ID} +fi + +# Resources +if [[ -z "${CORES:-}" ]]; then + read -r -p "CPU cores [2]: " CORES + CORES=${CORES:-2} +fi + +if [[ -z "${MEMORY:-}" ]]; then + read -r -p "Memory in MB [2048]: " MEMORY + MEMORY=${MEMORY:-2048} +fi + +if [[ -z "${DISK:-}" ]]; then + read -r -p "Disk size in GB [8]: " DISK + DISK=${DISK:-8} +fi + +# Storage +if [[ -z "${STORAGE:-}" ]]; then + AVAIL_STORAGE=$(pvesm status | awk '/^local-(zfs|lvm)/ {print $1; exit}') + [[ -z "$AVAIL_STORAGE" ]] && AVAIL_STORAGE="local" + read -r -p "Storage [${AVAIL_STORAGE}]: " STORAGE + STORAGE=${STORAGE:-$AVAIL_STORAGE} +fi + +# Network +if [[ -z "${BRIDGE:-}" ]]; then + read -r -p "Network bridge [vmbr0]: " BRIDGE + BRIDGE=${BRIDGE:-vmbr0} +fi + +if [[ -z "${IP_MODE:-}" ]]; then + read -r -p "IP mode (dhcp/static) [dhcp]: " IP_MODE + IP_MODE=${IP_MODE:-dhcp} +fi + +if [[ "$IP_MODE" == "static" ]]; then + read -r -p "Static IP (CIDR, e.g., 192.168.1.100/24): " STATIC_IP + read -r -p "Gateway IP: " GATEWAY +fi + +# Environment variables +declare -a ENV_VARS=() + +case "$OCI_IMAGE" in +postgres* | postgresql*) + echo "" + msg_info "PostgreSQL requires environment variables" + read -r -p "PostgreSQL password: " -s PG_PASS + echo "" + ENV_VARS+=("POSTGRES_PASSWORD=$PG_PASS") + + read -r -p "Create database (optional): " PG_DB + [[ -n "$PG_DB" ]] && ENV_VARS+=("POSTGRES_DB=$PG_DB") + + read -r -p "PostgreSQL user (optional): " PG_USER + [[ -n "$PG_USER" ]] && ENV_VARS+=("POSTGRES_USER=$PG_USER") + ;; + +mariadb* | mysql*) + echo "" + msg_info "MariaDB/MySQL requires environment variables" + read -r -p "Root password: " -s MYSQL_PASS + echo "" + ENV_VARS+=("MYSQL_ROOT_PASSWORD=$MYSQL_PASS") + + read -r -p "Create database (optional): " MYSQL_DB + [[ -n "$MYSQL_DB" ]] && ENV_VARS+=("MYSQL_DATABASE=$MYSQL_DB") + + read -r -p "Create user (optional): " MYSQL_USER + if [[ -n "$MYSQL_USER" ]]; then + ENV_VARS+=("MYSQL_USER=$MYSQL_USER") + read -r -p "User password: " -s MYSQL_USER_PASS + echo "" + ENV_VARS+=("MYSQL_PASSWORD=$MYSQL_USER_PASS") + fi + ;; + +*linkwarden*) + echo "" + msg_info "Linkwarden configuration" + read -r -p "NEXTAUTH_SECRET (press Enter to generate): " NEXTAUTH_SECRET + if [[ -z "$NEXTAUTH_SECRET" ]]; then + NEXTAUTH_SECRET=$(openssl rand -base64 32) + fi + ENV_VARS+=("NEXTAUTH_SECRET=$NEXTAUTH_SECRET") + + read -r -p "NEXTAUTH_URL [http://localhost:3000]: " NEXTAUTH_URL + NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000} + ENV_VARS+=("NEXTAUTH_URL=$NEXTAUTH_URL") + + read -r -p "DATABASE_URL (PostgreSQL connection string): " DATABASE_URL + [[ -n "$DATABASE_URL" ]] && ENV_VARS+=("DATABASE_URL=$DATABASE_URL") + ;; +esac + +# Additional env vars +read -r -p "Add custom environment variables? (y/N): " ADD_ENV +if [[ "${ADD_ENV,,}" =~ ^(y|yes)$ ]]; then + while true; do + read -r -p "Enter KEY=VALUE (or press Enter to finish): " CUSTOM_ENV + [[ -z "$CUSTOM_ENV" ]] && break + ENV_VARS+=("$CUSTOM_ENV") + done +fi + +# Privileged mode +read -r -p "Run as privileged container? (y/N): " PRIV_MODE +if [[ "${PRIV_MODE,,}" =~ ^(y|yes)$ ]]; then + UNPRIVILEGED="0" +else + UNPRIVILEGED="1" +fi + +# Auto-start +read -r -p "Start container after creation? (Y/n): " AUTO_START +if [[ "${AUTO_START,,}" =~ ^(n|no)$ ]]; then + START_AFTER="no" +else + START_AFTER="yes" +fi + +# Summary +echo "" +echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" +echo -e "${BL}Container Configuration Summary:${CL}" +echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" +echo -e " Image: $FULL_IMAGE" +echo -e " ID: $VMID" +echo -e " Name: $CT_NAME" +echo -e " CPUs: $CORES" +echo -e " Memory: ${MEMORY}MB" +echo -e " Disk: ${DISK}GB" +echo -e " Storage: $STORAGE" +echo -e " Network: $BRIDGE ($IP_MODE)" +[[ ${#ENV_VARS[@]} -gt 0 ]] && echo -e " Env vars: ${#ENV_VARS[@]} configured" +echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" +echo "" + +read -r -p "Proceed with creation? (Y/n): " CONFIRM +if [[ "${CONFIRM,,}" =~ ^(n|no)$ ]]; then + msg_error "Cancelled by user" + exit 0 +fi + +# Create container +msg_info "Creating container $VMID" + +# Build pct create command +PCT_CMD="pct create $VMID" +PCT_CMD+=" --hostname $CT_NAME" +PCT_CMD+=" --cores $CORES" +PCT_CMD+=" --memory $MEMORY" +PCT_CMD+=" --rootfs ${STORAGE}:${DISK},oci=${FULL_IMAGE}" +PCT_CMD+=" --unprivileged $UNPRIVILEGED" + +if [[ "$IP_MODE" == "static" && -n "$STATIC_IP" ]]; then + PCT_CMD+=" --net0 name=eth0,bridge=$BRIDGE,ip=$STATIC_IP" + [[ -n "$GATEWAY" ]] && PCT_CMD+=",gw=$GATEWAY" +else + PCT_CMD+=" --net0 name=eth0,bridge=$BRIDGE,ip=dhcp" +fi + +if eval "$PCT_CMD" 2>&1; then + msg_ok "Container created" +else + msg_error "Failed to create container" + exit 1 +fi + +# Set environment variables +if [[ ${#ENV_VARS[@]} -gt 0 ]]; then + msg_info "Configuring environment variables" + for env_var in "${ENV_VARS[@]}"; do + if pct set "$VMID" -env "$env_var" &>/dev/null; then + : + else + msg_error "Failed to set: $env_var" + fi + done + msg_ok "Environment variables configured (${#ENV_VARS[@]} variables)" +fi + +# Start container +if [[ "$START_AFTER" == "yes" ]]; then + msg_info "Starting container" + if pct start "$VMID" 2>&1; then + msg_ok "Container started" + + # Wait for network + sleep 3 + CT_IP=$(pct exec "$VMID" -- hostname -I 2>/dev/null | awk '{print $1}' || echo "N/A") + + echo "" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e "${BL}Container Information:${CL}" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e " ID: ${GN}$VMID${CL}" + echo -e " Name: ${GN}$CT_NAME${CL}" + echo -e " Image: ${GN}$FULL_IMAGE${CL}" + echo -e " IP: ${GN}$CT_IP${CL}" + echo -e " Status: ${GN}Running${CL}" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo "" + echo -e "${INFO} ${YW}Access console:${CL} pct console $VMID" + echo -e "${INFO} ${YW}View logs:${CL} pct logs $VMID" + echo "" + else + msg_error "Failed to start container" + fi +else + echo "" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e "${BL}Container Information:${CL}" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e " ID: ${GN}$VMID${CL}" + echo -e " Name: ${GN}$CT_NAME${CL}" + echo -e " Image: ${GN}$FULL_IMAGE${CL}" + echo -e " Status: ${YW}Stopped${CL}" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo "" + echo -e "${INFO} ${YW}Start with:${CL} pct start $VMID" + echo "" +fi diff --git a/vm/unifi-os-server-vm.sh b/vm/unifi-os-server-vm.sh index 5e356349b..524896c81 100644 --- a/vm/unifi-os-server-vm.sh +++ b/vm/unifi-os-server-vm.sh @@ -588,96 +588,17 @@ echo -en "\e[1A\e[0K" FILE=$(basename $URL) msg_ok "Downloaded ${CL}${BL}${FILE}${CL}" -# --- Inject UniFi Installer via Cloud-Init --- -msg_info "Preparing ${OS_DISPLAY} Cloud Image for UniFi OS" +msg_ok "Downloaded ${OS_DISPLAY} Cloud Image" -# Install virt-customize if not available -if ! command -v virt-customize &>/dev/null; then +# Expand root partition to use full disk space +msg_info "Expanding disk image to ${DISK_SIZE}" + +# Install virt-resize if not available +if ! command -v virt-resize &>/dev/null; then apt-get -qq update >/dev/null apt-get -qq install libguestfs-tools -y >/dev/null fi -# Create UniFi OS installation script and inject it into the image -virt-customize -a "${FILE}" --run-command "cat > /root/install-unifi-os.sh << 'INSTALLSCRIPT' -#!/bin/bash -set -x -exec > /var/log/unifi-install.log 2>&1 - -echo \"=== UniFi OS Installation Started at \$(date) ===\" - -# Wait for cloud-init to complete -if command -v cloud-init >/dev/null 2>&1; then - echo \"Waiting for cloud-init to complete...\" - cloud-init status --wait 2>/dev/null || true -fi - -# Install required packages -echo \"Installing required packages...\" -export DEBIAN_FRONTEND=noninteractive -apt-get update -apt-get install -y curl wget ca-certificates podman uidmap slirp4netns iptables - -# Configure Podman -echo \"Configuring Podman...\" -loginctl enable-linger root - -# Download UniFi OS Server -echo \"Downloading UniFi OS Server ${UOS_VERSION}...\" -cd /root -curl -fsSL '${UOS_URL}' -o unifi-installer.bin -chmod +x unifi-installer.bin - -# Install UniFi OS Server -echo \"Installing UniFi OS Server (this takes 3-5 minutes)...\" -./unifi-installer.bin install - -echo \"Waiting for services to start...\" -sleep 15 - -# Start UniFi OS Server -if systemctl list-unit-files | grep -q unifi-os-server; then - echo \"Starting UniFi OS Server service...\" - systemctl enable unifi-os-server - systemctl start unifi-os-server - sleep 10 - - if systemctl is-active --quiet unifi-os-server; then - echo \"SUCCESS: UniFi OS Server is running\" - else - echo \"WARNING: Checking service status...\" - systemctl status unifi-os-server --no-pager - fi -fi - -touch /root/.unifi-installed -echo \"=== Installation completed at \$(date) ===\" -INSTALLSCRIPT" >/dev/null 2>&1 - -virt-customize -a "${FILE}" --chmod 0755:/root/install-unifi-os.sh >/dev/null 2>&1 - -# Create systemd service for first-boot installation -virt-customize -a "${FILE}" --run-command "cat > /etc/systemd/system/unifi-firstboot.service << 'SVCFILE' -[Unit] -Description=UniFi OS First Boot Installation -After=cloud-init.service network-online.target -Wants=network-online.target -ConditionPathExists=!/root/.unifi-installed - -[Service] -Type=oneshot -ExecStart=/root/install-unifi-os.sh -RemainAfterExit=yes - -[Install] -WantedBy=multi-user.target -SVCFILE" >/dev/null 2>&1 - -virt-customize -a "${FILE}" --run-command "systemctl enable unifi-firstboot.service" >/dev/null 2>&1 - -msg_ok "Prepared ${OS_DISPLAY} image with UniFi OS installer" - -# 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) @@ -712,21 +633,10 @@ qm set "$VMID" \ qm resize "$VMID" scsi0 "$DISK_SIZE" >/dev/null qm set "$VMID" --agent enabled=1 >/dev/null -# Add Cloud-Init drive (standard Cloud-Init, no custom user-data) +# Add Cloud-Init drive msg_info "Configuring Cloud-Init" setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" >/dev/null 2>&1 -msg_ok "Cloud-Init configured (UniFi OS installs via systemd service on first boot)" - -# Display credentials immediately so user can login -if [ -n "$CLOUDINIT_CRED_FILE" ] && [ -f "$CLOUDINIT_CRED_FILE" ]; then - echo "" - echo -e "${INFO}${BOLD}${GN}Cloud-Init Credentials (save these now!):${CL}" - echo -e "${TAB}${DGN}User: ${BGN}${CLOUDINIT_USER:-root}${CL}" - echo -e "${TAB}${DGN}Password: ${BGN}${CLOUDINIT_PASSWORD}${CL}" - echo -e "${TAB}${RD}⚠️ UniFi OS installation runs automatically on first boot${CL}" - echo -e "${TAB}${INFO}Monitor: ${BL}tail -f /var/log/unifi-install.log${CL}" - echo "" -fi +msg_ok "Cloud-Init configured" DESCRIPTION=$( cat </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}" + msg_ok "VM IP Address: ${VM_IP}" break fi sleep 2 done - if [ -n "$VM_IP" ]; then - msg_info "Waiting for UniFi OS installation (via Cloud-Init, takes 5-8 minutes)" - - WAIT_COUNT=0 - MAX_WAIT=600 # 10 minutes max for Cloud-Init installation - 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 is ready!" - break - fi - - sleep 10 - WAIT_COUNT=$((WAIT_COUNT + 10)) - - # Update message every 30 seconds - if [ $((WAIT_COUNT - LAST_MSG_TIME)) -ge 30 ]; then - echo -e "${BFR}${TAB}${YW}${HOLD}Installation in progress... ${WAIT_COUNT}s elapsed${CL}" - echo -e "${TAB}${INFO}${YW}Monitor: ${BL}ssh ${CLOUDINIT_USER:-root}@${VM_IP} 'tail -f /var/log/unifi-install.log'${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, UniFi OS installation in progress" - echo -e "${TAB}${INFO}${YW}Installation runs via systemd service on first boot${CL}" - echo -e "${TAB}${INFO}${YW}This takes 5-8 minutes${CL}" - if [ "$USE_CLOUD_INIT" = "yes" ]; then - echo -e "${TAB}${INFO}${YW}SSH: ${BL}ssh ${CLOUDINIT_USER:-root}@${VM_IP}${CL}" - echo -e "${TAB}${INFO}${YW}Password: ${BGN}${CLOUDINIT_PASSWORD}${CL}" - echo -e "${TAB}${INFO}${YW}Monitor: ${BL}tail -f /var/log/unifi-install.log${CL}" - fi - echo -e "${TAB}${INFO}${YW}UniFi OS 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}" + if [ -z "$VM_IP" ]; then + msg_error "Could not detect VM IP address" echo -e "${TAB}${INFO}${YW}Use Proxmox Console to login with Cloud-Init credentials${CL}" echo -e "${TAB}${INFO}${YW}User: ${BGN}${CLOUDINIT_USER:-root}${CL} / Password: ${BGN}${CLOUDINIT_PASSWORD}${CL}" - echo -e "${TAB}${INFO}${YW}Monitor installation: ${BL}tail -f /var/log/unifi-install.log${CL}" + exit 1 fi + + # Wait for SSH to be ready + msg_info "Waiting for SSH to be ready" + SSH_READY=0 + for i in {1..30}; do + if timeout 5 sshpass -p "${CLOUDINIT_PASSWORD}" ssh -o ConnectTimeout=3 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "${CLOUDINIT_USER:-root}@${VM_IP}" "echo 'SSH Ready'" >/dev/null 2>&1; then + SSH_READY=1 + msg_ok "SSH connection ready" + break + fi + sleep 2 + done + + if [ $SSH_READY -eq 0 ]; then + msg_error "SSH connection failed" + echo -e "${TAB}${INFO}${YW}Manual login - User: ${BGN}${CLOUDINIT_USER:-root}${CL} / Password: ${BGN}${CLOUDINIT_PASSWORD}${CL}" + exit 1 + fi + + # Check if sshpass is installed + if ! command -v sshpass &>/dev/null; then + msg_info "Installing sshpass for automated SSH" + apt-get update -qq >/dev/null 2>&1 + apt-get install -y sshpass -qq >/dev/null 2>&1 + fi + + # Execute UniFi OS installation directly via SSH + msg_info "Installing UniFi OS Server ${UOS_VERSION} (takes 4-6 minutes)" + + # Create installation script + INSTALL_SCRIPT=$( + cat <<'EOFINSTALL' +#!/bin/bash +set -e +export DEBIAN_FRONTEND=noninteractive + +echo "[1/5] Updating system packages..." +apt-get update -qq +apt-get install -y curl wget ca-certificates podman uidmap slirp4netns iptables -qq + +echo "[2/5] Configuring Podman..." +loginctl enable-linger root + +echo "[3/5] Downloading UniFi OS Server installer..." +cd /root +curl -fsSL "UNIFI_URL" -o unifi-installer.bin +chmod +x unifi-installer.bin + +echo "[4/5] Installing UniFi OS Server (this takes 3-5 minutes)..." +./unifi-installer.bin install + +echo "[5/5] Starting UniFi OS Server..." +sleep 15 + +if systemctl list-unit-files | grep -q unifi-os-server; then + systemctl enable unifi-os-server + systemctl start unifi-os-server + sleep 10 + + if systemctl is-active --quiet unifi-os-server; then + echo "✓ UniFi OS Server is running" + else + echo "⚠ Service status:" + systemctl status unifi-os-server --no-pager || true + fi +fi + +echo "Installation completed!" +EOFINSTALL + ) + + # Replace URL placeholder + INSTALL_SCRIPT="${INSTALL_SCRIPT//UNIFI_URL/$UOS_URL}" + + # Execute installation via SSH (with output streaming) + if sshpass -p "${CLOUDINIT_PASSWORD}" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "${CLOUDINIT_USER:-root}@${VM_IP}" "bash -s" <<<"$INSTALL_SCRIPT" 2>&1 | while IFS= read -r line; do + echo -e "${TAB}${DGN}${line}${CL}" + done; then + msg_ok "UniFi OS Server installed successfully" + else + msg_error "Installation failed" + echo -e "${TAB}${INFO}${YW}Check logs: ${BL}ssh ${CLOUDINIT_USER:-root}@${VM_IP}${CL}" + exit 1 + fi + + # Wait for UniFi OS web interface + msg_info "Waiting for UniFi OS web interface (port 11443)" + PORT_OPEN=0 + for i in {1..60}; do + if timeout 2 bash -c ">/dev/tcp/${VM_IP}/11443" 2>/dev/null; then + PORT_OPEN=1 + msg_ok "UniFi OS Server web interface is ready" + break + fi + sleep 2 + done + + echo "" + if [ $PORT_OPEN -eq 1 ]; then + echo -e "${TAB}${GATEWAY}${BOLD}${GN}✓ UniFi OS Server is ready!${CL}" + echo -e "${TAB}${GATEWAY}${BOLD}${GN}✓ Access at: ${BGN}https://${VM_IP}:11443${CL}" + else + echo -e "${TAB}${INFO}${YW}UniFi OS is installed but web interface not yet available${CL}" + echo -e "${TAB}${INFO}${YW}Access at: ${BGN}https://${VM_IP}:11443${CL} ${YW}(may take 1-2 more minutes)${CL}" + fi + + echo -e "${TAB}${INFO}${DGN}SSH Access: ${BL}ssh ${CLOUDINIT_USER:-root}@${VM_IP}${CL}" + echo -e "${TAB}${INFO}${DGN}Password: ${BGN}${CLOUDINIT_PASSWORD}${CL}" + echo "" fi post_update_to_api "done" "none"