diff --git a/ct/headers/asterisk b/ct/headers/asterisk deleted file mode 100644 index ed4356862..000000000 --- a/ct/headers/asterisk +++ /dev/null @@ -1,6 +0,0 @@ - ___ __ _ __ - / | _____/ /____ _____(_)____/ /__ - / /| | / ___/ __/ _ \/ ___/ / ___/ //_/ - / ___ |(__ ) /_/ __/ / / (__ ) ,< -/_/ |_/____/\__/\___/_/ /_/____/_/|_| - diff --git a/ct/headers/domain-locker b/ct/headers/domain-locker deleted file mode 100644 index 0ab1a9b74..000000000 --- a/ct/headers/domain-locker +++ /dev/null @@ -1,6 +0,0 @@ - ____ _ __ __ - / __ \____ ____ ___ ____ _(_)___ / / ____ _____/ /_____ _____ - / / / / __ \/ __ `__ \/ __ `/ / __ \______/ / / __ \/ ___/ //_/ _ \/ ___/ - / /_/ / /_/ / / / / / / /_/ / / / / /_____/ /___/ /_/ / /__/ ,< / __/ / -/_____/\____/_/ /_/ /_/\__,_/_/_/ /_/ /_____/\____/\___/_/|_|\___/_/ - diff --git a/ct/headers/joplin-server b/ct/headers/joplin-server new file mode 100644 index 000000000..68b32c8fa --- /dev/null +++ b/ct/headers/joplin-server @@ -0,0 +1,6 @@ + __ ___ _____ + / /___ ____ / (_)___ / ___/___ ______ _____ _____ + __ / / __ \/ __ \/ / / __ \______\__ \/ _ \/ ___/ | / / _ \/ ___/ +/ /_/ / /_/ / /_/ / / / / / /_____/__/ / __/ / | |/ / __/ / +\____/\____/ .___/_/_/_/ /_/ /____/\___/_/ |___/\___/_/ + /_/ diff --git a/ct/headers/kanba b/ct/headers/kanba deleted file mode 100644 index 40c351f8b..000000000 --- a/ct/headers/kanba +++ /dev/null @@ -1,6 +0,0 @@ - __ __ __ - / //_/___ _____ / /_ ____ _ - / ,< / __ `/ __ \/ __ \/ __ `/ - / /| / /_/ / / / / /_/ / /_/ / -/_/ |_\__,_/_/ /_/_.___/\__,_/ - diff --git a/ct/headers/librenms b/ct/headers/librenms deleted file mode 100644 index cf3dd4e4f..000000000 --- a/ct/headers/librenms +++ /dev/null @@ -1,6 +0,0 @@ - __ _ __ - / / (_) /_ ________ ____ ____ ___ _____ - / / / / __ \/ ___/ _ \/ __ \/ __ `__ \/ ___/ - / /___/ / /_/ / / / __/ / / / / / / / (__ ) -/_____/_/_.___/_/ \___/_/ /_/_/ /_/ /_/____/ - diff --git a/ct/headers/mealie b/ct/headers/mealie new file mode 100644 index 000000000..a5d36d54c --- /dev/null +++ b/ct/headers/mealie @@ -0,0 +1,6 @@ + __ ___ ___ + / |/ /__ ____ _/ (_)__ + / /|_/ / _ \/ __ `/ / / _ \ + / / / / __/ /_/ / / / __/ +/_/ /_/\___/\__,_/_/_/\___/ + diff --git a/ct/headers/omada b/ct/headers/omada deleted file mode 100644 index 3629b79d1..000000000 --- a/ct/headers/omada +++ /dev/null @@ -1,6 +0,0 @@ - ____ __ - / __ \____ ___ ____ _____/ /___ _ - / / / / __ `__ \/ __ `/ __ / __ `/ -/ /_/ / / / / / / /_/ / /_/ / /_/ / -\____/_/ /_/ /_/\__,_/\__,_/\__,_/ - diff --git a/ct/headers/passbolt b/ct/headers/passbolt deleted file mode 100644 index 91f0ab71d..000000000 --- a/ct/headers/passbolt +++ /dev/null @@ -1,6 +0,0 @@ - ____ __ ____ - / __ \____ ___________/ /_ ____ / / /_ - / /_/ / __ `/ ___/ ___/ __ \/ __ \/ / __/ - / ____/ /_/ (__ |__ ) /_/ / /_/ / / /_ -/_/ \__,_/____/____/_.___/\____/_/\__/ - diff --git a/ct/headers/qdrant b/ct/headers/qdrant new file mode 100644 index 000000000..f6a89dbc3 --- /dev/null +++ b/ct/headers/qdrant @@ -0,0 +1,6 @@ + ____ __ __ + / __ \____/ /________ _____ / /_ + / / / / __ / ___/ __ `/ __ \/ __/ +/ /_/ / /_/ / / / /_/ / / / / /_ +\___\_\__,_/_/ \__,_/_/ /_/\__/ + diff --git a/ct/headers/upgopher b/ct/headers/upgopher deleted file mode 100644 index e1126d09c..000000000 --- a/ct/headers/upgopher +++ /dev/null @@ -1,6 +0,0 @@ - __ __ __ - / / / /___ ____ _____ ____ / /_ ___ _____ - / / / / __ \/ __ `/ __ \/ __ \/ __ \/ _ \/ ___/ -/ /_/ / /_/ / /_/ / /_/ / /_/ / / / / __/ / -\____/ .___/\__, /\____/ .___/_/ /_/\___/_/ - /_/ /____/ /_/ diff --git a/ct/joplin-server.sh b/ct/joplin-server.sh new file mode 100644 index 000000000..d7824f8ba --- /dev/null +++ b/ct/joplin-server.sh @@ -0,0 +1,61 @@ +#!/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://joplinapp.org/ + +APP="Joplin-Server" +var_tags="${var_tags:-notes}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-4096}" +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 [[ ! -d /opt/joplin-server ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "joplin-server" "laurent22/joplin"; then + msg_info "Stopping Services" + systemctl stop joplin-server + msg_ok "Stopped Services" + + fetch_and_deploy_gh_release "joplin-server" "laurent22/joplin" "tarball" "latest" + + msg_info "Updating Joplin-Server" + cd /opt/joplin-server + sed -i "/onenote-converter/d" packages/lib/package.json + $STD yarn config set --home enableTelemetry 0 + export BUILD_SEQUENCIAL=1 + $STD yarn install --inline-builds + msg_ok "Updated Joplin-Server" + + msg_info "Starting Services" + systemctl start joplin-server + 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}:22300${CL}" diff --git a/ct/qdrant.sh b/ct/qdrant.sh new file mode 100644 index 000000000..dc8852c7b --- /dev/null +++ b/ct/qdrant.sh @@ -0,0 +1,45 @@ +#!/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/qdrant/qdrant + +APP="Qdrant" +var_tags="${var_tags:-}" +var_cpu="${var_cpu:-4}" +var_ram="${var_ram:-8192}" +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 [[ ! -d /var/lib/qdrant ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + if check_for_gh_release "qdrant" "qdrant/qdrant"; then + fetch_and_deploy_gh_release "qdrant" "qdrant/qdrant" "binary" "latest" "/usr/bin/qdrant" + chown -R root:root /var/lib/qdrant + chmod -R 755 /var/lib/qdrant + 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}:6333${CL}" diff --git a/frontend/public/json/ente.json b/frontend/public/json/ente.json new file mode 100644 index 000000000..794580914 --- /dev/null +++ b/frontend/public/json/ente.json @@ -0,0 +1,48 @@ +{ + "name": "Ente", + "slug": "ente", + "categories": [ + 20 + ], + "date_created": "2025-11-22", + "type": "ct", + "updateable": false, + "privileged": false, + "config_path": "/opt", + "interface_port": 3000, + "documentation": "https://github.com/ente-io/ente", + "website": "https://ente.io/", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ente-photos.svg", + "description": "Ente is a service that provides a fully open source, end-to-end encrypted platform for you to store your data in the cloud without needing to trust the service provider. On top of this platform, we have built two apps so far: Ente Photos (an alternative to Apple and Google Photos) and Ente Auth (a 2FA alternative to the deprecated Authy).", + "install_methods": [ + { + "type": "default", + "script": "ct/ente.sh", + "resources": { + "cpu": 4, + "ram": 4096, + "hdd": 10, + "os": "debian", + "version": "12" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Please use `journalctl -u ente-museum.service -n 10` to read logs for the signup verification code", + "type": "info" + }, + { + "text": "If you want to use the Ente CLI to add/whitelist admins, please follow the instructions at https://ente.io/help/self-hosting/administration/cli", + "type": "info" + }, + { + "text": "To see Museium config: `cat /opt/ente/server/museum.yaml`", + "type": "info" + } + ] +} diff --git a/frontend/public/json/qdrant.json b/frontend/public/json/qdrant.json new file mode 100644 index 000000000..01e5d558c --- /dev/null +++ b/frontend/public/json/qdrant.json @@ -0,0 +1,35 @@ +{ + "name": "Qdrant", + "slug": "qdrant", + "categories": [ + 20 + ], + "date_created": "2025-11-25", + "type": "ct", + "updateable": false, + "privileged": false, + "config_path": "/etc/qdrant/config.yaml", + "interface_port": 6333, + "documentation": "https://github.com/qdrant/qdrant", + "website": "https://qdrant.tech/", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/qdrant.webp", + "description": "Qdrant is a vector search engine and vector database that allows you to store, search, and manage high-dimensional vectors efficiently.", + "install_methods": [ + { + "type": "default", + "script": "ct/qdrant.sh", + "resources": { + "cpu": 1, + "ram": 1024, + "hdd": 5, + "os": "debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [] +} diff --git a/install/ente-install.sh b/install/ente-install.sh index c0fe3697f..9a4fbdee6 100644 --- a/install/ente-install.sh +++ b/install/ente-install.sh @@ -28,8 +28,22 @@ PG_VERSION="17" setup_postgresql setup_go NODE_VERSION="24" NODE_MODULE="yarn" setup_nodejs ENTE_CLI_VERSION=$(curl -s https://api.github.com/repos/ente-io/ente/releases | jq -r '[.[] | select(.tag_name | startswith("cli-v"))][0].tag_name') -fetch_and_deploy_gh_release "ente" "ente-io/ente" "tarball" "latest" "/opt/ente" -fetch_and_deploy_gh_release "ente" "ente-io/ente" "tarball" "$ENTE_CLI_VERSION" "/usr/local/bin/ente" "ente-cli-$ENTE_CLI_VERSION-linux-amd64.tar.gz" +fetch_and_deploy_gh_release "ente-server" "ente-io/ente" "tarball" "latest" "/opt/ente" +fetch_and_deploy_gh_release "ente-cli" "ente-io/ente" "prebuild" "$ENTE_CLI_VERSION" "/usr/local/bin" "ente-$ENTE_CLI_VERSION-linux-amd64.tar.gz" + +$STD mkdir -p /opt/ente/cli +msg_info "Configuring Ente CLI" +cat <>~/.bashrc +export ENTE_CLI_SECRETS_PATH=/opt/ente/cli/secrets.txt +export PATH="/usr/local/bin:$PATH" +EOF +$STD source ~/.bashrc +$STD mkdir -p ~/.ente +cat <~/.ente/config.yaml +endpoint: + api: http://localhost:8080 +EOF +msg_ok "Configured Ente CLI" msg_info "Setting up PostgreSQL" DB_NAME="ente_db" @@ -133,13 +147,37 @@ jwt: EOF msg_ok "Created museum.yaml" +read -r -p "Enter the public URL for Ente backend (e.g., https://api.ente.yourdomain.com or http://192.168.1.100:8080) leave empty to use container IP: " backend_url +if [[ -z "$backend_url" ]]; then + LOCAL_IP=$(hostname -I | awk '{print $1}') + ENTE_BACKEND_URL="http://$LOCAL_IP:8080" + msg_info "No URL provided" + msg_ok "using local IP: $ENTE_BACKEND_URL\n" +else + ENTE_BACKEND_URL="$backend_url" + msg_info "URL provided" + msg_ok "Using provided URL: $ENTE_BACKEND_URL\n" +fi + +read -r -p "Enter the public URL for Ente albums (e.g., https://albums.ente.yourdomain.com or http://192.168.1.100:3002) leave empty to use container IP: " albums_url +if [[ -z "$albums_url" ]]; then + LOCAL_IP=$(hostname -I | awk '{print $1}') + ENTE_ALBUMS_URL="http://$LOCAL_IP:3002" + msg_info "No URL provided" + msg_ok "using local IP: $ENTE_ALBUMS_URL\n" +else + ENTE_ALBUMS_URL="$albums_url" + msg_info "URL provided" + msg_ok "Using provided URL: $ENTE_ALBUMS_URL\n" +fi + +export NEXT_PUBLIC_ENTE_ENDPOINT=$ENTE_BACKEND_URL +export NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=$ENTE_ALBUMS_URL + msg_info "Building Web Applications" -# Get container IP address -CONTAINER_IP=$(hostname -I | awk '{print $1}') cd /opt/ente/web +export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 $STD yarn install -export NEXT_PUBLIC_ENTE_ENDPOINT=http://${CONTAINER_IP}:8080 -export NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://${CONTAINER_IP}:3002 $STD yarn build $STD yarn build:accounts $STD yarn build:auth @@ -153,12 +191,35 @@ cp -r apps/cast/out /var/www/ente/apps/cast # Save build configuration for future rebuilds cat </opt/ente/rebuild-frontend.sh #!/usr/bin/env bash -# Rebuild Ente frontend with current IP -CONTAINER_IP=\$(hostname -I | awk '{print \$1}') -echo "Building frontend with IP: \$CONTAINER_IP" +# Rebuild Ente frontend +# Prompt for backend URL +read -r -p "Enter the public URL for Ente backend (e.g., https://api.ente.yourdomain.com or http://192.168.1.100:8080) leave empty to use container IP: " backend_url +if [[ -z "\$backend_url" ]]; then + LOCAL_IP=$(hostname -I | awk '{print $1}') + ENTE_BACKEND_URL="http://\$LOCAL_IP:8080" + echo "No URL provided, using local IP: \$ENTE_BACKEND_URL\n" +else + ENTE_BACKEND_URL="\$backend_url" + echo "Using provided URL: \$ENTE_BACKEND_URL\n" +fi + +# Prompt for albums URL +read -r -p "Enter the public URL for Ente albums (e.g., https://albums.ente.yourdomain.com or http://192.168.1.100:3002) leave empty to use container IP: " albums_url +if [[ -z "\$albums_url" ]]; then + LOCAL_IP=\$(hostname -I | awk '{print $1}') + ENTE_ALBUMS_URL="http://\$LOCAL_IP:3002" + echo "No URL provided, using local IP: \$ENTE_ALBUMS_URL\n" +else + ENTE_ALBUMS_URL="\$albums_url" + echo "Using provided URL: \$ENTE_ALBUMS_URL\n" +fi + +export NEXT_PUBLIC_ENTE_ENDPOINT=\$ENTE_BACKEND_URL +export NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=\$ENTE_ALBUMS_URL + +echo "Building Web Applications\n" + cd /opt/ente/web -export NEXT_PUBLIC_ENTE_ENDPOINT=http://\${CONTAINER_IP}:8080 -export NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://\${CONTAINER_IP}:3002 yarn build yarn build:accounts yarn build:auth @@ -312,8 +373,9 @@ echo -e "━━━━━━━━━━━━━━━━━━━━━━━ echo -e "\n${BL}Access URLs:${CL}" echo -e " Photos: http://${CONTAINER_IP}:3000" echo -e " Accounts: http://${CONTAINER_IP}:3001" +echo -e " Albums: ${ENTE_ALBUMS_URL}" echo -e " Auth: http://${CONTAINER_IP}:3003" -echo -e " API: http://${CONTAINER_IP}:8080" +echo -e " API: ${ENTE_BACKEND_URL}" echo -e "\n${YW}⚠️ Important Post-Installation Steps:${CL}" echo -e "\n${BL}1. Create your first account:${CL}" echo -e " • Open http://${CONTAINER_IP}:3000 in your browser" diff --git a/install/joplin-server-install.sh b/install/joplin-server-install.sh new file mode 100644 index 000000000..35d85fbf2 --- /dev/null +++ b/install/joplin-server-install.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2025 community-scripts ORG +# Author: Slaviša Arežina (tremor021) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://joplinapp.org/ + +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 \ + rsync +msg_ok "Installed Dependencies" + +PG_VERSION="17" setup_postgresql +NODE_VERSION=24 NODE_MODULE="yarn,npm,pm2" setup_nodejs +mkdir -p /opt/pm2 +export PM2_HOME=/opt/pm2 +$STD pm2 install pm2-logrotate +$STD pm2 set pm2-logrotate:max_size 100MB +$STD pm2 set pm2-logrotate:retain 5 +$STD pm2 set pm2-logrotate:compress tr + +msg_info "Setting up PostgreSQL Database" +DB_NAME=joplin +DB_USER=joplin +DB_PASS="$(openssl rand -base64 18 | cut -c1-13)" +$STD sudo -u postgres psql -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PASS';" +$STD sudo -u postgres psql -c "CREATE DATABASE $DB_NAME WITH OWNER $DB_USER ENCODING 'UTF8' TEMPLATE template0;" +$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET client_encoding TO 'utf8';" +$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET default_transaction_isolation TO 'read committed';" +$STD sudo -u postgres psql -c "ALTER ROLE $DB_USER SET timezone TO 'UTC'" +{ + echo "Joplin-Credentials" + echo "Joplin Database User: $DB_USER" + echo "Joplin Database Password: $DB_PASS" + echo "Joplin Database Name: $DB_NAME" +} >>~/joplin.creds +msg_ok "Set up PostgreSQL Database" + +fetch_and_deploy_gh_release "joplin-server" "laurent22/joplin" "tarball" "latest" + +msg_info "Setting up Joplin Server (Patience)" +LOCAL_IP=$(hostname -I | awk '{print $1}') +cd /opt/joplin-server +sed -i "/onenote-converter/d" packages/lib/package.json +$STD yarn config set --home enableTelemetry 0 +export BUILD_SEQUENCIAL=1 +$STD yarn install --inline-builds + +cat </opt/joplin-server/.env +PM2_HOME=/opt/pm2 +NODE_ENV=production +APP_BASE_URL=http://$LOCAL_IP:22300 +APP_PORT=22300 +DB_CLIENT=pg +POSTGRES_PASSWORD=$DB_PASS +POSTGRES_DATABASE=$DB_NAME +POSTGRES_USER=$DB_USER +POSTGRES_PORT=5432 +POSTGRES_HOST=localhost +EOF +msg_ok "Setup Joplin Server" + +msg_info "Setting up Service" +cat </etc/systemd/system/joplin-server.service +[Unit] +Description=Joplin Server Service +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/joplin-server/packages/server +EnvironmentFile=/opt/joplin-server/.env +ExecStart=/usr/bin/yarn start-prod +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now joplin-server +msg_ok "Service Setup" + +motd_ssh +customize +cleanup_lxc diff --git a/install/qdrant-install.sh b/install/qdrant-install.sh new file mode 100644 index 000000000..4d6ed15f1 --- /dev/null +++ b/install/qdrant-install.sh @@ -0,0 +1,64 @@ +#!/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/qdrant/qdrant + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +fetch_and_deploy_gh_release "qdrant" "qdrant/qdrant" "binary" "latest" "/usr/bin/qdrant" + +msg_info "Creating Qdrant Configuration" +mkdir -p /var/lib/qdrant/storage +mkdir -p /var/lib/qdrant/snapshots +mkdir -p /etc/qdrant +chown -R root:root /var/lib/qdrant +chmod -R 755 /var/lib/qdrant + +cat >/etc/qdrant/config.yaml </etc/systemd/system/qdrant.service </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 +} + +# ----------------------------------------------------------------------------- +# Community-Scripts bootstrap loader +# - Always sources build.func from remote +# - Updates local core files only if build.func changed +# - Local cache: /usr/local/community-scripts/core +# ----------------------------------------------------------------------------- + +# FUNC_DIR="/usr/local/community-scripts/core" +# mkdir -p "$FUNC_DIR" + +# BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func" +# BUILD_REV="$FUNC_DIR/build.rev" +# DEVMODE="${DEVMODE:-no}" + +# # --- Step 1: fetch build.func content once, compute hash --- +# build_content="$(curl -fsSL "$BUILD_URL")" || { +# echo "❌ Failed to fetch build.func" +# exit 1 +# } + +# newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}') +# oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "") + +# # --- Step 2: if build.func changed, offer update for core files --- +# if [ "$newhash" != "$oldhash" ]; then +# echo "⚠️ build.func changed!" + +# while true; do +# read -rp "Refresh local core files? [y/N/diff]: " ans +# case "$ans" in +# [Yy]*) +# echo "$newhash" >"$BUILD_REV" + +# update_func_file() { +# local file="$1" +# local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file" +# local local_path="$FUNC_DIR/$file" + +# echo "⬇️ Downloading $file ..." +# curl -fsSL "$url" -o "$local_path" || { +# echo "❌ Failed to fetch $file" +# exit 1 +# } +# echo "✔️ Updated $file" +# } + +# update_func_file core.func +# update_func_file error_handler.func +# update_func_file tools.func +# break +# ;; +# [Dd]*) +# for file in core.func error_handler.func tools.func; do +# local_path="$FUNC_DIR/$file" +# url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file" +# remote_tmp="$(mktemp)" + +# curl -fsSL "$url" -o "$remote_tmp" || continue + +# if [ -f "$local_path" ]; then +# echo "🔍 Diff for $file:" +# diff -u "$local_path" "$remote_tmp" || echo "(no differences)" +# else +# echo "📦 New file $file will be installed" +# fi + +# rm -f "$remote_tmp" +# done +# ;; +# *) +# echo "❌ Skipped updating local core files" +# break +# ;; +# esac +# done +# else +# if [ "$DEVMODE" != "yes" ]; then +# echo "✔️ build.func unchanged → using existing local core files" +# fi +# fi + +# if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then +# return 0 2>/dev/null || exit 0 +# fi +# _COMMUNITY_SCRIPTS_LOADER=1 + +# # --- Step 3: always source local versions of the core files --- +# source "$FUNC_DIR/core.func" +# source "$FUNC_DIR/error_handler.func" +# source "$FUNC_DIR/tools.func" + +# # --- Step 4: finally, source build.func directly from memory --- +# # (no tmp file needed) +# source <(printf "%s" "$build_content") + +# ------------------------------------------------------------------------------ +# Load core + error handler functions from community-scripts repo +# +# - Prefer curl if available, fallback to wget +# - Load: core.func, error_handler.func, api.func +# - Initialize error traps after loading +# ------------------------------------------------------------------------------ + +source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func) + +if command -v curl >/dev/null 2>&1; then + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func) + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) + load_functions + catch_errors + #echo "(build.func) Loaded core.func via curl" +elif command -v wget >/dev/null 2>&1; then + source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func) + source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) + load_functions + catch_errors + #echo "(build.func) Loaded core.func via wget" +fi + +# ============================================================================== +# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# maxkeys_check() +# +# - Reads kernel keyring limits (maxkeys, maxbytes) +# - Checks current usage for LXC user (UID 100000) +# - Warns if usage is close to limits and suggests sysctl tuning +# - Exits if thresholds are exceeded +# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html +# ------------------------------------------------------------------------------ + +maxkeys_check() { + # Read kernel parameters + per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) + per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0) + + # Exit if kernel parameters are unavailable + if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then + echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}" + exit 1 + fi + + # Fetch key usage for user ID 100000 (typical for containers) + used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0) + used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0) + + # Calculate thresholds and suggested new limits + threshold_keys=$((per_user_maxkeys - 100)) + threshold_bytes=$((per_user_maxbytes - 1000)) + new_limit_keys=$((per_user_maxkeys * 2)) + new_limit_bytes=$((per_user_maxbytes * 2)) + + # Check if key or byte usage is near limits + failure=0 + if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then + echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}" + echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." + failure=1 + fi + if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then + echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}" + echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." + failure=1 + fi + + # Provide next steps if issues are detected + if [[ "$failure" -eq 1 ]]; then + echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}" + exit 1 + fi + + # Silent success - only show errors if they exist +} + +# ============================================================================== +# SECTION 3: CONTAINER SETUP UTILITIES +# ============================================================================== + +# ------------------------------------------------------------------------------ +# get_current_ip() +# +# - Returns current container IP depending on OS type +# - Debian/Ubuntu: uses `hostname -I` +# - Alpine: parses eth0 via `ip -4 addr` +# - Returns "Unknown" if OS type cannot be determined +# ------------------------------------------------------------------------------ +get_current_ip() { + if [ -f /etc/os-release ]; then + # Check for Debian/Ubuntu (uses hostname -I) + if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then + CURRENT_IP=$(hostname -I | awk '{print $1}') + # Check for Alpine (uses ip command) + elif grep -q 'ID=alpine' /etc/os-release; then + CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) + else + CURRENT_IP="Unknown" + fi + fi + echo "$CURRENT_IP" +} + +# ------------------------------------------------------------------------------ +# update_motd_ip() +# +# - Updates /etc/motd with current container IP +# - Removes old IP entries to avoid duplicates +# ------------------------------------------------------------------------------ +update_motd_ip() { + MOTD_FILE="/etc/motd" + + if [ -f "$MOTD_FILE" ]; then + # Remove existing IP Address lines to prevent duplication + sed -i '/IP Address:/d' "$MOTD_FILE" + + IP=$(get_current_ip) + # Add the new IP address + echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE" + fi +} + +# ------------------------------------------------------------------------------ +# install_ssh_keys_into_ct() +# +# - Installs SSH keys into container root account if SSH is enabled +# - Uses pct push or direct input to authorized_keys +# - Falls back to warning if no keys provided +# ------------------------------------------------------------------------------ +install_ssh_keys_into_ct() { + [[ "$SSH" != "yes" ]] && return 0 + + if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then + msg_info "Installing selected SSH keys into CT ${CTID}" + pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || { + msg_error "prepare /root/.ssh failed" + return 1 + } + pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 || + pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || { + msg_error "write authorized_keys failed" + return 1 + } + pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true + msg_ok "Installed SSH keys into CT ${CTID}" + return 0 + fi + + # Fallback: nichts ausgewählt + msg_warn "No SSH keys to install (skipping)." + return 0 +} + +# ------------------------------------------------------------------------------ +# find_host_ssh_keys() +# +# - Scans system for available SSH keys +# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys) +# - Returns list of files containing valid SSH public keys +# - Sets FOUND_HOST_KEY_COUNT to number of keys found +# ------------------------------------------------------------------------------ +find_host_ssh_keys() { + local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))' + local -a files=() cand=() + local g="${var_ssh_import_glob:-}" + local total=0 f base c + + shopt -s nullglob + if [[ -n "$g" ]]; then + for pat in $g; do cand+=($pat); done + else + cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2) + cand+=(/root/.ssh/*.pub) + cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*) + fi + shopt -u nullglob + + for f in "${cand[@]}"; do + [[ -f "$f" && -r "$f" ]] || continue + base="$(basename -- "$f")" + case "$base" in + known_hosts | known_hosts.* | config) continue ;; + id_*) [[ "$f" != *.pub ]] && continue ;; + esac + + # CRLF safe check for host keys + c=$(tr -d '\r' <"$f" | awk ' + /^[[:space:]]*#/ {next} + /^[[:space:]]*$/ {next} + {print} + ' | grep -E -c '"$re"' || true) + + if ((c > 0)); then + files+=("$f") + total=$((total + c)) + fi + done + + # Fallback to /root/.ssh/authorized_keys + if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then + if grep -E -q "$re" /root/.ssh/authorized_keys; then + files+=(/root/.ssh/authorized_keys) + total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0))) + fi + fi + + FOUND_HOST_KEY_COUNT="$total" + ( + IFS=: + echo "${files[*]}" + ) +} + +# ============================================================================== +# SECTION 4: STORAGE & RESOURCE MANAGEMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# _write_storage_to_vars() +# +# - Writes storage selection to vars file +# - Removes old entries (commented and uncommented) to avoid duplicates +# - Arguments: vars_file, key (var_container_storage/var_template_storage), value +# ------------------------------------------------------------------------------ +_write_storage_to_vars() { + # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value + local vf="$1" key="$2" val="$3" + # remove uncommented and commented versions to avoid duplicates + sed -i "/^[#[:space:]]*${key}=/d" "$vf" + echo "${key}=${val}" >>"$vf" +} + +choose_and_set_storage_for_file() { + # $1 = vars_file, $2 = class ('container'|'template') + local vf="$1" class="$2" key="" current="" + case "$class" in + container) key="var_container_storage" ;; + template) key="var_template_storage" ;; + *) + msg_error "Unknown storage class: $class" + return 1 + ;; + esac + + current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf") + + # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4). + local content="rootdir" + [[ "$class" == "template" ]] && content="vztmpl" + local count + count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l) + + if [[ "$count" -eq 1 ]]; then + STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}') + STORAGE_INFO="" + else + # If the current value is preselectable, we could show it, but per your requirement we always offer selection + select_storage "$class" || return 1 + fi + + _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT" + + # Keep environment in sync for later steps (e.g. app-default save) + if [[ "$class" == "container" ]]; then + export var_container_storage="$STORAGE_RESULT" + export CONTAINER_STORAGE="$STORAGE_RESULT" + else + export var_template_storage="$STORAGE_RESULT" + export TEMPLATE_STORAGE="$STORAGE_RESULT" + fi + + # Silent operation - no output message +} + +# ============================================================================== +# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# base_settings() +# +# - Defines all base/default variables for container creation +# - Reads from environment variables (var_*) +# - Provides fallback defaults for OS type/version +# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK) +# - Sets up container type, resources, network, SSH, features, and tags +# ------------------------------------------------------------------------------ +base_settings() { + # Default Settings + CT_TYPE=${var_unprivileged:-"1"} + + # Resource allocation: App defaults take precedence if HIGHER + # Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values + local final_disk="${var_disk:-4}" + local final_cpu="${var_cpu:-1}" + local final_ram="${var_ram:-1024}" + + # If app declared higher values, use those instead + if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then + if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then + final_disk="${APP_DEFAULT_DISK}" + fi + fi + + if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then + if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then + final_cpu="${APP_DEFAULT_CPU}" + fi + fi + + if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then + if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then + final_ram="${APP_DEFAULT_RAM}" + fi + fi + + DISK_SIZE="${final_disk}" + CORE_COUNT="${final_cpu}" + RAM_SIZE="${final_ram}" + VERBOSE=${var_verbose:-"${1:-no}"} + PW=${var_pw:-""} + CT_ID=${var_ctid:-$NEXTID} + HN=${var_hostname:-$NSAPP} + BRG=${var_brg:-"vmbr0"} + NET=${var_net:-"dhcp"} + IPV6_METHOD=${var_ipv6_method:-"none"} + IPV6_STATIC=${var_ipv6_static:-""} + GATE=${var_gateway:-""} + APT_CACHER=${var_apt_cacher:-""} + APT_CACHER_IP=${var_apt_cacher_ip:-""} + + # Runtime check: Verify APT cacher is reachable if configured + if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then + if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then + msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142" + msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation" + APT_CACHER="" + APT_CACHER_IP="" + else + msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142" + fi + fi + + MTU=${var_mtu:-""} + SD=${var_storage:-""} + NS=${var_ns:-""} + MAC=${var_mac:-""} + VLAN=${var_vlan:-""} + SSH=${var_ssh:-"no"} + SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""} + UDHCPC_FIX=${var_udhcpc_fix:-""} + TAGS="community-script,${var_tags:-}" + ENABLE_FUSE=${var_fuse:-"${1:-no}"} + ENABLE_TUN=${var_tun:-"${1:-no}"} + + # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts + if [ -z "$var_os" ]; then + var_os="debian" + fi + if [ -z "$var_version" ]; then + var_version="12" + fi +} + +# ------------------------------------------------------------------------------ +# default_var_settings +# +# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing) +# - Loads var_* values from default.vars (safe parser, no source/eval) +# - Precedence: ENV var_* > default.vars > built-in defaults +# - Maps var_verbose → VERBOSE +# - Calls base_settings "$VERBOSE" and echo_default +# ------------------------------------------------------------------------------ +default_var_settings() { + # Allowed var_* keys (alphabetically sorted) + # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) + local VAR_WHITELIST=( + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse + var_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 + ) + + # Snapshot: environment variables (highest precedence) + declare -A _HARD_ENV=() + local _k + for _k in "${VAR_WHITELIST[@]}"; do + if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi + done + + # Find default.vars location + local _find_default_vars + _find_default_vars() { + local f + for f in \ + /usr/local/community-scripts/default.vars \ + "$HOME/.config/community-scripts/default.vars" \ + "./default.vars"; do + [ -f "$f" ] && { + echo "$f" + return 0 + } + done + return 1 + } + # Allow override of storages via env (for non-interactive use cases) + [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage" + [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage" + + # Create once, with storages already selected, no var_ctid/var_hostname lines + local _ensure_default_vars + _ensure_default_vars() { + _find_default_vars >/dev/null 2>&1 && return 0 + + local canonical="/usr/local/community-scripts/default.vars" + # Silent creation - no msg_info output + mkdir -p /usr/local/community-scripts + + # Pick storages before writing the file (always ask unless only one) + # Create a minimal temp file to write into + : >"$canonical" + + # Base content (no var_ctid / var_hostname here) + cat >"$canonical" <<'EOF' +# Community-Scripts defaults (var_* only). Lines starting with # are comments. +# Precedence: ENV var_* > default.vars > built-ins. +# Keep keys alphabetically sorted. + +# Container type +var_unprivileged=1 + +# Resources +var_cpu=1 +var_disk=4 +var_ram=1024 + +# Network +var_brg=vmbr0 +var_net=dhcp +var_ipv6_method=none +# var_gateway= +# var_vlan= +# var_mtu= +# var_mac= +# var_ns= + +# SSH +var_ssh=no +# var_ssh_authorized_key= + +# APT cacher (optional - with example) +# var_apt_cacher=yes +# var_apt_cacher_ip=192.168.1.10 + +# Features/Tags/verbosity +var_fuse=no +var_tun=no +var_tags=community-script +var_verbose=no + +# Security (root PW) – empty => autologin +# var_pw= +EOF + + # Now choose storages (always prompt unless just one exists) + choose_and_set_storage_for_file "$canonical" template + choose_and_set_storage_for_file "$canonical" container + + chmod 0644 "$canonical" + # Silent creation - no output message + } + + # Whitelist check + local _is_whitelisted_key + _is_whitelisted_key() { + local k="$1" + local w + for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done + return 1 + } + + # Safe parser for KEY=VALUE lines + local _load_vars_file + _load_vars_file() { + local file="$1" + [ -f "$file" ] || return 0 + msg_info "Loading defaults from ${file}" + local line key val + while IFS= read -r line || [ -n "$line" ]; do + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z "$line" || "$line" == \#* ]] && continue + if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + local var_key="${BASH_REMATCH[1]}" + local var_val="${BASH_REMATCH[2]}" + + [[ "$var_key" != var_* ]] && continue + _is_whitelisted_key "$var_key" || { + msg_debug "Ignore non-whitelisted ${var_key}" + continue + } + + # Strip quotes + if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then + var_val="${BASH_REMATCH[1]}" + elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then + var_val="${BASH_REMATCH[1]}" + fi + + # Unsafe characters + case $var_val in + \"*\") + var_val=${var_val#\"} + var_val=${var_val%\"} + ;; + \'*\') + var_val=${var_val#\'} + var_val=${var_val%\'} + ;; + esac # Hard env wins + [[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue + # Set only if not already exported + [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" + else + msg_warn "Malformed line in ${file}: ${line}" + fi + done <"$file" + msg_ok "Loaded ${file}" + } + + # 1) Ensure file exists + _ensure_default_vars + + # 2) Load file + local dv + dv="$(_find_default_vars)" || { + msg_error "default.vars not found after ensure step" + return 1 + } + _load_vars_file "$dv" + + # 3) Map var_verbose → VERBOSE + if [[ -n "${var_verbose:-}" ]]; then + case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac + else + VERBOSE="no" + fi + + # 4) Apply base settings and show summary + METHOD="mydefaults-global" + base_settings "$VERBOSE" + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}" + echo_default +} + +# ------------------------------------------------------------------------------ +# get_app_defaults_path() +# +# - Returns full path for app-specific defaults file +# - Example: /usr/local/community-scripts/defaults/.vars +# ------------------------------------------------------------------------------ + +get_app_defaults_path() { + local n="${NSAPP:-${APP,,}}" + echo "/usr/local/community-scripts/defaults/${n}.vars" +} + +# ------------------------------------------------------------------------------ +# maybe_offer_save_app_defaults +# +# - Called after advanced_settings returned with fully chosen values. +# - If no .vars exists, offers to persist current advanced settings +# into /usr/local/community-scripts/defaults/.vars +# - Only writes whitelisted var_* keys. +# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc. +# ------------------------------------------------------------------------------ +if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then + # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) + declare -ag VAR_WHITELIST=( + var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse + var_gateway var_hostname var_ipv6_method var_mac var_mtu + var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged + var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage + ) +fi + +# Note: _is_whitelisted_key() is defined above in default_var_settings section + +_sanitize_value() { + # Disallow Command-Substitution / Shell-Meta + case "$1" in + *'$('* | *'`'* | *';'* | *'&'* | *'<('*) + echo "" + return 0 + ;; + esac + echo "$1" +} + +# Map-Parser: read var_* from file into _VARS_IN associative array +# Note: Main _load_vars_file() with full validation is defined in default_var_settings section +# This simplified version is used specifically for diff operations via _VARS_IN array +declare -A _VARS_IN +_load_vars_file_to_map() { + local file="$1" + [ -f "$file" ] || return 0 + _VARS_IN=() # Clear array + local line key val + while IFS= read -r line || [ -n "$line" ]; do + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [ -z "$line" ] && continue + case "$line" in + \#*) continue ;; + esac + key=$(printf "%s" "$line" | cut -d= -f1) + val=$(printf "%s" "$line" | cut -d= -f2-) + case "$key" in + var_*) + if _is_whitelisted_key "$key"; then + _VARS_IN["$key"]="$val" + fi + ;; + esac + done <"$file" +} + +# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) +_build_vars_diff() { + local oldf="$1" newf="$2" + local k + local -A OLD=() NEW=() + _load_vars_file_to_map "$oldf" + for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done + _load_vars_file_to_map "$newf" + for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done + + local out + out+="# Diff for ${APP} (${NSAPP})\n" + out+="# Old: ${oldf}\n# New: ${newf}\n\n" + + local found_change=0 + + # Changed & Removed + for k in "${!OLD[@]}"; do + if [[ -v NEW["$k"] ]]; then + if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then + out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" + found_change=1 + fi + else + out+="- ${k}\n - old: ${OLD[$k]}\n" + found_change=1 + fi + done + + # Added + for k in "${!NEW[@]}"; do + if [[ ! -v OLD["$k"] ]]; then + out+="+ ${k}\n + new: ${NEW[$k]}\n" + found_change=1 + fi + done + + if [[ $found_change -eq 0 ]]; then + out+="(No differences)\n" + fi + + printf "%b" "$out" +} + +# Build a temporary .vars file from current advanced settings +_build_current_app_vars_tmp() { + tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" + + # NET/GW + _net="${NET:-}" + _gate="" + case "${GATE:-}" in + ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; + esac + + # IPv6 + _ipv6_method="${IPV6_METHOD:-auto}" + _ipv6_static="" + _ipv6_gateway="" + if [ "$_ipv6_method" = "static" ]; then + _ipv6_static="${IPV6_ADDR:-}" + _ipv6_gateway="${IPV6_GATE:-}" + fi + + # MTU/VLAN/MAC + _mtu="" + _vlan="" + _mac="" + case "${MTU:-}" in + ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; + esac + case "${VLAN:-}" in + ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; + esac + case "${MAC:-}" in + ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; + esac + + # DNS / Searchdomain + _ns="" + _searchdomain="" + case "${NS:-}" in + -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; + esac + case "${SD:-}" in + -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; + esac + + # SSH / APT / Features + _ssh="${SSH:-no}" + _ssh_auth="${SSH_AUTHORIZED_KEY:-}" + _apt_cacher="${APT_CACHER:-}" + _apt_cacher_ip="${APT_CACHER_IP:-}" + _fuse="${ENABLE_FUSE:-no}" + _tun="${ENABLE_TUN:-no}" + _tags="${TAGS:-}" + _verbose="${VERBOSE:-no}" + + # Type / Resources / Identity + _unpriv="${CT_TYPE:-1}" + _cpu="${CORE_COUNT:-1}" + _ram="${RAM_SIZE:-1024}" + _disk="${DISK_SIZE:-4}" + _hostname="${HN:-$NSAPP}" + + # Storage + _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" + _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" + + { + echo "# App-specific defaults for ${APP} (${NSAPP})" + echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + echo + + echo "var_unprivileged=$(_sanitize_value "$_unpriv")" + echo "var_cpu=$(_sanitize_value "$_cpu")" + echo "var_ram=$(_sanitize_value "$_ram")" + echo "var_disk=$(_sanitize_value "$_disk")" + + [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" + [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" + [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" + [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" + [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" + [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" + [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" + + [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" + # var_ipv6_static removed - static IPs are unique, can't be default + + [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" + [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" + + [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" + [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" + + [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" + [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" + [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" + [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" + + [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" + [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" + + [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" + [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" + } >"$tmpf" + + echo "$tmpf" +} + +# ------------------------------------------------------------------------------ +# maybe_offer_save_app_defaults() +# +# - Called after advanced_settings() +# - Offers to save current values as app defaults if not existing +# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel +# ------------------------------------------------------------------------------ +maybe_offer_save_app_defaults() { + local app_vars_path + app_vars_path="$(get_app_defaults_path)" + + # always build from current settings + local new_tmp diff_tmp + new_tmp="$(_build_current_app_vars_tmp)" + diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" + + # 1) if no file → offer to create + if [[ ! -f "$app_vars_path" ]]; then + if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then + mkdir -p "$(dirname "$app_vars_path")" + install -m 0644 "$new_tmp" "$app_vars_path" + msg_ok "Saved app defaults: ${app_vars_path}" + fi + rm -f "$new_tmp" "$diff_tmp" + return 0 + fi + + # 2) if file exists → build diff + _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp" + + # if no differences → do nothing + if grep -q "^(No differences)$" "$diff_tmp"; then + rm -f "$new_tmp" "$diff_tmp" + return 0 + fi + + # 3) if file exists → show menu with default selection "Update Defaults" + local app_vars_file + app_vars_file="$(basename "$app_vars_path")" + + while true; do + local sel + sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "APP DEFAULTS – ${APP}" \ + --menu "Differences detected. What do you want to do?" 20 78 10 \ + "Update Defaults" "Write new values to ${app_vars_file}" \ + "Keep Current" "Keep existing defaults (no changes)" \ + "View Diff" "Show a detailed diff" \ + "Cancel" "Abort without changes" \ + --default-item "Update Defaults" \ + 3>&1 1>&2 2>&3)" || { sel="Cancel"; } + + case "$sel" in + "Update Defaults") + install -m 0644 "$new_tmp" "$app_vars_path" + msg_ok "Updated app defaults: ${app_vars_path}" + break + ;; + "Keep Current") + msg_custom "ℹ️" "${BL}" "Keeping current app defaults: ${app_vars_path}" + break + ;; + "View Diff") + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "Diff – ${APP}" \ + --scrolltext --textbox "$diff_tmp" 25 100 + ;; + "Cancel" | *) + msg_custom "🚫" "${YW}" "Canceled. No changes to app defaults." + break + ;; + esac + done + + rm -f "$new_tmp" "$diff_tmp" +} + +ensure_storage_selection_for_vars_file() { + local vf="$1" + + # Read stored values (if any) + local tpl ct + tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) + ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) + + if [[ -n "$tpl" && -n "$ct" ]]; then + TEMPLATE_STORAGE="$tpl" + CONTAINER_STORAGE="$ct" + return 0 + fi + + choose_and_set_storage_for_file "$vf" template + choose_and_set_storage_for_file "$vf" container + + # Silent operation - no output message +} + +ensure_global_default_vars_file() { + local vars_path="/usr/local/community-scripts/default.vars" + if [[ ! -f "$vars_path" ]]; then + mkdir -p "$(dirname "$vars_path")" + touch "$vars_path" + fi + echo "$vars_path" +} + +# ============================================================================== +# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# advanced_settings() +# +# - Interactive whiptail menu for comprehensive container configuration +# - Allows user to customize: +# * Container type (privileged/unprivileged) +# * Root password +# * Container ID (CTID) +# * Hostname +# * Resources (disk size, CPU cores, RAM) +# * Network (IPv4/IPv6, gateway, DNS, MAC, VLAN, MTU) +# * SSH settings and key injection +# * Advanced features (FUSE, TUN, keyctl) +# * Tags for organization +# * Verbose/debug mode +# - Loops until user confirms or cancels +# - Validates all input and shows current selections +# ------------------------------------------------------------------------------ +advanced_settings() { + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58 + # Setting Default Tag for Advanced Settings + TAGS="community-script;${var_tags:-}" + CT_DEFAULT_TYPE="${CT_TYPE}" + CT_TYPE="" + while [ -z "$CT_TYPE" ]; do + if [ "$CT_DEFAULT_TYPE" == "1" ]; then + if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ + "1" "Unprivileged" ON \ + "0" "Privileged" OFF \ + 3>&1 1>&2 2>&3); then + if [ -n "$CT_TYPE" ]; then + CT_TYPE_DESC="Unprivileged" + if [ "$CT_TYPE" -eq 0 ]; then + CT_TYPE_DESC="Privileged" + fi + echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os |${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" + fi + else + exit_script + fi + fi + if [ "$CT_DEFAULT_TYPE" == "0" ]; then + if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ + "1" "Unprivileged" OFF \ + "0" "Privileged" ON \ + 3>&1 1>&2 2>&3); then + if [ -n "$CT_TYPE" ]; then + CT_TYPE_DESC="Unprivileged" + if [ "$CT_TYPE" -eq 0 ]; then + CT_TYPE_DESC="Privileged" + fi + echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" + fi + else + exit_script + fi + fi + done + + while true; do + if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then + # Empty = Autologin + if [[ -z "$PW1" ]]; then + PW="" + PW1="Automatic Login" + echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}" + break + fi + + # Invalid: contains spaces + if [[ "$PW1" == *" "* ]]; then + whiptail --msgbox "Password cannot contain spaces." 8 58 + continue + fi + + # Invalid: too short + if ((${#PW1} < 5)); then + whiptail --msgbox "Password must be at least 5 characters." 8 58 + continue + fi + + # Confirm password + if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then + if [[ "$PW1" == "$PW2" ]]; then + PW="-password $PW1" + echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}" + break + else + whiptail --msgbox "Passwords do not match. Please try again." 8 58 + fi + else + exit_script + fi + else + exit_script + fi + done + + if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then + if [ -z "$CT_ID" ]; then + CT_ID="$NEXTID" + fi + else + exit_script + fi + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + + while true; do + if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then + if [ -z "$CT_NAME" ]; then + HN="$NSAPP" + else + HN=$(echo "${CT_NAME,,}" | tr -d ' ') + fi + # Hostname validate (RFC 1123) + if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" + break + else + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 + fi + else + exit_script + fi + done + + while true; do + DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script + + if [ -z "$DISK_SIZE" ]; then + DISK_SIZE="$var_disk" + fi + + if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" + break + else + whiptail --msgbox "Disk size must be a positive integer!" 8 58 + fi + done + + while true; do + CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script + + if [ -z "$CORE_COUNT" ]; then + CORE_COUNT="$var_cpu" + fi + + if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" + break + else + whiptail --msgbox "CPU core count must be a positive integer!" 8 58 + fi + done + + while true; do + RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script + + if [ -z "$RAM_SIZE" ]; then + RAM_SIZE="$var_ram" + fi + + if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + break + else + whiptail --msgbox "RAM size must be a positive integer!" 8 58 + fi + done + + IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) + BRIDGES="" + OLD_IFS=$IFS + IFS=$'\n' + for iface_filepath in ${IFACE_FILEPATH_LIST}; do + + iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') + (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true + + if [ -f "${iface_indexes_tmpfile}" ]; then + + while read -r pair; do + start=$(echo "${pair}" | cut -d':' -f1) + end=$(echo "${pair}" | cut -d':' -f2) + + if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then + iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') + BRIDGES="${iface_name}"$'\n'"${BRIDGES}" + fi + + done <"${iface_indexes_tmpfile}" + rm -f "${iface_indexes_tmpfile}" + fi + + done + IFS=$OLD_IFS + BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) + if [[ -z "$BRIDGES" ]]; then + BRG="vmbr0" + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + else + # Build bridge menu with descriptions + BRIDGE_MENU_OPTIONS=() + while IFS= read -r bridge; do + if [[ -n "$bridge" ]]; then + # Get description from Proxmox built-in method - find comment for this specific bridge + description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') + if [[ -n "$description" ]]; then + BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") + else + BRIDGE_MENU_OPTIONS+=("$bridge" " ") + fi + fi + done <<<"$BRIDGES" + + BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) + if [[ -z "$BRG" ]]; then + exit_script + else + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + fi + fi + + # IPv4 methods: dhcp, static, none + while true; do + IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "IPv4 Address Management" \ + --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ + "dhcp" "Automatic (DHCP, recommended)" \ + "static" "Static (manual entry)" \ + 3>&1 1>&2 2>&3) + + exit_status=$? + if [ $exit_status -ne 0 ]; then + exit_script + fi + + case "$IPV4_METHOD" in + dhcp) + NET="dhcp" + GATE="" + echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" + break + ;; + static) + # Static: call and validate CIDR address + while true; do + NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ + --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) + if [ -z "$NET" ]; then + whiptail --msgbox "IPv4 address must not be empty." 8 58 + continue + elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then + echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" + break + else + whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 + fi + done + + # call and validate Gateway + while true; do + GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ + --title "Gateway IP" 3>&1 1>&2 2>&3) + if [ -z "$GATE1" ]; then + whiptail --msgbox "Gateway IP address cannot be empty." 8 58 + elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + whiptail --msgbox "Invalid Gateway IP address format." 8 58 + else + GATE=",gw=$GATE1" + echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" + break + fi + done + break + ;; + esac + done + + # IPv6 Address Management selection + while true; do + IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ + "Select IPv6 Address Management Type:" 15 58 4 \ + "auto" "SLAAC/AUTO (recommended, default)" \ + "dhcp" "DHCPv6" \ + "static" "Static (manual entry)" \ + "none" "Disabled" \ + --default-item "auto" 3>&1 1>&2 2>&3) + [ $? -ne 0 ] && exit_script + + case "$IPV6_METHOD" in + auto) + echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" + IPV6_ADDR="" + IPV6_GATE="" + break + ;; + dhcp) + echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" + IPV6_ADDR="dhcp" + IPV6_GATE="" + break + ;; + static) + # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) + while true; do + IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ + "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ + --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script + if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then + echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" + break + else + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ + "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 + fi + done + # Optional: ask for IPv6 gateway for static config + while true; do + IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ + "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) + if [ -z "$IPV6_GATE" ]; then + IPV6_GATE="" + break + elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then + break + else + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ + "Invalid IPv6 gateway format." 8 58 + fi + done + break + ;; + none) + echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" + IPV6_ADDR="none" + IPV6_GATE="" + break + ;; + *) + exit_script + ;; + esac + done + + if [ "$var_os" == "alpine" ]; then + APT_CACHER="" + APT_CACHER_IP="" + else + if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then + APT_CACHER="${APT_CACHER_IP:+yes}" + echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" + else + exit_script + fi + fi + + # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then + # DISABLEIP6="yes" + # else + # DISABLEIP6="no" + # fi + # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" + + if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then + if [ -z "$MTU1" ]; then + MTU1="Default" + MTU="" + else + MTU=",mtu=$MTU1" + fi + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" + else + exit_script + fi + + if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then + if [ -z "$SD" ]; then + SX=Host + SD="" + else + SX=$SD + SD="-searchdomain=$SD" + fi + echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" + else + exit_script + fi + + if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then + if [ -z "$NX" ]; then + NX=Host + NS="" + else + NS="-nameserver=$NX" + fi + echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" + else + exit_script + fi + + if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then + UDHCPC_FIX="yes" + else + UDHCPC_FIX="no" + fi + export UDHCPC_FIX + + if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then + if [ -z "$MAC1" ]; then + MAC1="Default" + MAC="" + else + MAC=",hwaddr=$MAC1" + echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" + fi + else + exit_script + fi + + if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then + if [ -z "$VLAN1" ]; then + VLAN1="Default" + VLAN="" + else + VLAN=",tag=$VLAN1" + fi + echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" + else + exit_script + fi + + if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then + if [ -n "${ADV_TAGS}" ]; then + ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') + TAGS="${ADV_TAGS}" + else + TAGS=";" + fi + echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" + else + exit_script + fi + + configure_ssh_settings + export SSH_KEYS_FILE + echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" + if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then + ENABLE_FUSE="yes" + else + ENABLE_FUSE="no" + fi + echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}" + + if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then + VERBOSE="yes" + else + VERBOSE="no" + fi + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" + + if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then + echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" + else + clear + header_info + echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" + echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" + advanced_settings + fi +} + +# ============================================================================== +# SECTION 7: USER INTERFACE & DIAGNOSTICS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# diagnostics_check() +# +# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics +# - Asks user whether to send anonymous diagnostic data +# - Saves DIAGNOSTICS=yes/no in the config file +# - Creates file if missing with default DIAGNOSTICS=yes +# - Reads current diagnostics setting from file +# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out +# ------------------------------------------------------------------------------ +diagnostics_check() { + if ! [ -d "/usr/local/community-scripts" ]; then + mkdir -p /usr/local/community-scripts + fi + + if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then + if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then + cat </usr/local/community-scripts/diagnostics +DIAGNOSTICS=yes + +#This file is used to store the diagnostics settings for the Community-Scripts API. +#https://github.com/community-scripts/ProxmoxVED/discussions/1836 +#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. +#You can review the data at https://community-scripts.github.io/ProxmoxVE/data +#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. +#This will disable the diagnostics feature. +#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue. +#This will enable the diagnostics feature. +#The following information will be sent: +#"disk_size" +#"core_count" +#"ram_size" +#"os_type" +#"os_version" +#"nsapp" +#"method" +#"pve_version" +#"status" +#If you have any concerns, please review the source code at /misc/build.func +EOF + DIAGNOSTICS="yes" + else + cat </usr/local/community-scripts/diagnostics +DIAGNOSTICS=no + +#This file is used to store the diagnostics settings for the Community-Scripts API. +#https://github.com/community-scripts/ProxmoxVED/discussions/1836 +#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. +#You can review the data at https://community-scripts.github.io/ProxmoxVE/data +#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. +#This will disable the diagnostics feature. +#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue. +#This will enable the diagnostics feature. +#The following information will be sent: +#"disk_size" +#"core_count" +#"ram_size" +#"os_type" +#"os_version" +#"nsapp" +#"method" +#"pve_version" +#"status" +#If you have any concerns, please review the source code at /misc/build.func +EOF + DIAGNOSTICS="no" + fi + else + DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics) + + fi +} + +diagnostics_menu() { + if [ "${DIAGNOSTICS:-no}" = "yes" ]; then + if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "DIAGNOSTIC SETTINGS" \ + --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ + --yes-button "No" --no-button "Back"; then + DIAGNOSTICS="no" + sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics + whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 + fi + else + if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "DIAGNOSTIC SETTINGS" \ + --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ + --yes-button "Yes" --no-button "Back"; then + DIAGNOSTICS="yes" + sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics + whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 + fi + fi +} + +# ------------------------------------------------------------------------------ +# echo_default() +# +# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.) +# - Uses icons and formatting for readability +# - Convert CT_TYPE to description +# ------------------------------------------------------------------------------ +echo_default() { + CT_TYPE_DESC="Unprivileged" + if [ "$CT_TYPE" -eq 0 ]; then + CT_TYPE_DESC="Privileged" + fi + echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + if [ "$VERBOSE" == "yes" ]; then + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" + fi + echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" + echo -e " " +} + +# ------------------------------------------------------------------------------ +# install_script() +# +# - Main entrypoint for installation mode +# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check) +# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit) +# - Applies chosen settings and triggers container build +# ------------------------------------------------------------------------------ +install_script() { + pve_check + shell_check + root_check + arch_check + ssh_check + maxkeys_check + diagnostics_check + + if systemctl is-active -q ping-instances.service; then + systemctl -q stop ping-instances.service + fi + + NEXTID=$(pvesh get /cluster/nextid) + timezone=$(cat /etc/timezone) + + # Show APP Header + header_info + + # --- Support CLI argument as direct preset (default, advanced, …) --- + CHOICE="${mode:-${1:-}}" + + # If no CLI argument → show whiptail menu + # Build menu dynamically based on available options + local appdefaults_option="" + local settings_option="" + local menu_items=( + "1" "Default Install" + "2" "Advanced Install" + "3" "My Defaults" + ) + + if [ -f "$(get_app_defaults_path)" ]; then + appdefaults_option="4" + menu_items+=("4" "App Defaults for ${APP}") + settings_option="5" + menu_items+=("5" "Settings") + else + settings_option="4" + menu_items+=("4" "Settings") + fi + + if [ -z "$CHOICE" ]; then + + TMP_CHOICE=$(whiptail \ + --backtitle "Proxmox VE Helper Scripts" \ + --title "Community-Scripts Options" \ + --ok-button "Select" --cancel-button "Exit Script" \ + --notags \ + --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ + 20 60 9 \ + "${menu_items[@]}" \ + --default-item "1" \ + 3>&1 1>&2 2>&3) || exit_script + CHOICE="$TMP_CHOICE" + fi + + APPDEFAULTS_OPTION="$appdefaults_option" + SETTINGS_OPTION="$settings_option" + + # --- Main case --- + local defaults_target="" + local run_maybe_offer="no" + case "$CHOICE" in + 1 | default | DEFAULT) + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" + VERBOSE="no" + METHOD="default" + base_settings "$VERBOSE" + echo_default + defaults_target="$(ensure_global_default_vars_file)" + ;; + 2 | advanced | ADVANCED) + header_info + + echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" + echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" + METHOD="advanced" + base_settings + advanced_settings + defaults_target="$(ensure_global_default_vars_file)" + run_maybe_offer="yes" + ;; + 3 | mydefaults | MYDEFAULTS) + default_var_settings || { + msg_error "Failed to apply default.vars" + exit 1 + } + defaults_target="/usr/local/community-scripts/default.vars" + ;; + "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) + if [ -f "$(get_app_defaults_path)" ]; then + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" + METHOD="appdefaults" + base_settings + _load_vars_file "$(get_app_defaults_path)" + echo_default + defaults_target="$(get_app_defaults_path)" + else + msg_error "No App Defaults available for ${APP}" + exit 1 + fi + ;; + "$SETTINGS_OPTION" | settings | SETTINGS) + settings_menu + defaults_target="" + ;; + *) + echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" + exit 1 + ;; + esac + + if [[ -n "$defaults_target" ]]; then + ensure_storage_selection_for_vars_file "$defaults_target" + fi + + if [[ "$run_maybe_offer" == "yes" ]]; then + maybe_offer_save_app_defaults + fi +} + +edit_default_storage() { + local vf="/usr/local/community-scripts/default.vars" + + # Ensure file exists + if [[ ! -f "$vf" ]]; then + mkdir -p "$(dirname "$vf")" + touch "$vf" + fi + + # Let ensure_storage_selection_for_vars_file handle everything + ensure_storage_selection_for_vars_file "$vf" +} + +settings_menu() { + while true; do + local settings_items=( + "1" "Manage API-Diagnostic Setting" + "2" "Edit Default.vars" + "3" "Edit Default Storage" + ) + if [ -f "$(get_app_defaults_path)" ]; then + settings_items+=("4" "Edit App.vars for ${APP}") + settings_items+=("5" "Exit") + else + settings_items+=("4" "Exit") + fi + + local choice + choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "Community-Scripts SETTINGS Menu" \ + --ok-button "OK" --cancel-button "Back" \ + --menu "\n\nChoose a settings option:\n\nUse TAB or Arrow keys to navigate, ENTER to select." 20 60 9 \ + "${settings_items[@]}" \ + 3>&1 1>&2 2>&3) || break + + case "$choice" in + 1) diagnostics_menu ;; + 2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;; + 3) edit_default_storage ;; + 4) + if [ -f "$(get_app_defaults_path)" ]; then + ${EDITOR:-nano} "$(get_app_defaults_path)" + else + exit_script + fi + ;; + 5) exit_script ;; + esac + done +} + +# ------------------------------------------------------------------------------ +# check_container_resources() +# +# - Compares host RAM/CPU with required values +# - Warns if under-provisioned and asks user to continue or abort +# ------------------------------------------------------------------------------ +check_container_resources() { + current_ram=$(free -m | awk 'NR==2{print $2}') + current_cpu=$(nproc) + + if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then + echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}" + echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n" + echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? " + read -r prompt + if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then + echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}" + exit 1 + fi + else + echo -e "" + fi +} + +# ------------------------------------------------------------------------------ +# check_container_storage() +# +# - Checks /boot partition usage +# - Warns if usage >80% and asks user confirmation before proceeding +# ------------------------------------------------------------------------------ +check_container_storage() { + total_size=$(df /boot --output=size | tail -n 1) + local used_size=$(df /boot --output=used | tail -n 1) + usage=$((100 * used_size / total_size)) + if ((usage > 80)); then + echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}" + echo -ne "Continue anyway? " + read -r prompt + if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then + echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}" + exit 1 + fi + fi +} + +# ------------------------------------------------------------------------------ +# ssh_extract_keys_from_file() +# +# - Extracts valid SSH public keys from given file +# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines +# ------------------------------------------------------------------------------ +ssh_extract_keys_from_file() { + local f="$1" + [[ -r "$f" ]] || return 0 + tr -d '\r' <"$f" | awk ' + /^[[:space:]]*#/ {next} + /^[[:space:]]*$/ {next} + # nackt: typ base64 [comment] + /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next} + # mit Optionen: finde ab erstem Key-Typ + { + match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/) + if (RSTART>0) { print substr($0, RSTART) } + } + ' +} + +# ------------------------------------------------------------------------------ +# ssh_build_choices_from_files() +# +# - Builds interactive whiptail checklist of available SSH keys +# - Generates fingerprint, type and comment for each key +# ------------------------------------------------------------------------------ +ssh_build_choices_from_files() { + local -a files=("$@") + CHOICES=() + COUNT=0 + MAPFILE="$(mktemp)" + local id key typ fp cmt base ln=0 + + for f in "${files[@]}"; do + [[ -f "$f" && -r "$f" ]] || continue + base="$(basename -- "$f")" + case "$base" in + known_hosts | known_hosts.* | config) continue ;; + id_*) [[ "$f" != *.pub ]] && continue ;; + esac + + # map every key in file + while IFS= read -r key; do + [[ -n "$key" ]] || continue + + typ="" + fp="" + cmt="" + # Only the pure key part (without options) is already included in ‘key’. + read -r _typ _b64 _cmt <<<"$key" + typ="${_typ:-key}" + cmt="${_cmt:-}" + # Fingerprint via ssh-keygen (if available) + if command -v ssh-keygen >/dev/null 2>&1; then + fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')" + fi + # Label shorten + [[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..." + + ln=$((ln + 1)) + COUNT=$((COUNT + 1)) + id="K${COUNT}" + echo "${id}|${key}" >>"$MAPFILE" + CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF") + done < <(ssh_extract_keys_from_file "$f") + done +} + +# ------------------------------------------------------------------------------ +# ssh_discover_default_files() +# +# - Scans standard paths for SSH keys +# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc. +# ------------------------------------------------------------------------------ +ssh_discover_default_files() { + local -a cand=() + shopt -s nullglob + cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2) + cand+=(/root/.ssh/*.pub) + cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*) + shopt -u nullglob + printf '%s\0' "${cand[@]}" +} + +configure_ssh_settings() { + SSH_KEYS_FILE="$(mktemp)" + : >"$SSH_KEYS_FILE" + + IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') + ssh_build_choices_from_files "${_def_files[@]}" + local default_key_count="$COUNT" + + local ssh_key_mode + if [[ "$default_key_count" -gt 0 ]]; then + ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ + "Provision SSH keys for root:" 14 72 4 \ + "found" "Select from detected keys (${default_key_count})" \ + "manual" "Paste a single public key" \ + "folder" "Scan another folder (path or glob)" \ + "none" "No keys" 3>&1 1>&2 2>&3) || exit_script + else + ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ + "No host keys detected; choose manual/none:" 12 72 2 \ + "manual" "Paste a single public key" \ + "none" "No keys" 3>&1 1>&2 2>&3) || exit_script + fi + + case "$ssh_key_mode" in + found) + local selection + selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \ + --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script + for tag in $selection; do + tag="${tag%\"}" + tag="${tag#\"}" + local line + line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) + [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" + done + ;; + manual) + SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)" + [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE" + ;; + folder) + local glob_path + glob_path=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3) + if [[ -n "$glob_path" ]]; then + shopt -s nullglob + read -r -a _scan_files <<<"$glob_path" + shopt -u nullglob + if [[ "${#_scan_files[@]}" -gt 0 ]]; then + ssh_build_choices_from_files "${_scan_files[@]}" + if [[ "$COUNT" -gt 0 ]]; then + local folder_selection + folder_selection=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \ + --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script + for tag in $folder_selection; do + tag="${tag%\"}" + tag="${tag#\"}" + local line + line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) + [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" + done + else + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60 + fi + else + whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60 + fi + fi + ;; + none) + : + ;; + esac + + if [[ -s "$SSH_KEYS_FILE" ]]; then + sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" + printf '\n' >>"$SSH_KEYS_FILE" + fi + + if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then + if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then + SSH="yes" + else + SSH="no" + fi + else + SSH="no" + fi +} + +# ------------------------------------------------------------------------------ +# start() +# +# - Entry point of script +# - On Proxmox host: calls install_script +# - In silent mode: runs update_script +# - Otherwise: shows update/setting menu +# ------------------------------------------------------------------------------ +start() { + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func) + if command -v pveversion >/dev/null 2>&1; then + install_script || return 0 + return 0 + elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then + VERBOSE="no" + set_std_mode + update_script + else + CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ + "Support/Update functions for ${APP} LXC. Choose an option:" \ + 12 60 3 \ + "1" "YES (Silent Mode)" \ + "2" "YES (Verbose Mode)" \ + "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3) + + case "$CHOICE" in + 1) + VERBOSE="no" + set_std_mode + ;; + 2) + VERBOSE="yes" + set_std_mode + ;; + 3) + clear + exit_script + exit + ;; + esac + update_script + fi +} + +# ============================================================================== +# SECTION 8: CONTAINER CREATION & DEPLOYMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# build_container() +# +# - Main function for creating and configuring LXC container +# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6) +# - Creates container via pct create with all specified settings +# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough +# - Starts container and waits for network connectivity +# - Installs base packages (curl, sudo, etc.) +# - Injects SSH keys if configured +# - Executes -install.sh inside container +# - Posts installation telemetry to API if diagnostics enabled +# ------------------------------------------------------------------------------ +build_container() { + # if [ "$VERBOSE" == "yes" ]; then set -x; fi + + NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" + + # MAC + if [[ -n "$MAC" ]]; then + case "$MAC" in + ,hwaddr=*) NET_STRING+="$MAC" ;; + *) NET_STRING+=",hwaddr=$MAC" ;; + esac + fi + + # IP (immer zwingend, Standard dhcp) + NET_STRING+=",ip=${NET:-dhcp}" + + # Gateway + if [[ -n "$GATE" ]]; then + case "$GATE" in + ,gw=*) NET_STRING+="$GATE" ;; + *) NET_STRING+=",gw=$GATE" ;; + esac + fi + + # VLAN + if [[ -n "$VLAN" ]]; then + case "$VLAN" in + ,tag=*) NET_STRING+="$VLAN" ;; + *) NET_STRING+=",tag=$VLAN" ;; + esac + fi + + # MTU + if [[ -n "$MTU" ]]; then + case "$MTU" in + ,mtu=*) NET_STRING+="$MTU" ;; + *) NET_STRING+=",mtu=$MTU" ;; + esac + fi + + # IPv6 Handling + case "$IPV6_METHOD" in + auto) NET_STRING="$NET_STRING,ip6=auto" ;; + dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;; + static) + NET_STRING="$NET_STRING,ip6=$IPV6_ADDR" + [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE" + ;; + none) ;; + esac + + if [ "$CT_TYPE" == "1" ]; then + FEATURES="keyctl=1,nesting=1" + else + FEATURES="nesting=1" + fi + + if [ "$ENABLE_FUSE" == "yes" ]; then + FEATURES="$FEATURES,fuse=1" + fi + + TEMP_DIR=$(mktemp -d) + pushd "$TEMP_DIR" >/dev/null + if [ "$var_os" == "alpine" ]; then + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)" + else + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" + fi + export DIAGNOSTICS="$DIAGNOSTICS" + export RANDOM_UUID="$RANDOM_UUID" + export 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" + export PCT_OPTIONS=" + -features $FEATURES + -hostname $HN + -tags $TAGS + $SD + $NS + $NET_STRING + -onboot 1 + -cores $CORE_COUNT + -memory $RAM_SIZE + -unprivileged $CT_TYPE + $PW +" + export TEMPLATE_STORAGE="${var_template_storage:-}" + export CONTAINER_STORAGE="${var_container_storage:-}" + create_lxc_container || exit $? + + LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" + + # ============================================================================ + # GPU/USB PASSTHROUGH CONFIGURATION + # ============================================================================ + + # List of applications that benefit from GPU acceleration + GPU_APPS=( + "immich" "channels" "emby" "ersatztv" "frigate" + "jellyfin" "plex" "scrypted" "tdarr" "unmanic" + "ollama" "fileflows" "open-webui" "tunarr" "debian" + "handbrake" "sunshine" "moonlight" "kodi" "stremio" + "viseron" + ) + + # Check if app needs GPU + is_gpu_app() { + local app="${1,,}" + for gpu_app in "${GPU_APPS[@]}"; do + [[ "$app" == "${gpu_app,,}" ]] && return 0 + done + return 1 + } + + # Detect all available GPU devices + detect_gpu_devices() { + INTEL_DEVICES=() + AMD_DEVICES=() + NVIDIA_DEVICES=() + + # Store PCI info to avoid multiple calls + local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") + + # Check for Intel GPU - look for Intel vendor ID [8086] + if echo "$pci_vga_info" | grep -q "\[8086:"; then + msg_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 + 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" + + # Simple passthrough - just bind /dev/nvidia* devices if they exist + for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do + [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") + done + + if [[ ${#NVIDIA_DEVICES[@]} -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 + + # Debug output + msg_debug "Intel devices: ${INTEL_DEVICES[*]}" + msg_debug "AMD devices: ${AMD_DEVICES[*]}" + msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" + } + + # Configure USB passthrough for privileged containers + configure_usb_passthrough() { + if [[ "$CT_TYPE" != "0" ]]; then + return 0 + fi + + msg_info "Configuring automatic USB passthrough (privileged container)" + cat <>"$LXC_CONFIG" +# Automatic USB passthrough (privileged container) +lxc.cgroup2.devices.allow: a +lxc.cap.drop: +lxc.cgroup2.devices.allow: c 188:* rwm +lxc.cgroup2.devices.allow: c 189:* rwm +lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir +lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file +lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file +lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file +lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file +EOF + msg_ok "USB passthrough configured" + } + + # Configure GPU passthrough + configure_gpu_passthrough() { + # Skip if not a GPU app and not privileged + if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then + return 0 + fi + + detect_gpu_devices + + # Count available GPU types + local gpu_count=0 + local available_gpus=() + + if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then + available_gpus+=("INTEL") + gpu_count=$((gpu_count + 1)) + fi + + if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then + available_gpus+=("AMD") + gpu_count=$((gpu_count + 1)) + fi + + if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then + available_gpus+=("NVIDIA") + gpu_count=$((gpu_count + 1)) + fi + + if [[ $gpu_count -eq 0 ]]; then + msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough" + return 0 + fi + + local selected_gpu="" + + if [[ $gpu_count -eq 1 ]]; then + # Automatic selection for single GPU + selected_gpu="${available_gpus[0]}" + msg_custom "⚙️" "${GN}" "Automatically configuring ${selected_gpu} GPU passthrough" + else + # Multiple GPUs - ask user + echo -e "\n${INFO} Multiple GPU types detected:" + for gpu in "${available_gpus[@]}"; do + echo " - $gpu" + done + read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu + selected_gpu="${selected_gpu^^}" + + # Validate selection + local valid=0 + for gpu in "${available_gpus[@]}"; do + [[ "$selected_gpu" == "$gpu" ]] && valid=1 + done + + if [[ $valid -eq 0 ]]; then + msg_warn "Invalid selection. Skipping GPU passthrough." + return 0 + fi + fi + + # Apply passthrough configuration based on selection + local dev_idx=0 + + case "$selected_gpu" in + INTEL | AMD) + local devices=() + [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") + [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") + + # Add lxc.mount.entry for each device + for dev in "${devices[@]}"; do + echo "lxc.mount.entry: $dev $dev none bind,optional,create=file" >>"$LXC_CONFIG" + + if [[ "$CT_TYPE" == "0" ]]; then + # Privileged container - also add cgroup allows + local major minor + major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") + minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") + + if [[ "$major" != "0" && "$minor" != "0" ]]; then + echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" + fi + fi + done + + export GPU_TYPE="$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 + + # Add lxc.mount.entry for each NVIDIA device + for dev in "${NVIDIA_DEVICES[@]}"; do + echo "lxc.mount.entry: $dev $dev none bind,optional,create=file" >>"$LXC_CONFIG" + + if [[ "$CT_TYPE" == "0" ]]; then + # Privileged container - also add cgroup allows + local major minor + major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") + minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") + + if [[ "$major" != "0" && "$minor" != "0" ]]; then + echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" + fi + fi + done + + export GPU_TYPE="NVIDIA" + msg_ok "NVIDIA GPU passthrough configured (${#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" +lxc.cgroup2.devices.allow: c 10:200 rwm +lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file +EOF + fi + + # Coral TPU passthrough + if [[ -e /dev/apex_0 ]]; then + msg_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 + + # ============================================================================ + # START CONTAINER AND INSTALL USERLAND + # ============================================================================ + + msg_info "Starting LXC Container" + pct start "$CTID" + + # Wait for container to be running + for i in {1..10}; do + if pct status "$CTID" | grep -q "status: running"; then + msg_ok "Started LXC Container" + break + fi + sleep 1 + if [ "$i" -eq 10 ]; then + msg_error "LXC Container did not reach running state" + exit 1 + fi + done + + # Wait for network (skip for Alpine initially) + if [ "$var_os" != "alpine" ]; then + msg_info "Waiting for network in LXC container" + + # Wait for IP + for i in {1..20}; do + ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) + [ -n "$ip_in_lxc" ] && break + sleep 1 + done + + if [ -z "$ip_in_lxc" ]; then + msg_error "No IP assigned to CT $CTID after 20s" + exit 1 + fi + + # Simple connectivity check - just verify IP is assigned + msg_ok "Network configured (IP: $ip_in_lxc)" + fi + # Function to get correct GID inside container + get_container_gid() { + local group="$1" + local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) + echo "${gid:-44}" # Default to 44 if not found + } + + fix_gpu_gids + + # Continue with standard container setup + msg_info "Customizing LXC Container" + + # # Install GPU userland if configured + # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then + # install_gpu_userland "VAAPI" + # fi + + # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then + # install_gpu_userland "NVIDIA" + # fi + + # Continue with standard container setup + if [ "$var_os" == "alpine" ]; then + sleep 3 + pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories +http://dl-cdn.alpinelinux.org/alpine/latest-stable/main +http://dl-cdn.alpinelinux.org/alpine/latest-stable/community +EOF' + pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null" + else + sleep 3 + pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen" + pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ + echo LANG=\$locale_line >/etc/default/locale && \ + locale-gen >/dev/null && \ + export LANG=\$locale_line" + + if [[ -z "${tz:-}" ]]; then + tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC") + fi + + if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then + pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime" + else + msg_warn "Skipping timezone setup – zone '$tz' not found in container" + fi + + pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || { + msg_error "apt-get base packages installation failed" + exit 1 + } + fi + + msg_ok "Customized LXC Container" + + # Install SSH keys + install_ssh_keys_into_ct + + # Run application installer + if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then + local exit_code=$? + # Try to copy installation log from container before exiting + if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then + pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "/tmp/install-${SESSION_ID}.log" 2>/dev/null || true + fi + exit $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 + fi + ;; + "" | n | no) + msg_custom "ℹ️" "${BL}" "Container was not removed." + ;; + *) + msg_warn "Invalid response. Container was not removed." + ;; + esac +} + +# ------------------------------------------------------------------------------ +# Storage discovery / selection helpers +# ------------------------------------------------------------------------------ +# ===== Storage discovery / selection helpers (ported from create_lxc.sh) ===== +resolve_storage_preselect() { + local class="$1" preselect="$2" required_content="" + case "$class" in + template) required_content="vztmpl" ;; + container) required_content="rootdir" ;; + *) return 1 ;; + esac + [[ -z "$preselect" ]] && return 1 + if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then + msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)" + return 1 + fi + + local line total used free + line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')" + if [[ -z "$line" ]]; then + STORAGE_INFO="n/a" + else + total="$(awk '{print $4}' <<<"$line")" + used="$(awk '{print $5}' <<<"$line")" + free="$(awk '{print $6}' <<<"$line")" + local total_h used_h free_h + if command -v numfmt >/dev/null 2>&1; then + total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")" + used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")" + free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")" + STORAGE_INFO="Free: ${free_h} Used: ${used_h}" + else + STORAGE_INFO="Free: ${free} Used: ${used}" + fi + fi + STORAGE_RESULT="$preselect" + return 0 +} + +fix_gpu_gids() { + if [[ -z "${GPU_TYPE:-}" ]]; then + return 0 + fi + + # Silent operation to avoid spinner conflicts + msg_custom "🔧" "${BL}" "Detecting and setting correct GPU group IDs" + + # Ermittle die tatsächlichen GIDs aus dem Container + local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") + local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") + + # Fallbacks wenn Gruppen nicht existieren + if [[ -z "$video_gid" ]]; then + # Versuche die video Gruppe zu erstellen + pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" + video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") + [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback + fi + + if [[ -z "$render_gid" ]]; then + # Versuche die render Gruppe zu erstellen + pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" + render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") + [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback + fi + + msg_custom "ℹ️" "${DGN}" "Container GIDs detected - video:${video_gid}, render:${render_gid}" + + # Prüfe ob die GIDs von den Defaults abweichen + local need_update=0 + if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then + need_update=1 + fi + + if [[ $need_update -eq 1 ]]; then + msg_custom "🔄" "${YW}" "Updating device GIDs in container config" + + # Stoppe Container für Config-Update + pct stop "$CTID" >/dev/null 2>&1 + + # Update die dev Einträge mit korrekten GIDs + # Backup der Config + cp "$LXC_CONFIG" "${LXC_CONFIG}.bak" + + # Parse und update jeden dev Eintrag + while IFS= read -r line; do + if [[ "$line" =~ ^dev[0-9]+: ]]; then + # Extract device path + local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/') + local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/') + + if [[ "$device_path" =~ renderD ]]; then + # RenderD device - use render GID + echo "${dev_num}: ${device_path},gid=${render_gid}" + elif [[ "$device_path" =~ card ]]; then + # Card device - use video GID + echo "${dev_num}: ${device_path},gid=${video_gid}" + else + # Keep original line + echo "$line" + fi + else + # Keep non-dev lines + echo "$line" + fi + done <"$LXC_CONFIG" >"${LXC_CONFIG}.new" + + mv "${LXC_CONFIG}.new" "$LXC_CONFIG" + + # Starte Container wieder + pct start "$CTID" >/dev/null 2>&1 + sleep 3 + + msg_ok "Device GIDs updated successfully" + else + msg_ok "Device GIDs are already correct" + fi + if [[ "$CT_TYPE" == "0" ]]; then + pct exec "$CTID" -- bash -c " + if [ -d /dev/dri ]; then + for dev in /dev/dri/*; do + if [ -e \"\$dev\" ]; then + if [[ \"\$dev\" =~ renderD ]]; then + chgrp ${render_gid} \"\$dev\" 2>/dev/null || true + else + chgrp ${video_gid} \"\$dev\" 2>/dev/null || true + fi + chmod 660 \"\$dev\" 2>/dev/null || true + fi + done + fi + " >/dev/null 2>&1 + fi +} + +check_storage_support() { + local CONTENT="$1" VALID=0 + while IFS= read -r line; do + local STORAGE_NAME + STORAGE_NAME=$(awk '{print $1}' <<<"$line") + [[ -n "$STORAGE_NAME" ]] && VALID=1 + done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') + [[ $VALID -eq 1 ]] +} + +select_storage() { + local CLASS=$1 CONTENT CONTENT_LABEL + case $CLASS in + container) + CONTENT='rootdir' + CONTENT_LABEL='Container' + ;; + template) + CONTENT='vztmpl' + CONTENT_LABEL='Container template' + ;; + iso) + CONTENT='iso' + CONTENT_LABEL='ISO image' + ;; + images) + CONTENT='images' + CONTENT_LABEL='VM Disk image' + ;; + backup) + CONTENT='backup' + CONTENT_LABEL='Backup' + ;; + snippets) + CONTENT='snippets' + CONTENT_LABEL='Snippets' + ;; + *) + msg_error "Invalid storage class '$CLASS'" + return 1 + ;; + esac + + declare -A STORAGE_MAP + local -a MENU=() + local COL_WIDTH=0 + + while read -r TAG TYPE _ TOTAL USED FREE _; do + [[ -n "$TAG" && -n "$TYPE" ]] || continue + local DISPLAY="${TAG} (${TYPE})" + local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED") + local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE") + local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" + STORAGE_MAP["$DISPLAY"]="$TAG" + MENU+=("$DISPLAY" "$INFO" "OFF") + ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} + done < <(pvesm status -content "$CONTENT" | awk 'NR>1') + + if [[ ${#MENU[@]} -eq 0 ]]; then + msg_error "No storage found for content type '$CONTENT'." + return 2 + fi + + if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then + STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" + STORAGE_INFO="${MENU[1]}" + return 0 + fi + + local WIDTH=$((COL_WIDTH + 42)) + while true; do + local DISPLAY_SELECTED + DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + --title "Storage Pools" \ + --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \ + 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; } + + DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED") + if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then + whiptail --msgbox "No valid storage selected. Please try again." 8 58 + continue + fi + STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}" + for ((i = 0; i < ${#MENU[@]}; i += 3)); do + if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then + STORAGE_INFO="${MENU[$i + 1]}" + break + fi + done + return 0 + done +} + +create_lxc_container() { + # ------------------------------------------------------------------------------ + # Optional verbose mode (debug tracing) + # ------------------------------------------------------------------------------ + if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi + + # ------------------------------------------------------------------------------ + # Helpers (dynamic versioning / template parsing) + # ------------------------------------------------------------------------------ + pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; } + pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; } + + ver_ge() { dpkg --compare-versions "$1" ge "$2"; } + ver_gt() { dpkg --compare-versions "$1" gt "$2"; } + ver_lt() { dpkg --compare-versions "$1" lt "$2"; } + + # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1" + parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; } + + # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create + # Returns: + # 0 = no upgrade needed + # 1 = upgraded (and if do_retry=yes and retry succeeded, creation done) + # 2 = user declined + # 3 = upgrade attempted but failed OR retry failed + offer_lxc_stack_upgrade_and_maybe_retry() { + local do_retry="${1:-no}" # yes|no + local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0 + + _pvec_i="$(pkg_ver pve-container)" + _lxcp_i="$(pkg_ver lxc-pve)" + _pvec_c="$(pkg_cand pve-container)" + _lxcp_c="$(pkg_cand lxc-pve)" + + if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then + ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1 + fi + if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then + ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1 + fi + if [[ $need -eq 0 ]]; then + msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)" + return 0 + fi + + echo + echo "An update for the Proxmox LXC stack is available:" + echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}" + echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}" + echo + read -rp "Do you want to upgrade now? [y/N] " _ans + case "${_ans,,}" in + y | yes) + msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)" + if apt-get update -qq >/dev/null && apt-get install -y --only-upgrade pve-container lxc-pve >/dev/null; then + msg_ok "LXC stack upgraded." + if [[ "$do_retry" == "yes" ]]; then + msg_info "Retrying container creation after upgrade" + if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then + msg_ok "Container created successfully after upgrade." + return 0 + else + msg_error "pct create still failed after upgrade. See $LOGFILE" + return 3 + fi + fi + return 1 + else + msg_error "Upgrade failed. Please check APT output." + return 3 + fi + ;; + *) return 2 ;; + esac + } + + # ------------------------------------------------------------------------------ + # Required input variables + # ------------------------------------------------------------------------------ + [[ "${CTID:-}" ]] || { + msg_error "You need to set 'CTID' variable." + exit 203 + } + [[ "${PCT_OSTYPE:-}" ]] || { + msg_error "You need to set 'PCT_OSTYPE' variable." + exit 204 + } + + msg_debug "CTID=$CTID" + msg_debug "PCT_OSTYPE=$PCT_OSTYPE" + msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" + + # ID checks + [[ "$CTID" -ge 100 ]] || { + msg_error "ID cannot be less than 100." + exit 205 + } + if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then + echo -e "ID '$CTID' is already in use." + unset CTID + msg_error "Cannot use ID that is already in use." + exit 206 + fi + + # Storage capability check + check_storage_support "rootdir" || { + msg_error "No valid storage found for 'rootdir' [Container]" + exit 1 + } + check_storage_support "vztmpl" || { + msg_error "No valid storage found for 'vztmpl' [Template]" + exit 1 + } + + # Template storage selection + if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then + TEMPLATE_STORAGE="$STORAGE_RESULT" + TEMPLATE_STORAGE_INFO="$STORAGE_INFO" + msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" + else + while true; do + if [[ -z "${var_template_storage:-}" ]]; then + if select_storage template; then + TEMPLATE_STORAGE="$STORAGE_RESULT" + TEMPLATE_STORAGE_INFO="$STORAGE_INFO" + msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" + break + fi + fi + done + fi + + # Container storage selection + if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then + CONTAINER_STORAGE="$STORAGE_RESULT" + CONTAINER_STORAGE_INFO="$STORAGE_INFO" + msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" + else + if [[ -z "${var_container_storage:-}" ]]; then + if select_storage container; then + CONTAINER_STORAGE="$STORAGE_RESULT" + CONTAINER_STORAGE_INFO="$STORAGE_INFO" + msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" + fi + fi + fi + + # Validate content types + msg_info "Validating content types of storage '$CONTAINER_STORAGE'" + STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs) + msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT" + grep -qw "rootdir" <<<"$STORAGE_CONTENT" || { + msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC." + exit 217 + } + $STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'" + + msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'" + TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs) + msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT" + if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then + msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail." + else + $STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'" + fi + + # Free space check + STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') + REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024)) + [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || { + msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G." + exit 214 + } + + # Cluster quorum (if cluster) + if [[ -f /etc/pve/corosync.conf ]]; then + msg_info "Checking cluster quorum" + if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then + msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)." + exit 210 + fi + msg_ok "Cluster is quorate" + fi + + # ------------------------------------------------------------------------------ + # Template discovery & validation + # ------------------------------------------------------------------------------ + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" + case "$PCT_OSTYPE" in + debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;; + alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;; + *) TEMPLATE_PATTERN="" ;; + esac + + msg_info "Searching for template '$TEMPLATE_SEARCH'" + + # Build regex patterns outside awk/grep for clarity + SEARCH_PATTERN="^${TEMPLATE_SEARCH}" + + #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" + #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" + #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" + + mapfile -t LOCAL_TEMPLATES < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + sed 's|.*/||' | sort -t - -k 2 -V + ) + + pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." + + 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 + ) + + if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then + echo "" + echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" + for i in "${!AVAILABLE_VERSIONS[@]}"; do + echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" + done + echo "" + read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice + + if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then + PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" + SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" + + #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" + + mapfile -t ONLINE_TEMPLATES < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk -F'\t' '{print $1}' | + grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | + sort -t - -k 2 -V 2>/dev/null || true + ) + + if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${ONLINE_TEMPLATES[-1]}" + TEMPLATE_SOURCE="online" + #echo "[DEBUG] Found alternative: $TEMPLATE" + else + msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" + exit 225 + fi + else + msg_custom "🚫" "${YW}" "Installation cancelled" + exit 0 + fi + else + msg_error "No ${PCT_OSTYPE} templates available at all" + exit 225 + fi + fi + + #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" + #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" + + TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" + if [[ -z "$TEMPLATE_PATH" ]]; then + TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) + [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" + fi + + # If we still don't have a path but have a valid template name, construct it + if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then + TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" + fi + + [[ -n "$TEMPLATE_PATH" ]] || { + if [[ -z "$TEMPLATE" ]]; then + msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" + + # Get available versions + mapfile -t AVAILABLE_VERSIONS < <( + pveam available -section system 2>/dev/null | + grep "^${PCT_OSTYPE}-" | + sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | + grep -E '^[0-9]+\.[0-9]+$' | + sort -u -V 2>/dev/null || sort -u + ) + + if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then + echo -e "\n${BL}Available versions:${CL}" + for i in "${!AVAILABLE_VERSIONS[@]}"; do + echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" + done + + echo "" + read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice + + if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then + export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" + export PCT_OSVERSION="$var_version" + msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" + + # Retry template search with new version + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" + SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" + + mapfile -t LOCAL_TEMPLATES < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + sed 's|.*/||' | sort -t - -k 2 -V + ) + mapfile -t ONLINE_TEMPLATES < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk -F'\t' '{print $1}' | + grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | + sort -t - -k 2 -V 2>/dev/null || true + ) + ONLINE_TEMPLATE="" + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + + if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${LOCAL_TEMPLATES[-1]}" + TEMPLATE_SOURCE="local" + else + TEMPLATE="$ONLINE_TEMPLATE" + TEMPLATE_SOURCE="online" + fi + + TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" + if [[ -z "$TEMPLATE_PATH" ]]; then + TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) + [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" + fi + + # If we still don't have a path but have a valid template name, construct it + if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then + TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" + fi + + [[ -n "$TEMPLATE_PATH" ]] || { + msg_error "Template still not found after version change" + exit 220 + } + else + msg_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 + + # Assemble pct options + PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}}) + [[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}") + + # Lock by template file (avoid concurrent downloads/creates) + lockfile="/tmp/template.${TEMPLATE}.lock" + exec 9>"$lockfile" || { + msg_error "Failed to create lock file '$lockfile'." + exit 200 + } + flock -w 60 9 || { + msg_error "Timeout while waiting for template lock." + exit 211 + } + + LOGFILE="/tmp/pct_create_${CTID}.log" + msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" + msg_debug "Logfile: $LOGFILE" + + # First attempt + if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then + msg_error "Container creation failed on ${TEMPLATE_STORAGE}. Checking template..." + + # Validate template file + if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then + msg_warn "Template file too small or missing – re-downloading." + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" + elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_warn "Template appears corrupted – re-downloading." + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" + else + msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." + fi + fi + + # Retry after repair + if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then + # Fallback to local storage + if [[ "$TEMPLATE_STORAGE" != "local" ]]; then + msg_warn "Retrying container creation with fallback to local storage..." + LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" + if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then + msg_info "Downloading template to local..." + pveam download local "$TEMPLATE" >/dev/null 2>&1 + fi + if pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then + msg_ok "Container successfully created using local fallback." + else + # --- Dynamic stack upgrade + auto-retry on the well-known error pattern --- + if grep -qiE 'unsupported .* version' "$LOGFILE"; then + echo + echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." + echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." + offer_lxc_stack_upgrade_and_maybe_retry "yes" + rc=$? + case $rc in + 0) : ;; # success - container created, continue + 2) + echo "Upgrade was declined. Please update and re-run: + apt update && apt install --only-upgrade pve-container lxc-pve" + exit 231 + ;; + 3) + echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" + exit 231 + ;; + esac + else + msg_error "Container creation failed even with local fallback. See $LOGFILE" + if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then + set -x + bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE" + set +x + fi + exit 209 + fi + fi + else + msg_error "Container creation failed on local storage. See $LOGFILE" + # --- Dynamic stack upgrade + auto-retry on the well-known error pattern --- + if grep -qiE 'unsupported .* version' "$LOGFILE"; then + echo + echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." + echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." + offer_lxc_stack_upgrade_and_maybe_retry "yes" + rc=$? + case $rc in + 0) : ;; # success - container created, continue + 2) + echo "Upgrade was declined. Please update and re-run: + apt update && apt install --only-upgrade pve-container lxc-pve" + exit 231 + ;; + 3) + echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" + exit 231 + ;; + esac + else + msg_error "Container creation failed. See $LOGFILE" + if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then + set -x + bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE" + set +x + fi + exit 209 + fi + fi + fi + fi + + # Verify container exists + pct list | awk '{print $1}' | grep -qx "$CTID" || { + msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE" + exit 215 + } + + # Verify config rootfs + grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || { + msg_error "RootFS entry missing in container config. See $LOGFILE" + exit 216 + } + + msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." +} + +# ============================================================================== +# SECTION 9: POST-INSTALLATION & FINALIZATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# description() +# +# - Sets container description with formatted HTML content +# - Includes: +# * Community-Scripts logo +# * Application name +# * Links to GitHub, Discussions, Issues +# * Ko-fi donation badge +# - Restarts ping-instances.service if present (monitoring) +# - Posts final "done" status to API telemetry +# ------------------------------------------------------------------------------ +description() { + IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) + + # Generate LXC Description + DESCRIPTION=$( + cat < + + Logo + + +

${APP} LXC

+ +

+ + spend Coffee + +

+ + + + GitHub + + + + Discussions + + + + Issues + + +EOF + ) + pct set "$CTID" -description "$DESCRIPTION" + + if [[ -f /etc/systemd/system/ping-instances.service ]]; then + systemctl start ping-instances.service + fi + + post_update_to_api "done" "none" +} + +# ============================================================================== +# SECTION 10: ERROR HANDLING & EXIT TRAPS +# ============================================================================== + +# ------------------------------------------------------------------------------ +# api_exit_script() +# +# - Exit trap handler for reporting to API telemetry +# - Captures exit code and reports to API using centralized error descriptions +# - Uses explain_exit_code() from error_handler.func for consistent error messages +# - Posts failure status with exit code to API (error description added automatically) +# - Only executes on non-zero exit codes +# ------------------------------------------------------------------------------ +api_exit_script() { + exit_code=$? + if [ $exit_code -ne 0 ]; then + post_update_to_api "failed" "$exit_code" + fi +} + +if command -v pveversion >/dev/null 2>&1; then + trap 'api_exit_script' EXIT +fi +trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR +trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT +trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM diff --git a/misc/build.func b/misc/build.func index d5dd0ef06..4ef714bd4 100644 --- a/misc/build.func +++ b/misc/build.func @@ -535,35 +535,6 @@ base_settings() { TAGS="community-script,${var_tags:-}" ENABLE_FUSE=${var_fuse:-"${1:-no}"} ENABLE_TUN=${var_tun:-"${1:-no}"} - ENABLE_NESTING=${var_nesting:-"${1:-1}"} - ENABLE_KEYCTL=${var_keyctl:-"${1:-0}"} - ALLOW_MOUNT_FS=${var_mount_fs:-""} - ENABLE_MKNOD=${var_mknod:-"${1:-0}"} - PROTECT_CT=${var_protection:-"${1:-no}"} - CT_TIMEZONE=${var_timezone:-""} - - # Normalize feature flags to 0/1 immediately (pct requires numeric values, not yes/no) - # This must happen here before any usage of these variables - case "${ENABLE_NESTING,,}" in - yes | true) ENABLE_NESTING="1" ;; - no | false) ENABLE_NESTING="0" ;; - esac - case "${ENABLE_KEYCTL,,}" in - yes | true) ENABLE_KEYCTL="1" ;; - no | false) ENABLE_KEYCTL="0" ;; - esac - case "${ENABLE_MKNOD,,}" in - yes | true) ENABLE_MKNOD="1" ;; - no | false) ENABLE_MKNOD="0" ;; - esac - case "${ENABLE_FUSE,,}" in - yes | true) ENABLE_FUSE="1" ;; - no | false) ENABLE_FUSE="0" ;; - esac - case "${PROTECT_CT,,}" in - yes | true) PROTECT_CT="1" ;; - no | false) PROTECT_CT="0" ;; - esac # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts if [ -z "$var_os" ]; then @@ -2298,35 +2269,19 @@ build_container() { none) ;; esac - # Build FEATURES array with advanced settings - # Note: All feature flags are already normalized to 0/1 in default_settings() - # Proxmox requires each feature as a separate parameter, not comma-separated string - FEATURES_ARRAY=() - FEATURES_ARRAY+=("nesting=${ENABLE_NESTING}") - - # keyctl: needed for Docker inside containers (systemd-networkd workaround) - # Typically needed for unprivileged containers with Docker - if [ "$CT_TYPE" == "1" ] || [ "$ENABLE_KEYCTL" == "1" ]; then - FEATURES_ARRAY+=("keyctl=1") + # Build FEATURES string (simple working version) + if [ "$CT_TYPE" == "1" ]; then + FEATURES="keyctl=1,nesting=1" + else + FEATURES="nesting=1" fi - # mknod: allow device node creation (requires kernel 5.3+, experimental) - if [ "$ENABLE_MKNOD" == "1" ]; then - FEATURES_ARRAY+=("mknod=1") + if [ "$ENABLE_FUSE" == "yes" ]; then + FEATURES="$FEATURES,fuse=1" fi - # FUSE: required for rclone, mergerfs, AppImage, etc. - if [ "$ENABLE_FUSE" == "1" ]; then - FEATURES_ARRAY+=("fuse=1") - fi - - # mount: allow specific filesystems (e.g., nfs, ext4, etc.) - # Format: mount=fstype1;fstype2;fstype3 (semicolon-separated, not comma!) - if [ -n "$ALLOW_MOUNT_FS" ]; then - # Replace commas with semicolons for proper pct syntax - ALLOW_MOUNT_FS_FORMATTED="${ALLOW_MOUNT_FS//,/;}" - FEATURES_ARRAY+=("mount=$ALLOW_MOUNT_FS_FORMATTED") - fi + # 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 @@ -2335,19 +2290,11 @@ build_container() { else export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" fi + + # Core exports for install.func export DIAGNOSTICS="$DIAGNOSTICS" export RANDOM_UUID="$RANDOM_UUID" export SESSION_ID="$SESSION_ID" - export BUILD_LOG="$BUILD_LOG" - export INSTALL_LOG="/root/.install-${SESSION_ID}.log" - export dev_mode="${dev_mode:-}" - export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}" - export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}" - export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}" - export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}" - export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}" - export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}" - export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" export CACHER="$APT_CACHER" export CACHER_IP="$APT_CACHER_IP" export tz="$timezone" @@ -2361,61 +2308,75 @@ build_container() { export CTTYPE="$CT_TYPE" export ENABLE_FUSE="$ENABLE_FUSE" export ENABLE_TUN="$ENABLE_TUN" - export ENABLE_NESTING="$ENABLE_NESTING" - export ENABLE_KEYCTL="$ENABLE_KEYCTL" - export ENABLE_MKNOD="$ENABLE_MKNOD" - export ALLOW_MOUNT_FS="$ALLOW_MOUNT_FS" - export PROTECT_CT="$PROTECT_CT" - export CT_TIMEZONE="$CT_TIMEZONE" export PCT_OSTYPE="$var_os" export PCT_OSVERSION="$var_version" export PCT_DISK_SIZE="$DISK_SIZE" - # Build PCT_OPTIONS array (not string) for proper parameter handling - PCT_OPTIONS=() + # 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}" - # Add features - each as separate -features parameter - for feature in "${FEATURES_ARRAY[@]}"; do - PCT_OPTIONS+=("-features" "$feature") - done - - PCT_OPTIONS+=("-hostname" "$HN") - PCT_OPTIONS+=("-tags" "$TAGS") + # 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+=($SD) # Storage device flags (already formatted) + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + $SD" fi + # Add nameserver if specified if [ -n "$NS" ]; then - PCT_OPTIONS+=($NS) # Nameserver flags (already formatted) + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + $NS" fi - # Network configuration (single string with all network parameters) - PCT_OPTIONS+=($NET_STRING) + # Network configuration + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + $NET_STRING + -onboot 1 + -cores $CORE_COUNT + -memory $RAM_SIZE + -unprivileged $CT_TYPE" - PCT_OPTIONS+=("-onboot" "1") - PCT_OPTIONS+=("-cores" "$CORE_COUNT") - PCT_OPTIONS+=("-memory" "$RAM_SIZE") - PCT_OPTIONS+=("-unprivileged" "$CT_TYPE") - - # Protection flag - if [ "$PROTECT_CT" == "1" ]; then - PCT_OPTIONS+=("-protection" "1") + # Protection flag (if var_protection was set) + if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + -protection 1" fi - # Timezone flag - if [ -n "$CT_TIMEZONE" ]; then - PCT_OPTIONS+=("-timezone" "$CT_TIMEZONE") + # Timezone flag (if var_timezone was set) + if [ -n "${CT_TIMEZONE:-}" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + -timezone $CT_TIMEZONE" fi - # Password flag (already formatted as "-password xxx") + # Password (already formatted) if [ -n "$PW" ]; then - PCT_OPTIONS+=($PW) + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING + $PW" fi - export PCT_OPTIONS + # 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" @@ -2557,7 +2518,7 @@ EOF if [[ $gpu_count -eq 1 ]]; then # Automatic selection for single GPU selected_gpu="${available_gpus[0]}" - msg_custom "⚙️" "${GN}" "Automatically configuring ${selected_gpu} GPU passthrough" + msg_ok "Automatically configuring ${selected_gpu} GPU passthrough" else # Multiple GPUs - ask user echo -e "\n${INFO} Multiple GPU types detected:" @@ -2934,7 +2895,7 @@ fix_gpu_gids() { return 0 fi - msg_custom "🔧" "${BL}" "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") @@ -3137,7 +3098,7 @@ create_lxc_container() { 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 + if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then msg_ok "Container created successfully after upgrade." return 0 else @@ -3563,9 +3524,12 @@ create_lxc_container() { grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid - # Assemble pct options - PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}}) - [[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}") + # 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" @@ -3579,11 +3543,22 @@ create_lxc_container() { } LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" - msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" + + # # 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 - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then + # 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 @@ -3602,7 +3577,7 @@ create_lxc_container() { fi # Retry after repair - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then + 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..." @@ -3611,7 +3586,7 @@ create_lxc_container() { 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 + 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 @@ -3635,7 +3610,7 @@ create_lxc_container() { msg_error "Container creation failed. See $LOGFILE" if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then set -x - pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" 2>&1 | tee -a "$LOGFILE" + pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" set +x fi exit 209 @@ -3667,7 +3642,7 @@ create_lxc_container() { msg_error "Container creation failed. See $LOGFILE" if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then set -x - pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" 2>&1 | tee -a "$LOGFILE" + pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" set +x fi exit 209 diff --git a/misc/install copy.func b/misc/install copy.func new file mode 100644 index 000000000..e9e319576 --- /dev/null +++ b/misc/install copy.func @@ -0,0 +1,277 @@ +# Copyright (c) 2021-2025 tteck +# Author: tteck (tteckster) +# Co-Author: MickLesk +# Co-Author: michelroegl-brunner +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE + +# ============================================================================== +# INSTALL.FUNC - CONTAINER INSTALLATION & SETUP +# ============================================================================== +# +# This file provides installation functions executed inside LXC containers +# after creation. Handles: +# +# - Network connectivity verification (IPv4/IPv6) +# - OS updates and package installation +# - DNS resolution checks +# - MOTD and SSH configuration +# - Container customization and auto-login +# +# Usage: +# - Sourced by -install.sh scripts +# - Executes via pct exec inside container +# - Requires internet connectivity +# +# ============================================================================== + +# ============================================================================== +# SECTION 1: INITIALIZATION +# ============================================================================== + +if ! command -v curl >/dev/null 2>&1; then + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + apt-get update >/dev/null 2>&1 + apt-get install -y curl >/dev/null 2>&1 +fi +source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func) +source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) +load_functions +catch_errors + +# ============================================================================== +# SECTION 2: NETWORK & CONNECTIVITY +# ============================================================================== + +# ------------------------------------------------------------------------------ +# verb_ip6() +# +# - Configures IPv6 based on DISABLEIPV6 variable +# - If DISABLEIPV6=yes: disables IPv6 via sysctl +# - Sets verbose mode via set_std_mode() +# ------------------------------------------------------------------------------ +verb_ip6() { + set_std_mode # Set STD mode based on VERBOSE + + if [ "$DISABLEIPV6" == "yes" ]; then + echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf + $STD sysctl -p + fi +} + +# ------------------------------------------------------------------------------ +# setting_up_container() +# +# - Verifies network connectivity via hostname -I +# - Retries up to RETRY_NUM times with RETRY_EVERY seconds delay +# - Removes Python EXTERNALLY-MANAGED restrictions +# - Disables systemd-networkd-wait-online.service for faster boot +# - Exits with error if network unavailable after retries +# ------------------------------------------------------------------------------ +setting_up_container() { + msg_info "Setting up Container OS" + for ((i = RETRY_NUM; i > 0; i--)); do + if [ "$(hostname -I)" != "" ]; then + break + fi + echo 1>&2 -en "${CROSS}${RD} No Network! " + sleep $RETRY_EVERY + done + if [ "$(hostname -I)" = "" ]; then + echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}" + echo -e "${NETWORK}Check Network Settings" + exit 1 + fi + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED + systemctl disable -q --now systemd-networkd-wait-online.service + msg_ok "Set up Container OS" + #msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)" + msg_ok "Network Connected: ${BL}$(hostname -I)" +} + +# ------------------------------------------------------------------------------ +# network_check() +# +# - Comprehensive network connectivity check for IPv4 and IPv6 +# - Tests connectivity to multiple DNS servers: +# * IPv4: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9) +# * IPv6: 2606:4700:4700::1111, 2001:4860:4860::8888, 2620:fe::fe +# - Verifies DNS resolution for GitHub and Community-Scripts domains +# - Prompts user to continue if no internet detected +# - Uses fatal() on DNS resolution failure for critical hosts +# ------------------------------------------------------------------------------ +network_check() { + set +e + trap - ERR + ipv4_connected=false + ipv6_connected=false + sleep 1 + + # Check IPv4 connectivity to Google, Cloudflare & Quad9 DNS servers. + if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then + msg_ok "IPv4 Internet Connected" + ipv4_connected=true + else + msg_error "IPv4 Internet Not Connected" + fi + + # Check IPv6 connectivity to Google, Cloudflare & Quad9 DNS servers. + if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null || ping6 -c 1 -W 1 2620:fe::fe &>/dev/null; then + msg_ok "IPv6 Internet Connected" + ipv6_connected=true + else + msg_error "IPv6 Internet Not Connected" + fi + + # If both IPv4 and IPv6 checks fail, prompt the user + if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then + read -r -p "No Internet detected, would you like to continue anyway? " prompt + 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 for GitHub-related domains (IPv4 and/or IPv6) + GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com" "git.community-scripts.org") + GIT_STATUS="Git DNS:" + DNS_FAILED=false + + for HOST in "${GIT_HOSTS[@]}"; do + RESOLVEDIP=$(getent hosts "$HOST" | awk '{ print $1 }' | grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^[a-fA-F0-9:]+$)' | head -n1) + if [[ -z "$RESOLVEDIP" ]]; then + GIT_STATUS+="$HOST:($DNSFAIL)" + DNS_FAILED=true + else + GIT_STATUS+=" $HOST:($DNSOK)" + 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 +} + +# ============================================================================== +# SECTION 3: OS UPDATE & PACKAGE MANAGEMENT +# ============================================================================== + +# ------------------------------------------------------------------------------ +# update_os() +# +# - Updates container OS via apt-get update and dist-upgrade +# - Configures APT cacher proxy if CACHER=yes (accelerates package downloads) +# - Removes Python EXTERNALLY-MANAGED restrictions for pip +# - Sources tools.func for additional setup functions after update +# - Uses $STD wrapper to suppress output unless VERBOSE=yes +# ------------------------------------------------------------------------------ +update_os() { + msg_info "Updating Container OS" + if [[ "$CACHER" == "yes" ]]; then + echo "Acquire::http::Proxy-Auto-Detect \"/usr/local/bin/apt-proxy-detect.sh\";" >/etc/apt/apt.conf.d/00aptproxy + cat <<'EOF' >/usr/local/bin/apt-proxy-detect.sh +#!/bin/bash +if nc -w1 -z "${CACHER_IP}" 3142; then + echo -n "http://${CACHER_IP}:3142" +else + echo -n "DIRECT" +fi +EOF + chmod +x /usr/local/bin/apt-proxy-detect.sh + fi + $STD apt-get update + $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED + msg_ok "Updated Container OS" + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func) +} + +# ============================================================================== +# SECTION 4: MOTD & SSH CONFIGURATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# motd_ssh() +# +# - Configures Message of the Day (MOTD) with container information +# - Creates /etc/profile.d/00_lxc-details.sh with: +# * Application name +# * Warning banner (DEV repository) +# * OS name and version +# * Hostname and IP address +# * GitHub repository link +# - Disables executable flag on /etc/update-motd.d/* scripts +# - Enables root SSH access if SSH_ROOT=yes +# - Configures TERM environment variable for better terminal support +# ------------------------------------------------------------------------------ +motd_ssh() { + grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc + + 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 '"') + elif [ -f "/etc/debian_version" ]; then + OS_NAME="Debian" + OS_VERSION=$(cat /etc/debian_version) + fi + + PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" + echo "echo -e \"\"" >"$PROFILE_FILE" + echo -e "echo -e \"${BOLD}${YW}${APPLICATION} LXC Container - DEV Repository${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${RD}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVED${CL}\"" >>"$PROFILE_FILE" + echo "echo \"\"" >>"$PROFILE_FILE" + + chmod -x /etc/update-motd.d/* + + if [[ "${SSH_ROOT}" == "yes" ]]; then + sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config + systemctl restart sshd + fi +} + +# ============================================================================== +# SECTION 5: CONTAINER CUSTOMIZATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# customize() +# +# - Customizes container for passwordless root login if PASSWORD is empty +# - Configures getty for auto-login via /etc/systemd/system/container-getty@1.service.d/override.conf +# - Creates /usr/bin/update script for easy application updates +# - Injects SSH authorized keys if SSH_AUTHORIZED_KEY variable is set +# - Sets proper permissions on SSH directories and key files +# ------------------------------------------------------------------------------ +customize() { + if [[ "$PASSWORD" == "" ]]; then + msg_info "Customizing Container" + GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf" + mkdir -p $(dirname $GETTY_OVERRIDE) + cat <$GETTY_OVERRIDE + [Service] + ExecStart= + ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM +EOF + systemctl daemon-reload + systemctl restart $(basename $(dirname $GETTY_OVERRIDE) | sed 's/\.d//') + msg_ok "Customized Container" + fi + echo "bash -c \"\$(curl -fsSL https://github.com/community-scripts/ProxmoxVED/raw/main/ct/${app}.sh)\"" >/usr/bin/update + chmod +x /usr/bin/update + 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 +} diff --git a/misc/install.func b/misc/install.func index 97bd19b8c..7c3f601d4 100644 --- a/misc/install.func +++ b/misc/install.func @@ -28,30 +28,16 @@ # SECTION 1: INITIALIZATION # ============================================================================== -# Ensure INSTALL_LOG is set (exported from build.func, but fallback if missing) -if [[ -z "${INSTALL_LOG:-}" ]]; then - INSTALL_LOG="/root/.install-${SESSION_ID:-unknown}.log" -fi - -# Dev mode: Persistent logs directory -if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then - mkdir -p /var/log/community-scripts - INSTALL_LOG="/var/log/community-scripts/install-${SESSION_ID:-unknown}-$(date +%Y%m%d_%H%M%S).log" -fi - if ! command -v curl >/dev/null 2>&1; then - printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 - apt-get update >/dev/null 2>&1 - apt-get install -y curl >/dev/null 2>&1 + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + apt-get update >/dev/null 2>&1 + apt-get install -y curl >/dev/null 2>&1 fi source <(curl -fsSL https://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 -# Re-parse dev_mode in container context (flags exported from host) -parse_dev_mode - # ============================================================================== # SECTION 2: NETWORK & CONNECTIVITY # ============================================================================== @@ -64,12 +50,12 @@ parse_dev_mode # - Sets verbose mode via set_std_mode() # ------------------------------------------------------------------------------ verb_ip6() { - set_std_mode # Set STD mode based on VERBOSE + set_std_mode # Set STD mode based on VERBOSE - if [ "$DISABLEIPV6" == "yes" ]; then - echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf - $STD sysctl -p - fi + if [ "$DISABLEIPV6" == "yes" ]; then + echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf + $STD sysctl -p + fi } # ------------------------------------------------------------------------------ @@ -82,24 +68,24 @@ verb_ip6() { # - Exits with error if network unavailable after retries # ------------------------------------------------------------------------------ setting_up_container() { - msg_info "Setting up Container OS" - for ((i = RETRY_NUM; i > 0; i--)); do - if [ "$(hostname -I)" != "" ]; then - break - fi - echo 1>&2 -en "${CROSS}${RD} No Network! " - sleep $RETRY_EVERY - done - if [ "$(hostname -I)" = "" ]; then - echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}" - echo -e "${NETWORK}Check Network Settings" - exit 1 + msg_info "Setting up Container OS" + for ((i = RETRY_NUM; i > 0; i--)); do + if [ "$(hostname -I)" != "" ]; then + break fi - rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED - systemctl disable -q --now systemd-networkd-wait-online.service - msg_ok "Set up Container OS" - #msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)" - msg_ok "Network Connected: ${BL}$(hostname -I)" + echo 1>&2 -en "${CROSS}${RD} No Network! " + sleep $RETRY_EVERY + done + if [ "$(hostname -I)" = "" ]; then + echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}" + echo -e "${NETWORK}Check Network Settings" + exit 1 + fi + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED + systemctl disable -q --now systemd-networkd-wait-online.service + msg_ok "Set up Container OS" + #msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)" + msg_ok "Network Connected: ${BL}$(hostname -I)" } # ------------------------------------------------------------------------------ @@ -114,65 +100,62 @@ setting_up_container() { # - Uses fatal() on DNS resolution failure for critical hosts # ------------------------------------------------------------------------------ network_check() { - set +e - trap - ERR - ipv4_connected=false - ipv6_connected=false - sleep 1 + set +e + trap - ERR + ipv4_connected=false + ipv6_connected=false + sleep 1 - # Check IPv4 connectivity to Google, Cloudflare & Quad9 DNS servers. - if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then - ipv4_connected=true - ipv4_status="${GN}✔${CL} IPv4" + # Check IPv4 connectivity to Google, Cloudflare & Quad9 DNS servers. + if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then + msg_ok "IPv4 Internet Connected" + ipv4_connected=true + else + msg_error "IPv4 Internet Not Connected" + fi + + # Check IPv6 connectivity to Google, Cloudflare & Quad9 DNS servers. + if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null || ping6 -c 1 -W 1 2620:fe::fe &>/dev/null; then + msg_ok "IPv6 Internet Connected" + ipv6_connected=true + else + msg_error "IPv6 Internet Not Connected" + fi + + # If both IPv4 and IPv6 checks fail, prompt the user + if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then + read -r -p "No Internet detected, would you like to continue anyway? " prompt + if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then + echo -e "${INFO}${RD}Expect Issues Without Internet${CL}" else - ipv4_status="${RD}✖${CL} IPv4" + echo -e "${NETWORK}Check Network Settings" + exit 1 fi + fi - # Check IPv6 connectivity to Google, Cloudflare & Quad9 DNS servers. - if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null || ping6 -c 1 -W 1 2620:fe::fe &>/dev/null; then - ipv6_connected=true - ipv6_status="${GN}✔${CL} IPv6" + # DNS resolution checks for GitHub-related domains (IPv4 and/or IPv6) + GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com" "git.community-scripts.org") + GIT_STATUS="Git DNS:" + DNS_FAILED=false + + for HOST in "${GIT_HOSTS[@]}"; do + RESOLVEDIP=$(getent hosts "$HOST" | awk '{ print $1 }' | grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^[a-fA-F0-9:]+$)' | head -n1) + if [[ -z "$RESOLVEDIP" ]]; then + GIT_STATUS+="$HOST:($DNSFAIL)" + DNS_FAILED=true else - ipv6_status="${RD}✖${CL} IPv6" + GIT_STATUS+=" $HOST:($DNSOK)" fi + done - # Show combined status - msg_ok "Internet: ${ipv4_status} ${ipv6_status}" + if [[ "$DNS_FAILED" == true ]]; then + fatal "$GIT_STATUS" + else + msg_ok "$GIT_STATUS" + fi - # If both IPv4 and IPv6 checks fail, prompt the user - if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then - read -r -p "No Internet detected, would you like to continue anyway? " prompt - 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 for GitHub-related domains (IPv4 and/or IPv6) - GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com" "git.community-scripts.org") - GIT_STATUS="Git DNS:" - DNS_FAILED=false - - for HOST in "${GIT_HOSTS[@]}"; do - RESOLVEDIP=$(getent hosts "$HOST" | awk '{ print $1 }' | grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^[a-fA-F0-9:]+$)' | head -n1) - if [[ -z "$RESOLVEDIP" ]]; then - GIT_STATUS+="$HOST:($DNSFAIL)" - DNS_FAILED=true - else - GIT_STATUS+=" $HOST:($DNSOK)" - 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 } # ============================================================================== @@ -189,10 +172,10 @@ network_check() { # - Uses $STD wrapper to suppress output unless VERBOSE=yes # ------------------------------------------------------------------------------ update_os() { - msg_info "Updating Container OS" - if [[ "$CACHER" == "yes" ]]; then - echo "Acquire::http::Proxy-Auto-Detect \"/usr/local/bin/apt-proxy-detect.sh\";" >/etc/apt/apt.conf.d/00aptproxy - cat <<'EOF' >/usr/local/bin/apt-proxy-detect.sh + msg_info "Updating Container OS" + if [[ "$CACHER" == "yes" ]]; then + echo "Acquire::http::Proxy-Auto-Detect \"/usr/local/bin/apt-proxy-detect.sh\";" >/etc/apt/apt.conf.d/00aptproxy + cat <<'EOF' >/usr/local/bin/apt-proxy-detect.sh #!/bin/bash if nc -w1 -z "${CACHER_IP}" 3142; then echo -n "http://${CACHER_IP}:3142" @@ -200,13 +183,13 @@ else echo -n "DIRECT" fi EOF - chmod +x /usr/local/bin/apt-proxy-detect.sh - fi - $STD apt-get update - $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade - rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED - msg_ok "Updated Container OS" - source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func) + chmod +x /usr/local/bin/apt-proxy-detect.sh + fi + $STD apt-get update + $STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade + rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED + msg_ok "Updated Container OS" + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func) } # ============================================================================== @@ -228,32 +211,32 @@ EOF # - Configures TERM environment variable for better terminal support # ------------------------------------------------------------------------------ motd_ssh() { - grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc + grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc - 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 '"') - elif [ -f "/etc/debian_version" ]; then - OS_NAME="Debian" - OS_VERSION=$(cat /etc/debian_version) - 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 '"') + elif [ -f "/etc/debian_version" ]; then + OS_NAME="Debian" + OS_VERSION=$(cat /etc/debian_version) + fi - PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" - echo "echo -e \"\"" >"$PROFILE_FILE" - echo -e "echo -e \"${BOLD}${YW}${APPLICATION} LXC Container - DEV Repository${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${RD}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVED${CL}\"" >>"$PROFILE_FILE" - echo "echo \"\"" >>"$PROFILE_FILE" + PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" + echo "echo -e \"\"" >"$PROFILE_FILE" + echo -e "echo -e \"${BOLD}${YW}${APPLICATION} LXC Container - DEV Repository${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${RD}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${YW} Repository: ${GN}https://github.com/community-scripts/ProxmoxVED${CL}\"" >>"$PROFILE_FILE" + echo "echo \"\"" >>"$PROFILE_FILE" - chmod -x /etc/update-motd.d/* + chmod -x /etc/update-motd.d/* - if [[ "${SSH_ROOT}" == "yes" ]]; then - sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config - systemctl restart sshd - fi + if [[ "${SSH_ROOT}" == "yes" ]]; then + sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config + systemctl restart sshd + fi } # ============================================================================== @@ -270,24 +253,25 @@ motd_ssh() { # - Sets proper permissions on SSH directories and key files # ------------------------------------------------------------------------------ customize() { - if [[ "$PASSWORD" == "" ]]; then - msg_info "Customizing Container" - GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf" - mkdir -p $(dirname $GETTY_OVERRIDE) - cat <$GETTY_OVERRIDE - [Service] - ExecStart= - ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM + if [[ "$PASSWORD" == "" ]]; then + msg_info "Customizing Container" + GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf" + mkdir -p $(dirname $GETTY_OVERRIDE) + cat <<'EOF' >$GETTY_OVERRIDE +[Service] +ExecStart= +ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 $TERM EOF - $STD systemctl daemon-reload || true - msg_ok "Customized Container" - fi - echo "bash -c \"\$(curl -fsSL https://github.com/community-scripts/ProxmoxVED/raw/main/ct/${app}.sh)\"" >/usr/bin/update - chmod +x /usr/bin/update - 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 + systemctl daemon-reload + systemctl restart $(basename $(dirname $GETTY_OVERRIDE) | sed 's/\.d//') + msg_ok "Customized Container" + fi + echo "bash -c \"\$(curl -fsSL https://github.com/community-scripts/ProxmoxVED/raw/main/ct/${app}.sh)\"" >/usr/bin/update + chmod +x /usr/bin/update + 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 } diff --git a/vm/docker-vm.sh b/vm/docker-vm.sh index c7f2b285c..68dc581e2 100644 --- a/vm/docker-vm.sh +++ b/vm/docker-vm.sh @@ -29,7 +29,7 @@ var_os="debian" var_version="13" DISK_SIZE="10G" USE_CLOUD_INIT="no" -INSTALL_PORTAINER="no" +# INSTALL_PORTAINER="no" OS_TYPE="" OS_VERSION="" @@ -284,16 +284,16 @@ function select_cloud_init() { fi } -function select_portainer() { - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "PORTAINER" \ - --yesno "Install Portainer for Docker management?\n\nPortainer is a lightweight management UI for Docker.\n\nAccess after installation:\n• HTTP: http://:9000\n• HTTPS: https://:9443" 14 68); then - INSTALL_PORTAINER="yes" - echo -e "${ADVANCED}${BOLD}${DGN}Portainer: ${BGN}yes${CL}" - else - INSTALL_PORTAINER="no" - echo -e "${ADVANCED}${BOLD}${DGN}Portainer: ${BGN}no${CL}" - fi -} +# function select_portainer() { +# if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "PORTAINER" \ +# --yesno "Install Portainer for Docker management?\n\nPortainer is a lightweight management UI for Docker.\n\nAccess after installation:\n• HTTP: http://:9000\n• HTTPS: https://:9443" 14 68); then +# INSTALL_PORTAINER="yes" +# echo -e "${ADVANCED}${BOLD}${DGN}Portainer: ${BGN}yes${CL}" +# else +# INSTALL_PORTAINER="no" +# echo -e "${ADVANCED}${BOLD}${DGN}Portainer: ${BGN}no${CL}" +# fi +# } function get_image_url() { local arch=$(dpkg --print-architecture) @@ -323,7 +323,7 @@ function default_settings() { select_cloud_init # Portainer Selection - ALWAYS ask - select_portainer + # select_portainer # Set defaults for other settings VMID=$(get_valid_nextid) @@ -367,7 +367,7 @@ function advanced_settings() { select_cloud_init # Portainer Selection - ALWAYS ask (at the beginning) - select_portainer + # select_portainer METHOD="advanced" [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid) @@ -703,7 +703,7 @@ for i in {1..10}; do done # Install Portainer if requested -INSTALL_PORTAINER_PLACEHOLDER +# INSTALL_PORTAINER_PLACEHOLDER # Create completion flag echo \"[\\$(date)] Docker installation completed successfully\" @@ -711,20 +711,20 @@ touch /root/.docker-installed INSTALLEOF" >/dev/null # Add Portainer installation script if requested -if [ "$INSTALL_PORTAINER" = "yes" ]; then - virt-customize -q -a "${FILE}" --run-command "cat > /root/install-portainer.sh << 'PORTAINEREOF' -#!/bin/bash -exec >> /var/log/install-docker.log 2>&1 -echo \"[\\$(date)] Installing Portainer\" -docker volume create portainer_data -docker run -d -p 9000:9000 -p 9443:9443 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest -echo \"[\\$(date)] Portainer installed and started\" -PORTAINEREOF" >/dev/null - virt-customize -q -a "${FILE}" --run-command "chmod +x /root/install-portainer.sh" >/dev/null - virt-customize -q -a "${FILE}" --run-command "sed -i 's|INSTALL_PORTAINER_PLACEHOLDER|/root/install-portainer.sh|' /root/install-docker.sh" >/dev/null -else - virt-customize -q -a "${FILE}" --run-command "sed -i 's|INSTALL_PORTAINER_PLACEHOLDER|echo \"[\\\\\\$(date)] Skipping Portainer installation\"|' /root/install-docker.sh" >/dev/null -fi +# if [ "$INSTALL_PORTAINER" = "yes" ]; then +# virt-customize -q -a "${FILE}" --run-command "cat > /root/install-portainer.sh << 'PORTAINEREOF' +# #!/bin/bash +# exec >> /var/log/install-docker.log 2>&1 +# echo \"[\\$(date)] Installing Portainer\" +# docker volume create portainer_data +# docker run -d -p 9000:9000 -p 9443:9443 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest +# echo \"[\\$(date)] Portainer installed and started\" +# PORTAINEREOF" >/dev/null +# virt-customize -q -a "${FILE}" --run-command "chmod +x /root/install-portainer.sh" >/dev/null +# virt-customize -q -a "${FILE}" --run-command "sed -i 's|INSTALL_PORTAINER_PLACEHOLDER|/root/install-portainer.sh|' /root/install-docker.sh" >/dev/null +# else +# virt-customize -q -a "${FILE}" --run-command "sed -i 's|INSTALL_PORTAINER_PLACEHOLDER|echo \"[\\\\\\$(date)] Skipping Portainer installation\"|' /root/install-docker.sh" >/dev/null +# fi virt-customize -q -a "${FILE}" --run-command "chmod +x /root/install-docker.sh" >/dev/null @@ -899,15 +899,15 @@ else echo -e "${TAB}${DGN}Docker: ${BGN}Latest (via get.docker.com)${CL}" fi -if [ "$INSTALL_PORTAINER" = "yes" ]; then - if [ -n "$VM_IP" ]; then - echo -e "${TAB}${DGN}Portainer: ${BGN}https://${VM_IP}:9443${CL}" - else - echo -e "${TAB}${DGN}Portainer: ${BGN}Will be accessible at https://:9443${CL}" - echo -e "${TAB}${YW}⚠️ Wait 2-3 minutes after boot for installation to complete${CL}" - echo -e "${TAB}${YW}⚠️ Get IP with: ${BL}qm guest cmd ${VMID} network-get-interfaces${CL}" - fi -fi +# if [ "$INSTALL_PORTAINER" = "yes" ]; then +# if [ -n "$VM_IP" ]; then +# echo -e "${TAB}${DGN}Portainer: ${BGN}https://${VM_IP}:9443${CL}" +# else +# echo -e "${TAB}${DGN}Portainer: ${BGN}Will be accessible at https://:9443${CL}" +# echo -e "${TAB}${YW}⚠️ Wait 2-3 minutes after boot for installation to complete${CL}" +# echo -e "${TAB}${YW}⚠️ Get IP with: ${BL}qm guest cmd ${VMID} network-get-interfaces${CL}" +# fi +# fi if [ "$USE_CLOUD_INIT" = "yes" ]; then display_cloud_init_info "$VMID" "$HN" fi