mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-11-01 00:52:34 +01:00
Compare commits
15
Commits
No files matched your search
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Generated
+1
-13
@@ -77,17 +77,6 @@
|
||||
"excludeGlobs": []
|
||||
}
|
||||
],
|
||||
"core": [
|
||||
{
|
||||
"fileStatus": null,
|
||||
"includeGlobs": [
|
||||
"misc/*.func"
|
||||
],
|
||||
"excludeGlobs": [
|
||||
"misc/api.func"
|
||||
]
|
||||
}
|
||||
],
|
||||
"documentation": [
|
||||
{
|
||||
"fileStatus": null,
|
||||
@@ -114,8 +103,7 @@
|
||||
{
|
||||
"fileStatus": null,
|
||||
"includeGlobs": [
|
||||
"api/**",
|
||||
"misc/api.func"
|
||||
"api/**"
|
||||
],
|
||||
"excludeGlobs": []
|
||||
}
|
||||
|
||||
+2
-3
@@ -267,15 +267,14 @@ jobs:
|
||||
const stubPath = path.join('ct', slug + '.sh');
|
||||
const content = [
|
||||
"#!/usr/bin/env bash",
|
||||
"source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)",
|
||||
"source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/core/main/core/core.func)",
|
||||
"# Copyright (c) 2021-2026 community-scripts ORG",
|
||||
"# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE",
|
||||
"",
|
||||
`APP="${appName.replace(/"/g, '\\"')}"`,
|
||||
"",
|
||||
'header_info "$APP"',
|
||||
'variables',
|
||||
'color',
|
||||
'header_info',
|
||||
"",
|
||||
'msg_error "This script is no longer available in community-scripts."',
|
||||
`msg_error "${deletedMessage.replace(/"/g, '\\"')}"`,
|
||||
|
||||
Generated
+2
-2
@@ -184,8 +184,8 @@ jobs:
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
'Only the script itself comes from this branch. Whatever it loads, the engine',
|
||||
'or the `misc/` helpers, still comes from `main`.',
|
||||
'Only the script itself comes from this branch. The engine it loads still',
|
||||
'comes from community-scripts/core on `main`.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -5,8 +5,8 @@ name: Sync ct/install to Incus
|
||||
#
|
||||
# The actual mirroring + transformation lives in community-scripts/Incus
|
||||
# (.github/workflows/sync-scripts.yml): it rewrites each script's bootstrap line
|
||||
# to load the engine from community-scripts/core (ProxmoxVE's misc/build.func is
|
||||
# a monolith with no Incus backend), so a plain 1:1 copy would break every Incus
|
||||
# to load the engine from community-scripts/core (ct scripts here load its PVE
|
||||
# backend; Incus needs the Incus one), so a plain 1:1 copy would break every Incus
|
||||
# script. We therefore only fire a repository_dispatch and let that single
|
||||
# source of truth do the work (and auto-approve + merge its own PR).
|
||||
#
|
||||
|
||||
+26
-1
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<a href="#">
|
||||
<img src="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo.png" height="100px" />
|
||||
<img src="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/.github/assets/logo.png" height="100px" />
|
||||
</a>
|
||||
</div>
|
||||
<h1 align="center">Changelog</h1>
|
||||
@@ -568,6 +568,31 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-10-31
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Jellyseerr, Overseerr, Paperless-ngx, Vikunja: skip migration prompts when unattended [@florianpunz](https://github.com/florianpunz) ([#18321](https://github.com/community-scripts/ProxmoxVE/pull/18321))
|
||||
- Discourse: track the Ruby version Discourse requires [@MickLesk](https://github.com/MickLesk) ([#18329](https://github.com/community-scripts/ProxmoxVE/pull/18329))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- MatterJS-Server: Add time sync [@tomfrenzel](https://github.com/tomfrenzel) ([#18330](https://github.com/community-scripts/ProxmoxVE/pull/18330))
|
||||
|
||||
### 📂 Github
|
||||
|
||||
- Drop the legacy engine copies from misc/ [@MickLesk](https://github.com/MickLesk) ([#18294](https://github.com/community-scripts/ProxmoxVE/pull/18294))
|
||||
|
||||
## 2026-10-30
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Reconya ([#18325](https://github.com/community-scripts/ProxmoxVE/pull/18325))
|
||||
- Blinko ([#18324](https://github.com/community-scripts/ProxmoxVE/pull/18324))
|
||||
- ChartDB ([#18323](https://github.com/community-scripts/ProxmoxVE/pull/18323))
|
||||
|
||||
## 2026-10-29
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<a href="misc/images/community-scripts.svg"><img src="misc/images/community-scripts.png" width="112" height="112" alt="Community Scripts — Proxmox VE logo" /></a>
|
||||
<!-- Editable artwork: misc/images/community-scripts.svg; PNG export: 512 × 512. -->
|
||||
<a href=".github/assets/community-scripts.svg"><img src=".github/assets/community-scripts.png" width="112" height="112" alt="Community Scripts — Proxmox VE logo" /></a>
|
||||
<!-- Editable artwork: .github/assets/community-scripts.svg; PNG export: 512 × 512. -->
|
||||
|
||||
<h1>Community Scripts — Proxmox VE</h1>
|
||||
<p><strong>One-command installations for services, containers and VMs on Proxmox VE</strong><br/>
|
||||
@@ -104,7 +104,6 @@ Virtual machine scripts need the same versions as containers.
|
||||
| `vm/` | Virtual machine scripts |
|
||||
| `tools/pve/` | Host tooling: post-install, hardware passthrough, maintenance |
|
||||
| `turnkey/` | TurnKey Linux appliance helper |
|
||||
| `misc/` | Legacy engine copies — current scripts load [core](https://github.com/community-scripts/core) at runtime, so changes here never reach a running install |
|
||||
|
||||
Application metadata — name, description, logo, tags — is managed on the website,
|
||||
not in this repository. Use the **Report Issue** link on any script page to
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/blinkospace/blinko
|
||||
|
||||
APP="Blinko"
|
||||
var_tags="${var_tags:-notes;ai;knowledge}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-6144}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
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/blinko ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "blinko" "blinkospace/blinko"; then
|
||||
msg_info "Stopping Blinko"
|
||||
systemctl stop blinko
|
||||
msg_ok "Stopped Blinko"
|
||||
|
||||
NODE_VERSION="24" NODE_MODULE="bun" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "blinko" "blinkospace/blinko" "tarball"
|
||||
|
||||
msg_info "Building Blinko"
|
||||
cd /opt/blinko
|
||||
$STD bun install
|
||||
$STD bunx prisma generate
|
||||
$STD bun run build:web
|
||||
$STD bun run build:seed
|
||||
mv dist/public server/public
|
||||
cp -r server/lute.min.js server/vditor dist/
|
||||
ln -s /opt/blinko_data/storage /opt/blinko/.blinko
|
||||
msg_ok "Built Blinko"
|
||||
|
||||
msg_info "Starting Blinko"
|
||||
systemctl start blinko
|
||||
msg_ok "Started Blinko"
|
||||
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 "${GATEWAY}${BGN}http://${IP}:1111${CL}"
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/chartdb/chartdb
|
||||
|
||||
APP="ChartDB"
|
||||
var_tags="${var_tags:-database;diagram;dev-tools}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-6144}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
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/chartdb ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "chartdb" "chartdb/chartdb"; then
|
||||
msg_info "Stopping ChartDB"
|
||||
systemctl stop nginx
|
||||
msg_ok "Stopped ChartDB"
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "chartdb" "chartdb/chartdb" "tarball"
|
||||
|
||||
msg_info "Building ChartDB"
|
||||
cd /opt/chartdb
|
||||
$STD npm ci
|
||||
NODE_OPTIONS="--max-old-space-size=4096" $STD npm run build
|
||||
rm -rf /opt/chartdb/node_modules
|
||||
msg_ok "Built ChartDB"
|
||||
|
||||
msg_info "Starting ChartDB"
|
||||
systemctl start nginx
|
||||
msg_ok "Started ChartDB"
|
||||
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 "${GATEWAY}${BGN}https://${IP}${CL}"
|
||||
+17
-2
@@ -51,16 +51,31 @@ function update_script() {
|
||||
cd /opt/discourse
|
||||
export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
export BUNDLE_SILENCE_ROOT_WARNING=1
|
||||
set -a
|
||||
source /opt/discourse/.env
|
||||
set +a
|
||||
$STD git pull origin main
|
||||
$STD bundle install
|
||||
msg_ok "Updated Discourse"
|
||||
|
||||
msg_info "Updating Dependencies"
|
||||
RUBY_TARGET=$(grep -A1 '^RUBY VERSION' /opt/discourse/Gemfile.lock | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
|
||||
RUBY_VERSION="${RUBY_TARGET:-4.0.7}" RUBY_INSTALL_RAILS="false" setup_ruby
|
||||
export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
|
||||
hash -r
|
||||
cd /opt/discourse
|
||||
$STD bundle install -j"$(nproc)"
|
||||
$STD pnpm install
|
||||
msg_ok "Updated Dependencies"
|
||||
|
||||
msg_info "Running Database Migrations"
|
||||
$STD runuser -u postgres -- psql -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||
$STD bundle exec rails db:migrate
|
||||
msg_ok "Ran Database Migrations"
|
||||
|
||||
msg_info "Precompiling Assets"
|
||||
$STD bundle exec rails assets:precompile
|
||||
msg_ok "Updated Discourse"
|
||||
msg_ok "Precompiled Assets"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start discourse discourse-sidekiq
|
||||
|
||||
@@ -39,6 +39,10 @@ function update_script() {
|
||||
echo "Seerr is the new unified Jellyseerr and Overseerr."
|
||||
echo "More info: https://docs.seerr.dev/blog/seerr-release"
|
||||
echo
|
||||
if [[ "${PHS_SILENT:-0}" == "1" ]]; then
|
||||
msg_warn "Seerr migration detected - skipping update, requires an interactive run"
|
||||
exit 75
|
||||
fi
|
||||
read -rp "Do you want to migrate to Seerr now? (y/N): " MIGRATE
|
||||
echo
|
||||
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
||||
|
||||
@@ -33,6 +33,15 @@ function update_script() {
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
|
||||
SERVICE_FILE="/etc/systemd/system/matterjs-server.service"
|
||||
if [[ -f "$SERVICE_FILE" ]] && ! grep -Fxq 'Environment=ENABLE_TIME_SYNC=true' "$SERVICE_FILE"; then
|
||||
msg_info "Enabling MatterJS Time Sync"
|
||||
sed -i '/^ExecStart=/i Environment=ENABLE_TIME_SYNC=true' "$SERVICE_FILE"
|
||||
$STD systemctl daemon-reload
|
||||
$STD systemctl restart matterjs-server
|
||||
msg_ok "Enabled MatterJS Time Sync"
|
||||
fi
|
||||
|
||||
CURRENT=$(cat /opt/matter-server/node_modules/matter-server/package.json | grep '"version"' | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
|
||||
LATEST=$(npm view matter-server version 2>/dev/null)
|
||||
if [[ $CURRENT != "$LATEST" ]]; then
|
||||
|
||||
@@ -38,6 +38,10 @@ function update_script() {
|
||||
echo "Seerr is the new unified Jellyseerr and Overseerr."
|
||||
echo "More info: https://docs.seerr.dev/blog/seerr-release"
|
||||
echo
|
||||
if [[ "${PHS_SILENT:-0}" == "1" ]]; then
|
||||
msg_warn "Seerr migration detected - skipping update, requires an interactive run"
|
||||
exit 75
|
||||
fi
|
||||
read -rp "Do you want to migrate to Seerr now? (y/N): " MIGRATE
|
||||
echo
|
||||
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
||||
|
||||
@@ -63,6 +63,10 @@ function update_script() {
|
||||
echo -e "${GATEWAY}${BGN}https://docs.paperless-ngx.com/migration-v3/#encryption-support${CL}"
|
||||
msg_warn "Before continuing make sure that you do not use encryption or have decrypted all documents."
|
||||
echo ""
|
||||
if [[ "${PHS_SILENT:-0}" == "1" ]]; then
|
||||
msg_warn "Paperless-ngx v3 migration detected - skipping update, requires an interactive run"
|
||||
exit 75
|
||||
fi
|
||||
read -rp "Do you want to continue with the update? (y/N): " MIGRATE
|
||||
echo
|
||||
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
||||
@@ -71,6 +75,11 @@ function update_script() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${PHS_SILENT:-0}" == "1" ]] && ! grep -q "uv run" /etc/systemd/system/paperless-webserver.service; then
|
||||
msg_warn "Paperless-ngx uv migration detected - skipping update, requires an interactive run"
|
||||
exit 75
|
||||
fi
|
||||
|
||||
msg_info "Stopping all Paperless-ngx Services"
|
||||
systemctl stop paperless-consumer paperless-webserver paperless-scheduler paperless-task-queue
|
||||
msg_ok "Stopped all Paperless-ngx Services"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/Dyneteq/reconya
|
||||
|
||||
APP="Reconya"
|
||||
var_tags="${var_tags:-network;discovery;security}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
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/reconya ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "reconya" "Dyneteq/reconya"; then
|
||||
msg_info "Stopping Reconya"
|
||||
systemctl stop reconya
|
||||
msg_ok "Stopped Reconya"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "reconya" "Dyneteq/reconya" "prebuild" "latest" "/opt/reconya" "reconya-linux-$(arch_resolve "amd64" "arm64").tar.gz"
|
||||
|
||||
msg_info "Starting Reconya"
|
||||
systemctl start reconya
|
||||
msg_ok "Started Reconya"
|
||||
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 "${GATEWAY}${BGN}http://${IP}:3008${CL}"
|
||||
+9
-1
@@ -36,6 +36,10 @@ function update_script() {
|
||||
msg_warn "This requires MANUAL config changes in /etc/vikunja/config.yml."
|
||||
msg_warn "See: https://vikunja.io/changelog/whats-new-in-vikunja-1.0.0/#config-changes"
|
||||
|
||||
if [[ "${PHS_SILENT:-0}" == "1" ]]; then
|
||||
msg_warn "Vikunja 1.0 config migration detected - skipping update, requires an interactive run"
|
||||
exit 75
|
||||
fi
|
||||
read -rp "Continue with update? (y to proceed): " -t 30 CONFIRM1 || exit 254
|
||||
[[ "$CONFIRM1" =~ ^[yY]$ ]] || exit 0
|
||||
|
||||
@@ -60,7 +64,11 @@ function update_script() {
|
||||
echo -e "${TAB}${YW}How do you want to handle /etc/vikunja/config.yml?${CL}"
|
||||
echo -e "${TAB} 1) Keep your current config"
|
||||
echo -e "${TAB} 2) Install the new package maintainer's config"
|
||||
read -rp " Choose [1/2] (default: 1): " -t 60 CONFIG_CHOICE || CONFIG_CHOICE="1"
|
||||
if [[ "${PHS_SILENT:-0}" == "1" ]]; then
|
||||
CONFIG_CHOICE="1"
|
||||
else
|
||||
read -rp " Choose [1/2] (default: 1): " -t 60 CONFIG_CHOICE || CONFIG_CHOICE="1"
|
||||
fi
|
||||
[[ -z "$CONFIG_CHOICE" ]] && CONFIG_CHOICE="1"
|
||||
|
||||
if [[ "$CONFIG_CHOICE" == "2" ]]; then
|
||||
|
||||
@@ -57,7 +57,7 @@ service:
|
||||
url: community-scripts/ProxmoxVE
|
||||
use_prerelease: false
|
||||
dashboard:
|
||||
icon: https://raw.githubusercontent.com/community-scripts/ProxmoxVE/refs/heads/main/misc/images/logo.png
|
||||
icon: https://raw.githubusercontent.com/community-scripts/ProxmoxVE/refs/heads/main/.github/assets/logo.png
|
||||
icon_link_to: https://community-scripts.org/
|
||||
web_url: https://github.com/community-scripts/ProxmoxVE/releases
|
||||
EOF
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/blinkospace/blinko
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
NODE_VERSION="24" NODE_MODULE="bun" setup_nodejs
|
||||
PG_VERSION="17" setup_postgresql
|
||||
PG_DB_NAME="blinko" PG_DB_USER="blinko" setup_postgresql_db
|
||||
|
||||
fetch_and_deploy_gh_release "blinko" "blinkospace/blinko" "tarball"
|
||||
|
||||
msg_info "Building Blinko"
|
||||
cd /opt/blinko
|
||||
$STD bun install
|
||||
$STD bunx prisma generate
|
||||
$STD bun run build:web
|
||||
$STD bun run build:seed
|
||||
# The server resolves its web root as ../server/public and lute/vditor next to its own bundle
|
||||
mv dist/public server/public
|
||||
cp -r server/lute.min.js server/vditor dist/
|
||||
msg_ok "Built Blinko"
|
||||
|
||||
msg_info "Configuring Blinko"
|
||||
mkdir -p /opt/blinko_data/storage
|
||||
# Blinko keeps uploads, the vector index and backups in .blinko under its working directory
|
||||
ln -s /opt/blinko_data/storage /opt/blinko/.blinko
|
||||
cat <<EOF >/opt/blinko_data/.env
|
||||
NODE_ENV=production
|
||||
DATABASE_URL=postgresql://blinko:${PG_DB_PASS}@localhost:5432/blinko
|
||||
TRUST_PROXY=1
|
||||
EOF
|
||||
chmod 600 /opt/blinko_data/.env
|
||||
msg_ok "Configured Blinko"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/blinko.service
|
||||
[Unit]
|
||||
Description=Blinko
|
||||
After=network.target postgresql.service
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/blinko
|
||||
EnvironmentFile=/opt/blinko_data/.env
|
||||
ExecStartPre=/opt/blinko/node_modules/.bin/prisma migrate deploy
|
||||
ExecStartPre=/usr/bin/node /opt/blinko/dist/seed.js
|
||||
ExecStart=/usr/bin/node /opt/blinko/dist/index.js
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now blinko
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/chartdb/chartdb
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y nginx
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
fetch_and_deploy_gh_release "chartdb" "chartdb/chartdb" "tarball"
|
||||
|
||||
msg_info "Building ChartDB"
|
||||
cd /opt/chartdb
|
||||
$STD npm ci
|
||||
NODE_OPTIONS="--max-old-space-size=4096" $STD npm run build
|
||||
rm -rf /opt/chartdb/node_modules
|
||||
msg_ok "Built ChartDB"
|
||||
|
||||
msg_info "Configuring ChartDB"
|
||||
mkdir -p /opt/chartdb_data
|
||||
cat <<'EOF' >/opt/chartdb_data/config.js
|
||||
window.env = {
|
||||
DISABLE_ANALYTICS: "true",
|
||||
// OPENAI_API_KEY: "",
|
||||
// OPENAI_API_ENDPOINT: "https://ollama.example.com/v1",
|
||||
// LLM_MODEL_NAME: "qwen2.5-coder:7b",
|
||||
};
|
||||
EOF
|
||||
create_self_signed_cert "chartdb"
|
||||
# HTTPS because the AI export (crypto.subtle) and the copy buttons need a secure context
|
||||
cat <<'EOF' >/etc/nginx/sites-available/chartdb
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
http2 on;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/ssl/chartdb/chartdb.crt;
|
||||
ssl_certificate_key /etc/ssl/chartdb/chartdb.key;
|
||||
|
||||
root /opt/chartdb/dist;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /config.js {
|
||||
alias /opt/chartdb_data/config.js;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
}
|
||||
EOF
|
||||
nginx_enable_site "chartdb"
|
||||
msg_ok "Configured ChartDB"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -28,9 +28,14 @@ $STD apt install -y \
|
||||
redis-server
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
msg_info "Cloning Discourse"
|
||||
$STD git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse
|
||||
msg_ok "Cloned Discourse"
|
||||
|
||||
PG_VERSION="17" PG_MODULES="pgvector" setup_postgresql
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
RUBY_VERSION="3.4.4" setup_ruby
|
||||
RUBY_TARGET=$(grep -A1 '^RUBY VERSION' /opt/discourse/Gemfile.lock | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
|
||||
RUBY_VERSION="${RUBY_TARGET:-4.0.7}" RUBY_INSTALL_RAILS="false" setup_ruby
|
||||
|
||||
msg_info "Configuring PostgreSQL for Discourse"
|
||||
DISCOURSE_DB_PASS=$(random_password 13)
|
||||
@@ -42,7 +47,6 @@ msg_ok "Configured PostgreSQL for Discourse"
|
||||
|
||||
msg_info "Configuring Discourse"
|
||||
DISCOURSE_SECRET_KEY=$(openssl rand -hex 64)
|
||||
$STD git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse
|
||||
cd /opt/discourse
|
||||
cat <<EOF >/opt/discourse/.env
|
||||
RAILS_ENV=production
|
||||
@@ -77,6 +81,7 @@ export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
|
||||
eval "$(rbenv init - bash)" 2>/dev/null || true
|
||||
export RAILS_ENV=production
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
export BUNDLE_SILENCE_ROOT_WARNING=1
|
||||
$STD corepack enable
|
||||
$STD bundle config set --local deployment true
|
||||
$STD bundle config set --local without 'test development'
|
||||
|
||||
@@ -43,6 +43,7 @@ After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Environment=ENABLE_TIME_SYNC=true
|
||||
ExecStart=/usr/bin/node /opt/matter-server/node_modules/matter-server/dist/esm/MatterServer.js --storage-path /var/lib/matterjs-server
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/Dyneteq/reconya
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
fetch_and_deploy_gh_release "reconya" "Dyneteq/reconya" "prebuild" "latest" "/opt/reconya" "reconya-linux-$(arch_resolve "amd64" "arm64").tar.gz"
|
||||
|
||||
msg_info "Configuring Reconya"
|
||||
mkdir -p /opt/reconya_data
|
||||
cat <<EOF >/opt/reconya_data/.env
|
||||
PORT=3008
|
||||
LOGIN_USERNAME=admin
|
||||
LOGIN_PASSWORD=$(random_password)
|
||||
DATABASE_NAME=reconya
|
||||
SQLITE_PATH=/opt/reconya_data/reconya.db
|
||||
PUBLIC_IP_LOOKUP_ENABLED=false
|
||||
GEOLOCATION_ENABLED=false
|
||||
VENDOR_LOOKUP_ONLINE_ENABLED=false
|
||||
OUI_DOWNLOAD_ENABLED=false
|
||||
EOF
|
||||
chmod 600 /opt/reconya_data/.env
|
||||
msg_ok "Configured Reconya"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/reconya.service
|
||||
[Unit]
|
||||
Description=Reconya
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/reconya_data
|
||||
EnvironmentFile=/opt/reconya_data/.env
|
||||
ExecStart=/opt/reconya/reconya-linux-$(arch_resolve "amd64" "arm64")
|
||||
# Reconya ignores SIGTERM and only shuts down on SIGINT
|
||||
KillSignal=SIGINT
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now reconya
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -1,275 +0,0 @@
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: tteck (tteckster)
|
||||
# Co-Author: MickLesk
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
apk update && apk add curl >/dev/null 2>&1
|
||||
fi
|
||||
# Mark container context BEFORE error handling starts: error_handler/on_exit
|
||||
# must write local failure artifacts instead of talking to the telemetry API
|
||||
# (the host is the single telemetry reporter).
|
||||
export TELEMETRY_CONTEXT="container"
|
||||
# A freshly booted container may not have DNS up yet. `source <(curl ...)` hides
|
||||
# curl's exit code, so a failed download would silently source an empty stream.
|
||||
_bootstrap_die() {
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "$1"
|
||||
else
|
||||
echo "FATAL: $1" >&2
|
||||
fi
|
||||
exit 115
|
||||
}
|
||||
|
||||
_bootstrap_source() {
|
||||
local url="$1" probe="$2" content
|
||||
content=$(curl -fsSL --connect-timeout 10 --retry 5 --retry-connrefused --retry-delay 2 "$url") ||
|
||||
_bootstrap_die "Failed to download ${url##*/}"
|
||||
source /dev/stdin <<<"$content"
|
||||
declare -f "$probe" >/dev/null 2>&1 ||
|
||||
_bootstrap_die "${url##*/} loaded but incomplete (missing ${probe})"
|
||||
}
|
||||
|
||||
_FUNC_BASE="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc"
|
||||
_bootstrap_source "$_FUNC_BASE/core.func" load_functions
|
||||
_bootstrap_source "$_FUNC_BASE/error_handler.func" catch_errors
|
||||
load_functions
|
||||
catch_errors
|
||||
|
||||
# Persist diagnostics setting inside container (exported from build.func)
|
||||
# so addon scripts running later can find the user's choice
|
||||
if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then
|
||||
mkdir -p /usr/local/community-scripts
|
||||
echo "DIAGNOSTICS=${DIAGNOSTICS:-no}" >/usr/local/community-scripts/diagnostics
|
||||
fi
|
||||
|
||||
# Get LXC IP address (must be called INSIDE container, after network is up)
|
||||
get_lxc_ip
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# post_progress_to_api()
|
||||
#
|
||||
# - Lightweight progress ping from inside the container
|
||||
# - Updates the existing telemetry record status
|
||||
# - Arguments:
|
||||
# * $1: status (optional, default: "configuring")
|
||||
# - Signals that the installation is actively progressing (not stuck)
|
||||
# - Fire-and-forget: never blocks or fails the script
|
||||
# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
|
||||
# ------------------------------------------------------------------------------
|
||||
post_progress_to_api() {
|
||||
command -v curl &>/dev/null || return 0
|
||||
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
||||
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
||||
|
||||
local progress_status="${1:-configuring}"
|
||||
|
||||
curl -fsS -m 5 -X POST "https://telemetry.community-scripts.org/telemetry" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"lxc\",\"nsapp\":\"${app:-unknown}\",\"status\":\"${progress_status}\",\"platform\":\"${TELEMETRY_PLATFORM:-}\",\"repo_source\":\"${REPO_SOURCE:-}\",\"repo_slug\":\"${REPO_SLUG:-}\"}" &>/dev/null || true
|
||||
}
|
||||
|
||||
# This function enables IPv6 if it's not disabled and sets verbose mode
|
||||
verb_ip6() {
|
||||
set_std_mode # Set STD mode based on VERBOSE
|
||||
|
||||
if [ "${IPV6_METHOD:-}" = "disable" ]; then
|
||||
msg_info "Disabling IPv6 (this may affect some services)"
|
||||
$STD sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
||||
$STD sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
||||
$STD sysctl -w net.ipv6.conf.lo.disable_ipv6=1
|
||||
mkdir -p /etc/sysctl.d
|
||||
$STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null <<EOF
|
||||
net.ipv6.conf.all.disable_ipv6 = 1
|
||||
net.ipv6.conf.default.disable_ipv6 = 1
|
||||
net.ipv6.conf.lo.disable_ipv6 = 1
|
||||
EOF
|
||||
$STD rc-update add sysctl default
|
||||
msg_ok "Disabled IPv6"
|
||||
fi
|
||||
}
|
||||
|
||||
# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
|
||||
setting_up_container() {
|
||||
msg_info "Setting up Container OS"
|
||||
while [ $i -gt 0 ]; do
|
||||
if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" != "" ]; then
|
||||
break
|
||||
fi
|
||||
echo 1>&2 -en "${CROSS}${RD} No Network! "
|
||||
sleep $RETRY_EVERY
|
||||
i=$((i - 1))
|
||||
done
|
||||
|
||||
if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then
|
||||
echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}"
|
||||
echo -e "${NETWORK}Check Network Settings"
|
||||
exit 121
|
||||
fi
|
||||
msg_ok "Set up Container OS"
|
||||
msg_ok "Network Connected: ${BL}$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | tail -n1)${CL}"
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
|
||||
network_check() {
|
||||
set +e
|
||||
trap - ERR
|
||||
ipv4_connected=false
|
||||
|
||||
# Check IPv4 connectivity to Cloudflare, Google & 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
|
||||
|
||||
if [[ $ipv4_connected == false ]]; then
|
||||
read -r -p "No Internet detected, would you like to continue anyway? <y/N> " prompt </dev/tty
|
||||
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||
echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
|
||||
else
|
||||
echo -e "${NETWORK}Check Network Settings"
|
||||
exit 122
|
||||
fi
|
||||
fi
|
||||
|
||||
# DNS resolution checks for GitHub-related domains
|
||||
GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com")
|
||||
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
|
||||
}
|
||||
|
||||
# This function updates the Container OS by running apk upgrade with mirror fallback
|
||||
update_os() {
|
||||
msg_info "Updating Container OS"
|
||||
configure_http_proxy
|
||||
if ! $STD apk -U upgrade; then
|
||||
msg_warn "apk update failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
|
||||
local alpine_mirrors="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
|
||||
local apk_ok=false
|
||||
for m in $(printf '%s\n' $alpine_mirrors | shuf); do
|
||||
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
|
||||
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${m}"
|
||||
cat <<EOF >/etc/apk/repositories
|
||||
http://$m/alpine/latest-stable/main
|
||||
http://$m/alpine/latest-stable/community
|
||||
EOF
|
||||
if $STD apk -U upgrade; then
|
||||
msg_ok "CDN set to ${m}: tests passed"
|
||||
apk_ok=true
|
||||
break
|
||||
else
|
||||
msg_warn "Mirror ${m} failed"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [[ "$apk_ok" != true ]]; then
|
||||
msg_error "All Alpine mirrors failed. Check network or try again later."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
local tools_content
|
||||
tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) || {
|
||||
msg_error "Failed to download tools.func"
|
||||
exit 115
|
||||
}
|
||||
source /dev/stdin <<<"$tools_content"
|
||||
if ! declare -f fetch_and_deploy_gh_release >/dev/null 2>&1; then
|
||||
msg_error "tools.func loaded but incomplete — missing expected functions"
|
||||
exit 115
|
||||
fi
|
||||
msg_ok "Updated Container OS"
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# This function modifies the message of the day (motd) and SSH settings
|
||||
motd_ssh() {
|
||||
echo "export TERM='xterm-256color'" >>/root/.bashrc
|
||||
|
||||
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
|
||||
|
||||
echo "[ -t 1 ] || return 0" >"$PROFILE_FILE"
|
||||
echo "echo -e \"\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
|
||||
echo "echo \"\"" >>"$PROFILE_FILE"
|
||||
cat <<EOF >>"$PROFILE_FILE"
|
||||
os_display="Unknown OS"
|
||||
if [ -r /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
os_display="\${PRETTY_NAME:-\${NAME:-Unknown OS}}"
|
||||
fi
|
||||
echo -e "${TAB}${OS}${YW} OS: ${GN}\${os_display}${CL}"
|
||||
EOF
|
||||
echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(ip -4 addr show eth0 | awk '/inet / {print \$2}' | cut -d/ -f1 | head -n 1)${CL}\"" >>"$PROFILE_FILE"
|
||||
|
||||
# Configure SSH if enabled
|
||||
if [[ "${SSH_ROOT}" == "yes" ]]; then
|
||||
# Enable sshd service
|
||||
$STD rc-update add sshd
|
||||
# Allow root login via SSH
|
||||
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
|
||||
# Start the sshd service
|
||||
$STD /etc/init.d/sshd start
|
||||
fi
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# Validate Timezone for some LXC's
|
||||
validate_tz() {
|
||||
[[ -f "/usr/share/zoneinfo/$1" ]]
|
||||
}
|
||||
|
||||
# This function customizes the container and enables passwordless login for the root user
|
||||
customize() {
|
||||
if [[ "$PASSWORD" == "" ]]; then
|
||||
msg_info "Customizing Container"
|
||||
passwd -d root >/dev/null 2>&1
|
||||
|
||||
# Ensure agetty is available
|
||||
apk add --no-cache --force-broken-world util-linux >/dev/null 2>&1
|
||||
|
||||
# Create persistent autologin boot script
|
||||
mkdir -p /etc/local.d
|
||||
cat <<'EOF' >/etc/local.d/autologin.start
|
||||
#!/bin/sh
|
||||
sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab
|
||||
kill -HUP 1
|
||||
EOF
|
||||
touch /root/.hushlogin
|
||||
|
||||
chmod +x /etc/local.d/autologin.start
|
||||
rc-update add local >/dev/null 2>&1
|
||||
|
||||
# Apply autologin immediately for current session
|
||||
/etc/local.d/autologin.start
|
||||
|
||||
msg_ok "Customized Container"
|
||||
fi
|
||||
|
||||
echo 'set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/'"${app}"'.sh)"' >/usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
post_progress_to_api
|
||||
}
|
||||
@@ -1,515 +0,0 @@
|
||||
#!/bin/ash
|
||||
# shellcheck shell=ash
|
||||
|
||||
# Expects existing msg_* functions and optional $STD from the framework.
|
||||
|
||||
# ------------------------------
|
||||
# helpers
|
||||
# ------------------------------
|
||||
lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
||||
has() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
need_tool() {
|
||||
# usage: need_tool curl jq unzip ...
|
||||
# setup missing tools via apk
|
||||
local missing=0 t
|
||||
for t in "$@"; do
|
||||
if ! has "$t"; then missing=1; fi
|
||||
done
|
||||
if [ "$missing" -eq 1 ]; then
|
||||
msg_info "Installing tools: $*"
|
||||
apk add --no-cache "$@" >/dev/null 2>&1 || {
|
||||
msg_error "apk add failed for: $*"
|
||||
return 100
|
||||
}
|
||||
msg_ok "Tools ready: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
net_resolves() {
|
||||
# better handling for missing getent on Alpine
|
||||
# usage: net_resolves api.github.com
|
||||
local host="$1"
|
||||
ping -c1 -W1 "$host" >/dev/null 2>&1 || nslookup "$host" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ensure_usr_local_bin_persist() {
|
||||
# Login shells: /etc/profile.d/
|
||||
local PROFILE_FILE="/etc/profile.d/10-localbin.sh"
|
||||
if [ ! -f "$PROFILE_FILE" ]; then
|
||||
echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE"
|
||||
chmod +x "$PROFILE_FILE"
|
||||
fi
|
||||
|
||||
# Non-login shells (pct enter): /root/.profile and /root/.bashrc
|
||||
for rc_file in /root/.profile /root/.bashrc; do
|
||||
if [ -f "$rc_file" ] && ! grep -q '/usr/local/bin' "$rc_file"; then
|
||||
echo 'export PATH="/usr/local/bin:$PATH"' >>"$rc_file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
download_with_progress() {
|
||||
# $1 url, $2 dest
|
||||
local url="$1" out="$2" cl
|
||||
need_tool curl pv || return 127
|
||||
cl=$(curl -fsSLI "$url" 2>/dev/null | awk 'tolower($0) ~ /^content-length:/ {print $2}' | tr -d '\r')
|
||||
if [ -n "$cl" ]; then
|
||||
curl -fsSL "$url" | pv -s "$cl" >"$out" || {
|
||||
msg_error "Download failed: $url"
|
||||
return 250
|
||||
}
|
||||
else
|
||||
curl -fL# -o "$out" "$url" || {
|
||||
msg_error "Download failed: $url"
|
||||
return 250
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# GitHub: check Release
|
||||
# ------------------------------
|
||||
check_for_gh_release() {
|
||||
# app, repo, [pinned]
|
||||
local app="$1" source="$2" pinned="${3:-}"
|
||||
local app_lc
|
||||
app_lc="$(lower "$app" | tr -d ' ')"
|
||||
local current_file="$HOME/.${app_lc}"
|
||||
local current="" release tag
|
||||
|
||||
msg_info "Check for update: $app"
|
||||
|
||||
net_resolves api.github.com || {
|
||||
msg_error "DNS/network error: api.github.com"
|
||||
return 6
|
||||
}
|
||||
need_tool curl jq || return 127
|
||||
|
||||
tag=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | jq -r '.tag_name // empty')
|
||||
[ -z "$tag" ] && {
|
||||
msg_error "Unable to fetch latest tag for $app"
|
||||
return 22
|
||||
}
|
||||
release="${tag#v}"
|
||||
|
||||
[ -f "$current_file" ] && current="$(cat "$current_file")"
|
||||
|
||||
if [ -n "$pinned" ]; then
|
||||
if [ "$pinned" = "$release" ]; then
|
||||
msg_ok "$app pinned to v$pinned (no update)"
|
||||
return 1
|
||||
fi
|
||||
if [ "$current" = "$pinned" ]; then
|
||||
msg_ok "$app pinned v$pinned installed (upstream v$release)"
|
||||
return 1
|
||||
fi
|
||||
msg_info "$app pinned v$pinned (upstream v$release) → update/downgrade"
|
||||
CHECK_UPDATE_RELEASE="$pinned"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$release" != "$current" ] || [ ! -f "$current_file" ]; then
|
||||
CHECK_UPDATE_RELEASE="$release"
|
||||
msg_info "New release available: v$release (current: v${current:-none})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_ok "$app is up to date (v$release)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# GitHub: get Release & deploy (Alpine)
|
||||
# modes: tarball | prebuild | singlefile
|
||||
# ------------------------------
|
||||
fetch_and_deploy_gh() {
|
||||
# $1 app, $2 repo, [$3 mode], [$4 version], [$5 target], [$6 asset_pattern
|
||||
local app="$1" repo="$2" mode="${3:-tarball}" version="${4:-latest}" target="${5:-/opt/$1}" pattern="${6:-}"
|
||||
local app_lc
|
||||
app_lc="$(lower "$app" | tr -d ' ')"
|
||||
local vfile="$HOME/.${app_lc}"
|
||||
local json url filename tmpd unpack
|
||||
|
||||
net_resolves api.github.com || {
|
||||
msg_error "DNS/network error"
|
||||
return 6
|
||||
}
|
||||
need_tool curl jq tar || return 127
|
||||
[ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true
|
||||
|
||||
tmpd="$(mktemp -d)" || return 252
|
||||
mkdir -p "$target"
|
||||
|
||||
# Release JSON
|
||||
if [ "$version" = "latest" ]; then
|
||||
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")" || {
|
||||
msg_error "GitHub API failed"
|
||||
rm -rf "$tmpd"
|
||||
return 22
|
||||
}
|
||||
else
|
||||
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || {
|
||||
msg_error "GitHub API failed"
|
||||
rm -rf "$tmpd"
|
||||
return 22
|
||||
}
|
||||
fi
|
||||
|
||||
# correct Version
|
||||
version="$(printf '%s' "$json" | jq -r '.tag_name // empty')"
|
||||
version="${version#v}"
|
||||
|
||||
[ -z "$version" ] && {
|
||||
msg_error "No tag in release json"
|
||||
rm -rf "$tmpd"
|
||||
return 65
|
||||
}
|
||||
|
||||
case "$mode" in
|
||||
tarball | source)
|
||||
url="$(printf '%s' "$json" | jq -r '.tarball_url // empty')"
|
||||
[ -z "$url" ] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz"
|
||||
filename="${app_lc}-${version}.tar.gz"
|
||||
download_with_progress "$url" "$tmpd/$filename" || {
|
||||
rm -rf "$tmpd"
|
||||
return 250
|
||||
}
|
||||
tar -xzf "$tmpd/$filename" -C "$tmpd" || {
|
||||
msg_error "tar extract failed"
|
||||
rm -rf "$tmpd"
|
||||
return 251
|
||||
}
|
||||
unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)"
|
||||
# copy content of unpack to target
|
||||
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||
msg_error "copy failed"
|
||||
rm -rf "$tmpd"
|
||||
return 252
|
||||
}
|
||||
;;
|
||||
prebuild)
|
||||
[ -n "$pattern" ] || {
|
||||
msg_error "prebuild requires asset pattern"
|
||||
rm -rf "$tmpd"
|
||||
return 65
|
||||
}
|
||||
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
|
||||
BEGIN{IGNORECASE=1}
|
||||
$0 ~ p {print; exit}
|
||||
')"
|
||||
[ -z "$url" ] && {
|
||||
msg_error "asset not found for pattern: $pattern"
|
||||
rm -rf "$tmpd"
|
||||
return 250
|
||||
}
|
||||
filename="${url##*/}"
|
||||
download_with_progress "$url" "$tmpd/$filename" || {
|
||||
rm -rf "$tmpd"
|
||||
return 250
|
||||
}
|
||||
# unpack archive (Zip or tarball)
|
||||
case "$filename" in
|
||||
*.zip)
|
||||
need_tool unzip || {
|
||||
rm -rf "$tmpd"
|
||||
return 127
|
||||
}
|
||||
mkdir -p "$tmpd/unp"
|
||||
unzip -q "$tmpd/$filename" -d "$tmpd/unp"
|
||||
;;
|
||||
*.tar.gz | *.tgz | *.tar.xz | *.tar.zst | *.tar.bz2)
|
||||
mkdir -p "$tmpd/unp"
|
||||
tar -xf "$tmpd/$filename" -C "$tmpd/unp"
|
||||
;;
|
||||
*)
|
||||
msg_error "unsupported archive: $filename"
|
||||
rm -rf "$tmpd"
|
||||
return 251
|
||||
;;
|
||||
esac
|
||||
# top-level folder strippen
|
||||
if [ "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 1 ] && [ -z "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type f | head -n1)" ]; then
|
||||
unpack="$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d)"
|
||||
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||
msg_error "copy failed"
|
||||
rm -rf "$tmpd"
|
||||
return 252
|
||||
}
|
||||
else
|
||||
(cd "$tmpd/unp" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||
msg_error "copy failed"
|
||||
rm -rf "$tmpd"
|
||||
return 252
|
||||
}
|
||||
fi
|
||||
;;
|
||||
singlefile)
|
||||
[ -n "$pattern" ] || {
|
||||
msg_error "singlefile requires asset pattern"
|
||||
rm -rf "$tmpd"
|
||||
return 65
|
||||
}
|
||||
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
|
||||
BEGIN{IGNORECASE=1}
|
||||
$0 ~ p {print; exit}
|
||||
')"
|
||||
[ -z "$url" ] && {
|
||||
msg_error "asset not found for pattern: $pattern"
|
||||
rm -rf "$tmpd"
|
||||
return 250
|
||||
}
|
||||
filename="${url##*/}"
|
||||
download_with_progress "$url" "$target/$app" || {
|
||||
rm -rf "$tmpd"
|
||||
return 250
|
||||
}
|
||||
chmod +x "$target/$app"
|
||||
;;
|
||||
*)
|
||||
msg_error "Unknown mode: $mode"
|
||||
rm -rf "$tmpd"
|
||||
return 65
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$version" >"$vfile"
|
||||
ensure_usr_local_bin_persist
|
||||
rm -rf "$tmpd"
|
||||
msg_ok "Deployed $app ($version) → $target"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# yq (mikefarah) – Alpine
|
||||
# ------------------------------
|
||||
setup_yq() {
|
||||
# prefer apk, unless FORCE_GH=1
|
||||
if [ "${FORCE_GH:-0}" != "1" ] && apk info -e yq >/dev/null 2>&1; then
|
||||
msg_info "Updating yq via apk"
|
||||
apk add --no-cache --upgrade yq >/dev/null 2>&1 || true
|
||||
msg_ok "yq ready ($(yq --version 2>/dev/null))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
need_tool curl || return 127
|
||||
local arch bin url tmp
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="amd64" ;;
|
||||
aarch64) arch="arm64" ;;
|
||||
*)
|
||||
msg_error "Unsupported arch for yq: $(uname -m)"
|
||||
return 238
|
||||
;;
|
||||
esac
|
||||
url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}"
|
||||
tmp="$(mktemp)"
|
||||
download_with_progress "$url" "$tmp" || return 250
|
||||
/usr/bin/install -m 0755 "$tmp" /usr/local/bin/yq
|
||||
rm -f "$tmp"
|
||||
msg_ok "Setup yq ($(yq --version 2>/dev/null))"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# Adminer – Alpine
|
||||
# ------------------------------
|
||||
setup_adminer() {
|
||||
need_tool curl || return 127
|
||||
msg_info "Setup Adminer (Alpine)"
|
||||
mkdir -p /var/www/localhost/htdocs/adminer
|
||||
curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \
|
||||
-o /var/www/localhost/htdocs/adminer/index.php || {
|
||||
msg_error "Adminer download failed"
|
||||
return 250
|
||||
}
|
||||
msg_ok "Adminer at /adminer (served by your webserver)"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# uv – Alpine (musl tarball)
|
||||
# optional: PYTHON_VERSION="3.12"
|
||||
# ------------------------------
|
||||
setup_uv() {
|
||||
need_tool curl tar || return 127
|
||||
local UV_BIN="/usr/local/bin/uv"
|
||||
local arch tarball url tmpd ver installed
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="x86_64-unknown-linux-musl" ;;
|
||||
aarch64) arch="aarch64-unknown-linux-musl" ;;
|
||||
*)
|
||||
msg_error "Unsupported arch for uv: $(uname -m)"
|
||||
return 238
|
||||
;;
|
||||
esac
|
||||
|
||||
ver="$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.tag_name' 2>/dev/null)"
|
||||
ver="${ver#v}"
|
||||
[ -z "$ver" ] && {
|
||||
msg_error "uv: cannot determine latest version"
|
||||
return 250
|
||||
}
|
||||
|
||||
if has "$UV_BIN"; then
|
||||
installed="$($UV_BIN -V 2>/dev/null | awk '{print $2}')"
|
||||
[ "$installed" = "$ver" ] && {
|
||||
msg_ok "uv $ver already installed"
|
||||
return 0
|
||||
}
|
||||
msg_info "Updating uv $installed → $ver"
|
||||
else
|
||||
msg_info "Setup uv $ver"
|
||||
fi
|
||||
|
||||
tmpd="$(mktemp -d)" || return 252
|
||||
tarball="uv-${arch}.tar.gz"
|
||||
url="https://github.com/astral-sh/uv/releases/download/v${ver}/${tarball}"
|
||||
|
||||
download_with_progress "$url" "$tmpd/uv.tar.gz" || {
|
||||
rm -rf "$tmpd"
|
||||
return 250
|
||||
}
|
||||
tar -xzf "$tmpd/uv.tar.gz" -C "$tmpd" || {
|
||||
msg_error "uv: extract failed"
|
||||
rm -rf "$tmpd"
|
||||
return 251
|
||||
}
|
||||
|
||||
# tar contains ./uv
|
||||
if [ -x "$tmpd/uv" ]; then
|
||||
/usr/bin/install -m 0755 "$tmpd/uv" "$UV_BIN"
|
||||
else
|
||||
# fallback: in subfolder
|
||||
/usr/bin/install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || {
|
||||
msg_error "uv binary not found in tar"
|
||||
rm -rf "$tmpd"
|
||||
return 252
|
||||
}
|
||||
fi
|
||||
rm -rf "$tmpd"
|
||||
ensure_usr_local_bin_persist
|
||||
msg_ok "Setup uv $ver"
|
||||
|
||||
if [ -n "${PYTHON_VERSION:-}" ]; then
|
||||
local match
|
||||
match="$(uv python list --only-downloads 2>/dev/null | awk -v maj="$PYTHON_VERSION" '
|
||||
$0 ~ "^cpython-"maj"\\." { print $0 }' | awk -F- '{print $2}' | sort -V | tail -n1)"
|
||||
[ -z "$match" ] && {
|
||||
msg_error "No matching Python for $PYTHON_VERSION"
|
||||
return 250
|
||||
}
|
||||
if ! uv python list | grep -q "cpython-${match}-linux"; then
|
||||
msg_info "Installing Python $match via uv"
|
||||
uv python install "$match" || {
|
||||
msg_error "uv python install failed"
|
||||
return 150
|
||||
}
|
||||
msg_ok "Python $match installed (uv)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# Java – Alpine (OpenJDK)
|
||||
# JAVA_VERSION: 17|21 (Default 21)
|
||||
# ------------------------------
|
||||
setup_java() {
|
||||
local JAVA_VERSION="${JAVA_VERSION:-21}" pkg
|
||||
case "$JAVA_VERSION" in
|
||||
17) pkg="openjdk17-jdk" ;;
|
||||
21 | *) pkg="openjdk21-jdk" ;;
|
||||
esac
|
||||
msg_info "Setup Java (OpenJDK $JAVA_VERSION)"
|
||||
apk add --no-cache "$pkg" >/dev/null 2>&1 || {
|
||||
msg_error "apk add $pkg failed"
|
||||
return 100
|
||||
}
|
||||
# set JAVA_HOME
|
||||
local prof="/etc/profile.d/20-java.sh"
|
||||
if [ ! -f "$prof" ]; then
|
||||
echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(command -v java))))' >"$prof"
|
||||
echo 'case ":$PATH:" in *:$JAVA_HOME/bin:*) ;; *) export PATH="$JAVA_HOME/bin:$PATH";; esac' >>"$prof"
|
||||
chmod +x "$prof"
|
||||
fi
|
||||
msg_ok "Java ready: $(java -version 2>&1 | head -n1)"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# Go – Alpine (apk prefers, else tarball)
|
||||
# ------------------------------
|
||||
setup_go() {
|
||||
if [ -z "${GO_VERSION:-}" ]; then
|
||||
msg_info "Setup Go (apk)"
|
||||
apk add --no-cache go >/dev/null 2>&1 || {
|
||||
msg_error "apk add go failed"
|
||||
return 100
|
||||
}
|
||||
msg_ok "Go ready: $(go version 2>/dev/null)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
need_tool curl tar || return 127
|
||||
local ARCH TARBALL URL TMP
|
||||
case "$(uname -m)" in
|
||||
x86_64) ARCH="amd64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
*)
|
||||
msg_error "Unsupported arch for Go: $(uname -m)"
|
||||
return 238
|
||||
;;
|
||||
esac
|
||||
TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz"
|
||||
URL="https://go.dev/dl/${TARBALL}"
|
||||
msg_info "Setup Go $GO_VERSION (tarball)"
|
||||
TMP="$(mktemp)"
|
||||
download_with_progress "$URL" "$TMP" || return 250
|
||||
rm -rf /usr/local/go
|
||||
tar -C /usr/local -xzf "$TMP" || {
|
||||
msg_error "extract go failed"
|
||||
rm -f "$TMP"
|
||||
return 251
|
||||
}
|
||||
rm -f "$TMP"
|
||||
ln -sf /usr/local/go/bin/go /usr/local/bin/go
|
||||
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
|
||||
ensure_usr_local_bin_persist
|
||||
msg_ok "Go ready: $(go version 2>/dev/null)"
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
# Composer – Alpine
|
||||
# uses php83-cli + openssl + phar
|
||||
# ------------------------------
|
||||
setup_composer() {
|
||||
local COMPOSER_BIN="/usr/local/bin/composer"
|
||||
if ! has php; then
|
||||
# prefers php83
|
||||
msg_info "Installing PHP CLI for Composer"
|
||||
apk add --no-cache php83-cli php83-openssl php83-phar php83-iconv >/dev/null 2>&1 || {
|
||||
# Fallback to generic php if 83 not available
|
||||
apk add --no-cache php-cli php-openssl php-phar php-iconv >/dev/null 2>&1 || {
|
||||
msg_error "Failed to install php-cli for composer"
|
||||
return 100
|
||||
}
|
||||
}
|
||||
msg_ok "PHP CLI ready: $(php -v | head -n1)"
|
||||
fi
|
||||
|
||||
if [ -x "$COMPOSER_BIN" ]; then
|
||||
msg_info "Updating Composer"
|
||||
else
|
||||
msg_info "Setup Composer"
|
||||
fi
|
||||
|
||||
need_tool curl || return 127
|
||||
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
|
||||
msg_error "composer installer download failed"
|
||||
return 250
|
||||
}
|
||||
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 || {
|
||||
msg_error "composer install failed"
|
||||
return 150
|
||||
}
|
||||
rm -f /tmp/composer-setup.php
|
||||
ensure_usr_local_bin_persist
|
||||
msg_ok "Composer ready: $(composer --version 2>/dev/null)"
|
||||
}
|
||||
-1224
File diff suppressed because it is too large.
Load diff
-7331
File diff suppressed because it is too large.
Load diff
@@ -1,708 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: community-scripts ORG
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
|
||||
# Revision: 1
|
||||
|
||||
# ==============================================================================
|
||||
# CLOUD-INIT.FUNC - VM CLOUD-INIT CONFIGURATION LIBRARY
|
||||
# ==============================================================================
|
||||
#
|
||||
# Universal helper library for Cloud-Init configuration in Proxmox VMs.
|
||||
# Provides functions for:
|
||||
#
|
||||
# - Native Proxmox Cloud-Init setup (user, password, network, SSH keys)
|
||||
# - Interactive configuration dialogs (whiptail)
|
||||
# - IP address retrieval via qemu-guest-agent
|
||||
# - Cloud-Init status monitoring and waiting
|
||||
#
|
||||
# Usage:
|
||||
# source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/cloud-init.func)
|
||||
# setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
|
||||
#
|
||||
# Compatible with: Debian, Ubuntu, and all Cloud-Init enabled distributions
|
||||
# ==============================================================================
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 1: CONFIGURATION DEFAULTS
|
||||
# ==============================================================================
|
||||
# These can be overridden before sourcing this library
|
||||
|
||||
# Disable 'unbound variable' errors for this library (restored at end)
|
||||
_OLD_SET_STATE=$(set +o | grep -E 'set -(e|u|o)')
|
||||
set +u
|
||||
|
||||
CLOUDINIT_DEFAULT_USER="${CLOUDINIT_DEFAULT_USER:-root}"
|
||||
CLOUDINIT_DNS_SERVERS="${CLOUDINIT_DNS_SERVERS:-1.1.1.1 8.8.8.8}"
|
||||
CLOUDINIT_SEARCH_DOMAIN="${CLOUDINIT_SEARCH_DOMAIN:-local}"
|
||||
CLOUDINIT_SSH_KEYS="${CLOUDINIT_SSH_KEYS:-}" # Empty by default - user must explicitly provide keys
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 2: SSH KEY DISCOVERY AND SELECTION
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _ci_ssh_extract_keys_from_file - Extracts valid SSH public keys from a file
|
||||
# ------------------------------------------------------------------------------
|
||||
function _ci_ssh_extract_keys_from_file() {
|
||||
local file="$1"
|
||||
[[ -f "$file" && -r "$file" ]] || return 0
|
||||
grep -E '^(ssh-(rsa|ed25519|dss|ecdsa)|ecdsa-sha2-)' "$file" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _ci_ssh_discover_files - Scans standard paths for SSH keys
|
||||
# ------------------------------------------------------------------------------
|
||||
function _ci_ssh_discover_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[@]}"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _ci_ssh_build_choices - Builds whiptail checklist from SSH key files
|
||||
#
|
||||
# Sets: CI_SSH_CHOICES (array), CI_SSH_COUNT (int), CI_SSH_MAPFILE (path)
|
||||
# ------------------------------------------------------------------------------
|
||||
function _ci_ssh_build_choices() {
|
||||
local -a files=("$@")
|
||||
CI_SSH_CHOICES=()
|
||||
CI_SSH_COUNT=0
|
||||
CI_SSH_MAPFILE="$(mktemp)"
|
||||
local id key typ fp cmt base
|
||||
|
||||
for f in "${files[@]}"; do
|
||||
[[ -f "$f" && -r "$f" ]] || continue
|
||||
base="$(basename -- "$f")"
|
||||
# Skip known_hosts and private keys
|
||||
case "$base" in
|
||||
known_hosts | known_hosts.* | config) continue ;;
|
||||
id_*) [[ "$f" != *.pub ]] && continue ;;
|
||||
esac
|
||||
|
||||
while IFS= read -r key; do
|
||||
[[ -n "$key" ]] || continue
|
||||
|
||||
typ=""
|
||||
fp=""
|
||||
cmt=""
|
||||
read -r _typ _b64 _cmt <<<"$key"
|
||||
typ="${_typ:-key}"
|
||||
cmt="${_cmt:-}"
|
||||
|
||||
# Get 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
|
||||
|
||||
# Shorten long comments
|
||||
[[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
|
||||
|
||||
CI_SSH_COUNT=$((CI_SSH_COUNT + 1))
|
||||
id="K${CI_SSH_COUNT}"
|
||||
echo "${id}|${key}" >>"$CI_SSH_MAPFILE"
|
||||
CI_SSH_CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
|
||||
done < <(_ci_ssh_extract_keys_from_file "$f")
|
||||
done
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# configure_cloudinit_ssh_keys - Interactive SSH key selection for Cloud-Init
|
||||
#
|
||||
# Usage: configure_cloudinit_ssh_keys
|
||||
# Sets: CLOUDINIT_SSH_KEYS (path to temporary file with selected keys)
|
||||
# ------------------------------------------------------------------------------
|
||||
function configure_cloudinit_ssh_keys() {
|
||||
local backtitle="Proxmox VE Helper Scripts"
|
||||
local ssh_key_mode
|
||||
|
||||
# Create temp file for selected keys
|
||||
CLOUDINIT_SSH_KEYS_TEMP="$(mktemp)"
|
||||
: >"$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
|
||||
# Discover keys and build choices
|
||||
IFS=$'\0' read -r -d '' -a _def_files < <(_ci_ssh_discover_files && printf '\0')
|
||||
_ci_ssh_build_choices "${_def_files[@]}"
|
||||
local default_key_count="$CI_SSH_COUNT"
|
||||
|
||||
if [[ "$default_key_count" -gt 0 ]]; then
|
||||
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
|
||||
"Provision SSH keys for Cloud-Init VM:" 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 SSH keys (password auth only)" 3>&1 1>&2 2>&3) || return 1
|
||||
else
|
||||
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
|
||||
"No host keys detected. Choose:" 12 72 3 \
|
||||
"manual" "Paste a single public key" \
|
||||
"folder" "Scan another folder (path or glob)" \
|
||||
"none" "No SSH keys (password auth only)" 3>&1 1>&2 2>&3) || return 1
|
||||
fi
|
||||
|
||||
case "$ssh_key_mode" in
|
||||
found)
|
||||
# Show checklist with individual keys
|
||||
local selection
|
||||
selection=$(whiptail --backtitle "$backtitle" --title "SELECT SSH KEYS" \
|
||||
--checklist "Select one or more keys to import:" 20 140 10 "${CI_SSH_CHOICES[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
for tag in $selection; do
|
||||
tag="${tag%\"}"
|
||||
tag="${tag#\"}"
|
||||
local line
|
||||
line=$(grep -E "^${tag}\|" "$CI_SSH_MAPFILE" | head -n1 | cut -d'|' -f2-)
|
||||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
done
|
||||
local imported
|
||||
imported=$(wc -l <"$CLOUDINIT_SSH_KEYS_TEMP")
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}${imported} key(s) selected${CL}"
|
||||
;;
|
||||
manual)
|
||||
local pubkey
|
||||
pubkey=$(whiptail --backtitle "$backtitle" --title "PASTE SSH PUBLIC KEY" \
|
||||
--inputbox "Paste your SSH public key (ssh-rsa, ssh-ed25519, etc.):" 10 76 3>&1 1>&2 2>&3) || return 1
|
||||
if [[ -n "$pubkey" ]]; then
|
||||
echo "$pubkey" >"$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}1 key added manually${CL}"
|
||||
else
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}none (empty input)${CL}"
|
||||
CLOUDINIT_SSH_KEYS=""
|
||||
rm -f "$CLOUDINIT_SSH_KEYS_TEMP" "$CI_SSH_MAPFILE" 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
folder)
|
||||
local glob_path
|
||||
glob_path=$(whiptail --backtitle "$backtitle" --title "SCAN FOLDER/GLOB" \
|
||||
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub):" 10 72 3>&1 1>&2 2>&3) || return 1
|
||||
if [[ -n "$glob_path" ]]; then
|
||||
shopt -s nullglob
|
||||
local -a _scan_files=($glob_path)
|
||||
shopt -u nullglob
|
||||
if [[ "${#_scan_files[@]}" -gt 0 ]]; then
|
||||
_ci_ssh_build_choices "${_scan_files[@]}"
|
||||
if [[ "$CI_SSH_COUNT" -gt 0 ]]; then
|
||||
local folder_selection
|
||||
folder_selection=$(whiptail --backtitle "$backtitle" --title "SELECT FOLDER KEYS" \
|
||||
--checklist "Select key(s) to import:" 20 140 10 "${CI_SSH_CHOICES[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
for tag in $folder_selection; do
|
||||
tag="${tag%\"}"
|
||||
tag="${tag#\"}"
|
||||
local line
|
||||
line=$(grep -E "^${tag}\|" "$CI_SSH_MAPFILE" | head -n1 | cut -d'|' -f2-)
|
||||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
done
|
||||
local imported
|
||||
imported=$(wc -l <"$CLOUDINIT_SSH_KEYS_TEMP")
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}${imported} key(s) from folder${CL}"
|
||||
else
|
||||
whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60
|
||||
fi
|
||||
else
|
||||
whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
none | *)
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}none (password auth only)${CL}"
|
||||
CLOUDINIT_SSH_KEYS=""
|
||||
rm -f "$CLOUDINIT_SSH_KEYS_TEMP" "$CI_SSH_MAPFILE" 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Cleanup mapfile
|
||||
rm -f "$CI_SSH_MAPFILE" 2>/dev/null
|
||||
|
||||
# Set the variable for setup_cloud_init to use
|
||||
if [[ -s "$CLOUDINIT_SSH_KEYS_TEMP" ]]; then
|
||||
CLOUDINIT_SSH_KEYS="$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
else
|
||||
CLOUDINIT_SSH_KEYS=""
|
||||
rm -f "$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 3: HELPER FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _ci_msg - Internal message helper with fallback
|
||||
# ------------------------------------------------------------------------------
|
||||
function _ci_msg_info() { msg_info "$1" 2>/dev/null || echo "[INFO] $1"; }
|
||||
function _ci_msg_ok() { msg_ok "$1" 2>/dev/null || echo "[OK] $1"; }
|
||||
function _ci_msg_warn() { msg_warn "$1" 2>/dev/null || echo "[WARN] $1"; }
|
||||
function _ci_msg_error() { msg_error "$1" 2>/dev/null || echo "[ERROR] $1"; }
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# validate_ip_cidr - Validate IP address in CIDR format
|
||||
# Usage: validate_ip_cidr "192.168.1.100/24" && echo "Valid"
|
||||
# Returns: 0 if valid, 1 if invalid
|
||||
# ------------------------------------------------------------------------------
|
||||
function validate_ip_cidr() {
|
||||
local ip_cidr="$1"
|
||||
# Match: 0-255.0-255.0-255.0-255/0-32
|
||||
if [[ "$ip_cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
|
||||
# Validate each octet is 0-255
|
||||
local ip="${ip_cidr%/*}"
|
||||
IFS='.' read -ra octets <<<"$ip"
|
||||
for octet in "${octets[@]}"; do
|
||||
((octet > 255)) && return 1
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# validate_ip - Validate plain IP address (no CIDR)
|
||||
# Usage: validate_ip "192.168.1.1" && echo "Valid"
|
||||
# ------------------------------------------------------------------------------
|
||||
function validate_ip() {
|
||||
local ip="$1"
|
||||
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||
IFS='.' read -ra octets <<<"$ip"
|
||||
for octet in "${octets[@]}"; do
|
||||
((octet > 255)) && return 1
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 3: MAIN CLOUD-INIT FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# setup_cloud_init - Configures Proxmox Native Cloud-Init
|
||||
# ------------------------------------------------------------------------------
|
||||
# Parameters:
|
||||
# $1 - VMID (required)
|
||||
# $2 - Storage name (required)
|
||||
# $3 - Hostname (optional, default: vm-<vmid>)
|
||||
# $4 - Enable Cloud-Init (yes/no, default: no)
|
||||
# $5 - User (optional, default: root)
|
||||
# $6 - Network mode (dhcp/static, default: dhcp)
|
||||
# $7 - Static IP (optional, format: 192.168.1.100/24)
|
||||
# $8 - Gateway (optional)
|
||||
# $9 - Nameservers (optional, default: 1.1.1.1 8.8.8.8)
|
||||
#
|
||||
# Returns: 0 on success, 1 on failure
|
||||
# Exports: CLOUDINIT_USER, CLOUDINIT_PASSWORD, CLOUDINIT_CRED_FILE
|
||||
# ==============================================================================
|
||||
function setup_cloud_init() {
|
||||
local vmid="$1"
|
||||
local storage="$2"
|
||||
local hostname="${3:-vm-${vmid}}"
|
||||
local enable="${4:-no}"
|
||||
local ciuser="${5:-$CLOUDINIT_DEFAULT_USER}"
|
||||
local network_mode="${6:-dhcp}"
|
||||
local static_ip="${7:-}"
|
||||
local gateway="${8:-}"
|
||||
local nameservers="${9:-$CLOUDINIT_DNS_SERVERS}"
|
||||
|
||||
# Skip if not enabled
|
||||
if [ "$enable" != "yes" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Validate static IP if provided
|
||||
if [ "$network_mode" = "static" ]; then
|
||||
if [ -n "$static_ip" ] && ! validate_ip_cidr "$static_ip"; then
|
||||
_ci_msg_error "Invalid static IP format: $static_ip (expected: x.x.x.x/xx)"
|
||||
return 65
|
||||
fi
|
||||
if [ -n "$gateway" ] && ! validate_ip "$gateway"; then
|
||||
_ci_msg_error "Invalid gateway IP format: $gateway"
|
||||
return 65
|
||||
fi
|
||||
fi
|
||||
|
||||
_ci_msg_info "Configuring Cloud-Init"
|
||||
|
||||
# Create Cloud-Init drive (try ide2 first, then scsi1 as fallback)
|
||||
if ! qm set "$vmid" --ide2 "${storage}:cloudinit" >/dev/null 2>&1; then
|
||||
qm set "$vmid" --scsi1 "${storage}:cloudinit" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Set user
|
||||
qm set "$vmid" --ciuser "$ciuser" >/dev/null
|
||||
|
||||
# Generate and set secure random password
|
||||
local cipassword=$(openssl rand -base64 16)
|
||||
qm set "$vmid" --cipassword "$cipassword" >/dev/null
|
||||
|
||||
# Add SSH keys only if explicitly provided (not auto-imported from host)
|
||||
if [ -n "${CLOUDINIT_SSH_KEYS:-}" ] && [ -f "$CLOUDINIT_SSH_KEYS" ]; then
|
||||
qm set "$vmid" --sshkeys "$CLOUDINIT_SSH_KEYS" >/dev/null 2>&1 || true
|
||||
_ci_msg_info "SSH keys imported from: $CLOUDINIT_SSH_KEYS"
|
||||
fi
|
||||
|
||||
# Configure network
|
||||
if [ "$network_mode" = "static" ] && [ -n "$static_ip" ] && [ -n "$gateway" ]; then
|
||||
qm set "$vmid" --ipconfig0 "ip=${static_ip},gw=${gateway}" >/dev/null
|
||||
else
|
||||
qm set "$vmid" --ipconfig0 "ip=dhcp" >/dev/null
|
||||
fi
|
||||
|
||||
# Set DNS servers
|
||||
qm set "$vmid" --nameserver "$nameservers" >/dev/null
|
||||
|
||||
# Set search domain
|
||||
qm set "$vmid" --searchdomain "$CLOUDINIT_SEARCH_DOMAIN" >/dev/null
|
||||
|
||||
# Enable package upgrades on first boot (if supported by Proxmox version)
|
||||
qm set "$vmid" --ciupgrade 1 >/dev/null 2>&1 || true
|
||||
|
||||
# Save credentials to file (with restrictive permissions)
|
||||
local cred_file="/tmp/${hostname}-${vmid}-cloud-init-credentials.txt"
|
||||
umask 077
|
||||
cat >"$cred_file" <<EOF
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ ⚠️ SECURITY WARNING: DELETE THIS FILE AFTER NOTING CREDENTIALS ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
Cloud-Init Credentials
|
||||
────────────────────────────────────────
|
||||
VM ID: ${vmid}
|
||||
Hostname: ${hostname}
|
||||
Created: $(date)
|
||||
|
||||
Username: ${ciuser}
|
||||
Password: ${cipassword}
|
||||
|
||||
Network: ${network_mode}$([ "$network_mode" = "static" ] && echo " (IP: ${static_ip}, GW: ${gateway})" || echo " (DHCP)")
|
||||
DNS: ${nameservers}
|
||||
|
||||
────────────────────────────────────────
|
||||
SSH Access (if keys configured):
|
||||
ssh ${ciuser}@<vm-ip>
|
||||
|
||||
Proxmox UI Configuration:
|
||||
VM ${vmid} > Cloud-Init > Edit
|
||||
- User, Password, SSH Keys
|
||||
- Network (IP Config)
|
||||
- DNS, Search Domain
|
||||
|
||||
────────────────────────────────────────
|
||||
🗑️ To delete this file:
|
||||
rm -f ${cred_file}
|
||||
────────────────────────────────────────
|
||||
EOF
|
||||
chmod 600 "$cred_file"
|
||||
|
||||
_ci_msg_ok "Cloud-Init configured (User: ${ciuser})"
|
||||
|
||||
# Export for use in calling script (DO NOT display password here - will be shown in summary)
|
||||
export CLOUDINIT_USER="$ciuser"
|
||||
export CLOUDINIT_PASSWORD="$cipassword"
|
||||
export CLOUDINIT_CRED_FILE="$cred_file"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 4: INTERACTIVE CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# configure_cloud_init_interactive - Whiptail dialog for Cloud-Init setup
|
||||
# ------------------------------------------------------------------------------
|
||||
# Prompts user for Cloud-Init configuration choices
|
||||
# Returns configuration via exported variables:
|
||||
# - CLOUDINIT_ENABLE (yes/no)
|
||||
# - CLOUDINIT_USER
|
||||
# - CLOUDINIT_NETWORK_MODE (dhcp/static)
|
||||
# - CLOUDINIT_IP (if static)
|
||||
# - CLOUDINIT_GW (if static)
|
||||
# - CLOUDINIT_DNS
|
||||
# ------------------------------------------------------------------------------
|
||||
function configure_cloud_init_interactive() {
|
||||
local default_user="${1:-root}"
|
||||
|
||||
# Check if whiptail is available
|
||||
if ! command -v whiptail >/dev/null 2>&1; then
|
||||
echo "Warning: whiptail not available, skipping interactive configuration"
|
||||
export CLOUDINIT_ENABLE="no"
|
||||
return 127
|
||||
fi
|
||||
|
||||
# Ask if user wants to enable Cloud-Init
|
||||
if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
|
||||
--yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n• User accounts and passwords\n• SSH keys\n• Network settings (DHCP/Static)\n• DNS configuration\n\nYou can also configure these settings later in Proxmox UI." 16 68); then
|
||||
export CLOUDINIT_ENABLE="no"
|
||||
return 0
|
||||
fi
|
||||
|
||||
export CLOUDINIT_ENABLE="yes"
|
||||
|
||||
# Username
|
||||
if CLOUDINIT_USER=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||
"Cloud-Init Username" 8 58 "$default_user" --title "USERNAME" 3>&1 1>&2 2>&3); then
|
||||
export CLOUDINIT_USER="${CLOUDINIT_USER:-$default_user}"
|
||||
else
|
||||
export CLOUDINIT_USER="$default_user"
|
||||
fi
|
||||
|
||||
# Network configuration
|
||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "NETWORK MODE" \
|
||||
--yesno "Use DHCP for network configuration?\n\nSelect 'No' for static IP configuration." 10 58); then
|
||||
export CLOUDINIT_NETWORK_MODE="dhcp"
|
||||
else
|
||||
export CLOUDINIT_NETWORK_MODE="static"
|
||||
|
||||
# Static IP with validation
|
||||
while true; do
|
||||
if CLOUDINIT_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||
"Static IP Address (CIDR format)\nExample: 192.168.1.100/24" 9 58 "" --title "IP ADDRESS" 3>&1 1>&2 2>&3); then
|
||||
if validate_ip_cidr "$CLOUDINIT_IP"; then
|
||||
export CLOUDINIT_IP
|
||||
break
|
||||
else
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID IP" \
|
||||
--msgbox "Invalid IP format: $CLOUDINIT_IP\n\nPlease use CIDR format: x.x.x.x/xx\nExample: 192.168.1.100/24" 10 50
|
||||
fi
|
||||
else
|
||||
_ci_msg_warn "Static IP required, falling back to DHCP"
|
||||
export CLOUDINIT_NETWORK_MODE="dhcp"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Gateway with validation
|
||||
if [ "$CLOUDINIT_NETWORK_MODE" = "static" ]; then
|
||||
while true; do
|
||||
if CLOUDINIT_GW=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||
"Gateway IP Address\nExample: 192.168.1.1" 8 58 "" --title "GATEWAY" 3>&1 1>&2 2>&3); then
|
||||
if validate_ip "$CLOUDINIT_GW"; then
|
||||
export CLOUDINIT_GW
|
||||
break
|
||||
else
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID GATEWAY" \
|
||||
--msgbox "Invalid gateway format: $CLOUDINIT_GW\n\nPlease use format: x.x.x.x\nExample: 192.168.1.1" 10 50
|
||||
fi
|
||||
else
|
||||
_ci_msg_warn "Gateway required, falling back to DHCP"
|
||||
export CLOUDINIT_NETWORK_MODE="dhcp"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# DNS Servers
|
||||
if CLOUDINIT_DNS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
|
||||
"DNS Servers (space-separated)" 8 58 "1.1.1.1 8.8.8.8" --title "DNS SERVERS" 3>&1 1>&2 2>&3); then
|
||||
export CLOUDINIT_DNS="${CLOUDINIT_DNS:-1.1.1.1 8.8.8.8}"
|
||||
else
|
||||
export CLOUDINIT_DNS="1.1.1.1 8.8.8.8"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 5: UTILITY FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# display_cloud_init_info - Show Cloud-Init summary after setup
|
||||
# ------------------------------------------------------------------------------
|
||||
function display_cloud_init_info() {
|
||||
local vmid="$1"
|
||||
local hostname="${2:-}"
|
||||
|
||||
if [ -n "$CLOUDINIT_CRED_FILE" ] && [ -f "$CLOUDINIT_CRED_FILE" ]; then
|
||||
if [ -n "${INFO:-}" ]; then
|
||||
echo -e "\n${INFO}${BOLD:-}${GN:-} Cloud-Init Configuration:${CL:-}"
|
||||
echo -e "${TAB:- }${DGN:-}User: ${BGN:-}${CLOUDINIT_USER:-root}${CL:-}"
|
||||
echo -e "${TAB:- }${DGN:-}Password: ${BGN:-}${CLOUDINIT_PASSWORD}${CL:-}"
|
||||
echo -e "${TAB:- }${DGN:-}Credentials: ${BL:-}${CLOUDINIT_CRED_FILE}${CL:-}"
|
||||
echo -e "${TAB:- }${RD:-}⚠️ Delete credentials file after noting password!${CL:-}"
|
||||
echo -e "${TAB:- }${YW:-}💡 Configure in Proxmox UI: VM ${vmid} > Cloud-Init${CL:-}"
|
||||
else
|
||||
echo ""
|
||||
echo "[INFO] Cloud-Init Configuration:"
|
||||
echo " User: ${CLOUDINIT_USER:-root}"
|
||||
echo " Password: ${CLOUDINIT_PASSWORD}"
|
||||
echo " Credentials: ${CLOUDINIT_CRED_FILE}"
|
||||
echo " ⚠️ Delete credentials file after noting password!"
|
||||
echo " Configure in Proxmox UI: VM ${vmid} > Cloud-Init"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# cleanup_cloud_init_credentials - Remove credentials file
|
||||
# ------------------------------------------------------------------------------
|
||||
# Usage: cleanup_cloud_init_credentials
|
||||
# Call this after user has noted/saved the credentials
|
||||
# ------------------------------------------------------------------------------
|
||||
function cleanup_cloud_init_credentials() {
|
||||
if [ -n "$CLOUDINIT_CRED_FILE" ] && [ -f "$CLOUDINIT_CRED_FILE" ]; then
|
||||
rm -f "$CLOUDINIT_CRED_FILE"
|
||||
_ci_msg_ok "Credentials file removed: $CLOUDINIT_CRED_FILE"
|
||||
unset CLOUDINIT_CRED_FILE
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# has_cloud_init - Check if VM has Cloud-Init configured
|
||||
# ------------------------------------------------------------------------------
|
||||
function has_cloud_init() {
|
||||
local vmid="$1"
|
||||
qm config "$vmid" 2>/dev/null | grep -qE "(ide2|scsi1):.*cloudinit"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# regenerate_cloud_init - Regenerate Cloud-Init configuration
|
||||
# ------------------------------------------------------------------------------
|
||||
function regenerate_cloud_init() {
|
||||
local vmid="$1"
|
||||
|
||||
if has_cloud_init "$vmid"; then
|
||||
_ci_msg_info "Regenerating Cloud-Init configuration"
|
||||
qm cloudinit update "$vmid" >/dev/null 2>&1 || true
|
||||
_ci_msg_ok "Cloud-Init configuration regenerated"
|
||||
return 0
|
||||
else
|
||||
_ci_msg_warn "VM $vmid does not have Cloud-Init configured"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# get_vm_ip - Get VM IP address via qemu-guest-agent
|
||||
# ------------------------------------------------------------------------------
|
||||
function get_vm_ip() {
|
||||
local vmid="$1"
|
||||
local timeout="${2:-30}"
|
||||
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
local vm_ip=$(qm guest cmd "$vmid" network-get-interfaces 2>/dev/null |
|
||||
jq -r '.[] | select(.name != "lo") | ."ip-addresses"[]? | select(."ip-address-type" == "ipv4") | ."ip-address"' 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$vm_ip" ]; then
|
||||
echo "$vm_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
return 7
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# wait_for_cloud_init - Wait for Cloud-Init to complete (requires SSH access)
|
||||
# ------------------------------------------------------------------------------
|
||||
function wait_for_cloud_init() {
|
||||
local vmid="$1"
|
||||
local timeout="${2:-300}"
|
||||
local vm_ip="${3:-}"
|
||||
|
||||
# Get IP if not provided
|
||||
if [ -z "$vm_ip" ]; then
|
||||
vm_ip=$(get_vm_ip "$vmid" 60)
|
||||
fi
|
||||
|
||||
if [ -z "$vm_ip" ]; then
|
||||
_ci_msg_warn "Unable to determine VM IP address"
|
||||
return 7
|
||||
fi
|
||||
|
||||
_ci_msg_info "Waiting for Cloud-Init to complete on ${vm_ip}"
|
||||
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if timeout 10 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"${CLOUDINIT_USER:-root}@${vm_ip}" "cloud-init status --wait" 2>/dev/null; then
|
||||
_ci_msg_ok "Cloud-Init completed successfully"
|
||||
return 0
|
||||
fi
|
||||
sleep 10
|
||||
elapsed=$((elapsed + 10))
|
||||
done
|
||||
|
||||
_ci_msg_warn "Cloud-Init did not complete within ${timeout}s"
|
||||
return 150
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 6: EXPORTS
|
||||
# ==============================================================================
|
||||
# Export all functions for use in other scripts
|
||||
|
||||
export -f setup_cloud_init 2>/dev/null || true
|
||||
export -f configure_cloud_init_interactive 2>/dev/null || true
|
||||
export -f display_cloud_init_info 2>/dev/null || true
|
||||
export -f cleanup_cloud_init_credentials 2>/dev/null || true
|
||||
export -f has_cloud_init 2>/dev/null || true
|
||||
export -f regenerate_cloud_init 2>/dev/null || true
|
||||
export -f get_vm_ip 2>/dev/null || true
|
||||
export -f wait_for_cloud_init 2>/dev/null || true
|
||||
export -f validate_ip_cidr 2>/dev/null || true
|
||||
export -f validate_ip 2>/dev/null || true
|
||||
|
||||
# Restore previous shell options if they were saved
|
||||
if [ -n "${_OLD_SET_STATE:-}" ]; then
|
||||
eval "$_OLD_SET_STATE"
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 7: EXAMPLES & DOCUMENTATION
|
||||
# ==============================================================================
|
||||
: <<'EXAMPLES'
|
||||
|
||||
# Example 1: Simple DHCP setup (most common)
|
||||
setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
|
||||
|
||||
# Example 2: Static IP setup
|
||||
setup_cloud_init "$VMID" "$STORAGE" "myserver" "yes" "root" "static" "192.168.1.100/24" "192.168.1.1"
|
||||
|
||||
# Example 3: Interactive configuration in advanced_settings()
|
||||
configure_cloud_init_interactive "admin"
|
||||
if [ "$CLOUDINIT_ENABLE" = "yes" ]; then
|
||||
setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" "$CLOUDINIT_USER" \
|
||||
"$CLOUDINIT_NETWORK_MODE" "$CLOUDINIT_IP" "$CLOUDINIT_GW" "$CLOUDINIT_DNS"
|
||||
fi
|
||||
|
||||
# Example 4: Display info after VM creation
|
||||
display_cloud_init_info "$VMID" "$HN"
|
||||
|
||||
# Example 5: Check if VM has Cloud-Init
|
||||
if has_cloud_init "$VMID"; then
|
||||
echo "Cloud-Init is configured"
|
||||
fi
|
||||
|
||||
# Example 6: Wait for Cloud-Init to complete after VM start
|
||||
if [ "$START_VM" = "yes" ]; then
|
||||
qm start "$VMID"
|
||||
sleep 30
|
||||
wait_for_cloud_init "$VMID" 300
|
||||
fi
|
||||
|
||||
# Example 7: Cleanup credentials file after user has noted password
|
||||
display_cloud_init_info "$VMID" "$HN"
|
||||
read -p "Have you saved the credentials? (y/N): " -r
|
||||
[[ $REPLY =~ ^[Yy]$ ]] && cleanup_cloud_init_credentials
|
||||
|
||||
# Example 8: Validate IP before using
|
||||
if validate_ip_cidr "192.168.1.100/24"; then
|
||||
echo "Valid IP/CIDR"
|
||||
fi
|
||||
|
||||
EXAMPLES
|
||||
-1968
File diff suppressed because it is too large.
Load diff
@@ -1,733 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# ------------------------------------------------------------------------------
|
||||
#
|
||||
# Provides error handling and signal management for all scripts.
|
||||
#
|
||||
# TELEMETRY CONTRACT:
|
||||
# - HOST context: this file reports terminal statuses via post_update_to_api
|
||||
# (full metadata + focused error trace).
|
||||
# - CONTAINER context: this file NEVER talks to the telemetry API. It writes
|
||||
# two local artifacts that the host picks up after lxc-attach returns:
|
||||
# /root/.install-<SESSION_ID>.failed → exit code (flag file)
|
||||
# /root/.install-<SESSION_ID>.log.errinfo → structured error capture
|
||||
# This guarantees the server only ever sees ONE terminal event per
|
||||
# execution - the host's complete one.
|
||||
#
|
||||
# Usage:
|
||||
# source <(curl -fsSL .../error_handler.func)
|
||||
# catch_errors
|
||||
#
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 1: EXIT CODE EXPLANATIONS (fallback)
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# explain_exit_code()
|
||||
#
|
||||
# - Canonical version lives in api.func (sourced before this file on the host)
|
||||
# - This fallback covers the container context where api.func is not sourced
|
||||
# ------------------------------------------------------------------------------
|
||||
if ! declare -f explain_exit_code &>/dev/null; then
|
||||
explain_exit_code() {
|
||||
local code="$1"
|
||||
case "$code" in
|
||||
1) echo "General error / Operation not permitted" ;;
|
||||
2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
|
||||
3) echo "General syntax or argument error" ;;
|
||||
10) echo "Docker / privileged mode required (unsupported environment)" ;;
|
||||
4) echo "curl: Feature not supported or protocol error" ;;
|
||||
5) echo "curl: Could not resolve proxy" ;;
|
||||
6) echo "curl: DNS resolution failed (could not resolve host)" ;;
|
||||
7) echo "curl: Failed to connect (network unreachable / host down)" ;;
|
||||
8) echo "curl: Server reply error (FTP/SFTP or apk untrusted key)" ;;
|
||||
16) echo "curl: HTTP/2 framing layer error" ;;
|
||||
18) echo "curl: Partial file (transfer not completed)" ;;
|
||||
22) echo "curl: HTTP error returned (404, 429, 500+)" ;;
|
||||
23) echo "curl: Write error (disk full or permissions)" ;;
|
||||
24) echo "curl: Write to local file failed" ;;
|
||||
25) echo "curl: Upload failed" ;;
|
||||
26) echo "curl: Read error on local file (I/O)" ;;
|
||||
27) echo "curl: Out of memory (memory allocation failed)" ;;
|
||||
28) echo "curl: Operation timeout (network slow or server not responding)" ;;
|
||||
30) echo "curl: FTP port command failed" ;;
|
||||
32) echo "curl: FTP SIZE command failed" ;;
|
||||
33) echo "curl: HTTP range error" ;;
|
||||
34) echo "curl: HTTP post error" ;;
|
||||
35) echo "curl: SSL/TLS handshake failed (certificate error)" ;;
|
||||
36) echo "curl: FTP bad download resume" ;;
|
||||
39) echo "curl: LDAP search failed" ;;
|
||||
44) echo "curl: Internal error (bad function call order)" ;;
|
||||
45) echo "curl: Interface error (failed to bind to specified interface)" ;;
|
||||
46) echo "curl: Bad password entered" ;;
|
||||
47) echo "curl: Too many redirects" ;;
|
||||
48) echo "curl: Unknown command line option specified" ;;
|
||||
51) echo "curl: SSL peer certificate or SSH host key verification failed" ;;
|
||||
52) echo "curl: Empty reply from server (got nothing)" ;;
|
||||
55) echo "curl: Failed sending network data" ;;
|
||||
56) echo "curl: Receive error (connection reset by peer)" ;;
|
||||
57) echo "curl: Unrecoverable poll/select error (system I/O failure)" ;;
|
||||
59) echo "curl: Couldn't use specified SSL cipher" ;;
|
||||
61) echo "curl: Bad/unrecognized transfer encoding" ;;
|
||||
63) echo "curl: Maximum file size exceeded" ;;
|
||||
75) echo "Temporary failure (retry later)" ;;
|
||||
78) echo "curl: Remote file not found (404 on FTP/file)" ;;
|
||||
79) echo "curl: SSH session error (key exchange/auth failed)" ;;
|
||||
92) echo "curl: HTTP/2 stream error (protocol violation)" ;;
|
||||
95) echo "curl: HTTP/3 layer error" ;;
|
||||
64) echo "Usage error (wrong arguments)" ;;
|
||||
65) echo "Data format error (bad input data)" ;;
|
||||
66) echo "Input file not found (cannot open input)" ;;
|
||||
67) echo "User not found (addressee unknown)" ;;
|
||||
68) echo "Host not found (hostname unknown)" ;;
|
||||
69) echo "Service unavailable" ;;
|
||||
70) echo "Internal software error" ;;
|
||||
71) echo "System error (OS-level failure)" ;;
|
||||
72) echo "Critical OS file missing" ;;
|
||||
73) echo "Cannot create output file" ;;
|
||||
74) echo "I/O error" ;;
|
||||
76) echo "Remote protocol error" ;;
|
||||
77) echo "Permission denied" ;;
|
||||
100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
|
||||
101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
|
||||
102) echo "APT: Lock held by another process (dpkg/apt still running)" ;;
|
||||
103) echo "Validation: Shell is not Bash" ;;
|
||||
104) echo "Validation: Not running as root (or invoked via sudo)" ;;
|
||||
105) echo "Validation: Proxmox VE version not supported" ;;
|
||||
106) echo "Validation: Unsupported architecture (requires amd64 or arm64)" ;;
|
||||
107) echo "Validation: Kernel key parameters unreadable" ;;
|
||||
108) echo "Validation: Kernel key limits exceeded" ;;
|
||||
109) echo "Proxmox: No available container ID after max attempts" ;;
|
||||
110) echo "Proxmox: Failed to apply default.vars" ;;
|
||||
111) echo "Proxmox: App defaults file not available" ;;
|
||||
112) echo "Proxmox: Invalid install menu option" ;;
|
||||
113) echo "LXC: Under-provisioned — user aborted update" ;;
|
||||
114) echo "LXC: Storage too low — user aborted update" ;;
|
||||
115) echo "Download: install.func download failed or incomplete" ;;
|
||||
116) echo "Proxmox: Default bridge vmbr0 not found" ;;
|
||||
117) echo "LXC: Container did not reach running state" ;;
|
||||
118) echo "LXC: No IP assigned to container after timeout" ;;
|
||||
119) echo "Proxmox: No valid storage for rootdir content" ;;
|
||||
120) echo "Proxmox: No valid storage for vztmpl content" ;;
|
||||
121) echo "LXC: Container network not ready (no IP after retries)" ;;
|
||||
122) echo "LXC: No internet connectivity — user declined to continue" ;;
|
||||
123) echo "LXC: Local IP detection failed" ;;
|
||||
124) echo "Command timed out (timeout command)" ;;
|
||||
125) echo "Command failed to start (Docker daemon or execution error)" ;;
|
||||
126) echo "Command invoked cannot execute (permission problem?)" ;;
|
||||
127) echo "Command not found" ;;
|
||||
128) echo "Invalid argument to exit" ;;
|
||||
129) echo "Killed by SIGHUP (terminal closed / hangup)" ;;
|
||||
130) echo "Aborted by user (SIGINT)" ;;
|
||||
131) echo "Killed by SIGQUIT (core dumped)" ;;
|
||||
132) echo "Killed by SIGILL (illegal CPU instruction)" ;;
|
||||
134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;;
|
||||
137) echo "Killed (SIGKILL / Out of memory?)" ;;
|
||||
139) echo "Segmentation fault (core dumped)" ;;
|
||||
141) echo "Broken pipe (SIGPIPE - output closed prematurely)" ;;
|
||||
143) echo "Terminated (SIGTERM)" ;;
|
||||
144) echo "Killed by signal 16 (SIGUSR1 / SIGSTKFLT)" ;;
|
||||
146) echo "Killed by signal 18 (SIGTSTP)" ;;
|
||||
150) echo "Systemd: Service failed to start" ;;
|
||||
151) echo "Systemd: Service unit not found" ;;
|
||||
152) echo "Permission denied (EACCES)" ;;
|
||||
153) echo "Build/compile failed (make/gcc/cmake)" ;;
|
||||
154) echo "Node.js: Native addon build failed (node-gyp)" ;;
|
||||
160) echo "Python: Virtualenv / uv environment missing or broken" ;;
|
||||
161) echo "Python: Dependency resolution failed" ;;
|
||||
162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
|
||||
170) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
|
||||
171) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
|
||||
172) echo "PostgreSQL: Database does not exist" ;;
|
||||
173) echo "PostgreSQL: Fatal error in query / syntax" ;;
|
||||
180) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
|
||||
181) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
|
||||
182) echo "MySQL/MariaDB: Database does not exist" ;;
|
||||
183) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
|
||||
190) echo "MongoDB: Connection failed (server not running)" ;;
|
||||
191) echo "MongoDB: Authentication failed (bad user/password)" ;;
|
||||
192) echo "MongoDB: Database not found" ;;
|
||||
193) echo "MongoDB: Fatal query error" ;;
|
||||
200) echo "Proxmox: Failed to create lock file" ;;
|
||||
203) echo "Proxmox: Missing CTID variable" ;;
|
||||
204) echo "Proxmox: Missing PCT_OSTYPE variable" ;;
|
||||
205) echo "Proxmox: Invalid CTID (<100)" ;;
|
||||
206) echo "Proxmox: CTID already in use" ;;
|
||||
207) echo "Proxmox: Password contains unescaped special characters" ;;
|
||||
208) echo "Proxmox: Invalid configuration (DNS/MAC/Network format)" ;;
|
||||
209) echo "Proxmox: Container creation failed" ;;
|
||||
210) echo "Proxmox: Cluster not quorate" ;;
|
||||
211) echo "Proxmox: Timeout waiting for template lock" ;;
|
||||
212) echo "Proxmox: Storage type 'iscsidirect' does not support containers (VMs only)" ;;
|
||||
213) echo "Proxmox: Storage type does not support 'rootdir' content" ;;
|
||||
214) echo "Proxmox: Not enough storage space" ;;
|
||||
215) echo "Proxmox: Container created but not listed (ghost state)" ;;
|
||||
216) echo "Proxmox: RootFS entry missing in config" ;;
|
||||
217) echo "Proxmox: Storage not accessible" ;;
|
||||
218) echo "Proxmox: Template file corrupted or incomplete" ;;
|
||||
219) echo "Proxmox: CephFS does not support containers - use RBD" ;;
|
||||
220) echo "Proxmox: Unable to resolve template path" ;;
|
||||
221) echo "Proxmox: Template file not readable" ;;
|
||||
222) echo "Proxmox: Template download failed" ;;
|
||||
223) echo "Proxmox: Template not available after download" ;;
|
||||
224) echo "Proxmox: PBS storage is for backups only" ;;
|
||||
225) echo "Proxmox: No template available for OS/Version" ;;
|
||||
226) echo "Proxmox: VM disk import or post-creation setup failed" ;;
|
||||
231) echo "Proxmox: LXC stack upgrade failed" ;;
|
||||
232) echo "Tools: Wrong execution environment (run on PVE host, not inside LXC)" ;;
|
||||
233) echo "Tools: Application not installed (update prerequisite missing)" ;;
|
||||
234) echo "Tools: No LXC containers found or available" ;;
|
||||
235) echo "Tools: Backup or restore operation failed" ;;
|
||||
236) echo "Tools: Required hardware not detected" ;;
|
||||
237) echo "Tools: Dependency package installation failed" ;;
|
||||
238) echo "Tools: OS or distribution not supported for this addon" ;;
|
||||
239) echo "npm/Node.js: Unexpected runtime error or dependency failure" ;;
|
||||
243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
|
||||
245) echo "Node.js: Invalid command-line option" ;;
|
||||
246) echo "Node.js: Internal JavaScript Parse Error" ;;
|
||||
247) echo "Node.js: Fatal internal error" ;;
|
||||
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
|
||||
249) echo "npm/pnpm/yarn: Unknown fatal error" ;;
|
||||
250) echo "App: Download failed or version not determined" ;;
|
||||
251) echo "App: File extraction failed (corrupt or incomplete archive)" ;;
|
||||
252) echo "App: Required file or resource not found" ;;
|
||||
253) echo "App: Data migration required — update aborted" ;;
|
||||
254) echo "App: User declined prompt or input timed out" ;;
|
||||
255) echo "DPKG: Fatal internal error" ;;
|
||||
*) echo "Unknown error" ;;
|
||||
esac
|
||||
}
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 2: CONTEXT DETECTION & CONTAINER ARTIFACTS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _is_container_context()
|
||||
#
|
||||
# - Returns 0 (true) when running INSIDE the LXC container being installed
|
||||
# - TELEMETRY_CONTEXT can override the heuristic ("host" / "container");
|
||||
# install.func sets TELEMETRY_CONTEXT=container during bootstrap
|
||||
# ------------------------------------------------------------------------------
|
||||
_is_container_context() {
|
||||
case "${TELEMETRY_CONTEXT:-}" in
|
||||
container) return 0 ;;
|
||||
host) return 1 ;;
|
||||
esac
|
||||
# Proxmox/Incus tooling exists only on the host
|
||||
command -v pveversion &>/dev/null && return 1
|
||||
command -v pct &>/dev/null && return 1
|
||||
command -v incus &>/dev/null && return 1
|
||||
# systemd-detect-virt reports lxc inside containers
|
||||
if command -v systemd-detect-virt &>/dev/null; then
|
||||
case "$(systemd-detect-virt -c 2>/dev/null)" in
|
||||
lxc | lxc-libvirt | openvz) return 0 ;;
|
||||
esac
|
||||
fi
|
||||
# PCT_OSTYPE is exported into the install environment by the host
|
||||
[[ -n "${PCT_OSTYPE:-}" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _container_write_failure()
|
||||
#
|
||||
# - Writes the failure artifacts inside the container for the host to pick up:
|
||||
# * flag file with the exit code
|
||||
# * .errinfo capture (if silent() has not already written a better one)
|
||||
# * copy of the install log
|
||||
# - This REPLACES any direct telemetry send from the container
|
||||
# - Arguments: $1 = exit_code, $2 = command (optional), $3 = line (optional)
|
||||
# ------------------------------------------------------------------------------
|
||||
_container_write_failure() {
|
||||
local exit_code="${1:-1}"
|
||||
local command="${2:-}"
|
||||
local line="${3:-}"
|
||||
local sid="${SESSION_ID:-error}"
|
||||
|
||||
# Flag file with exit code (host reads this after lxc-attach returns)
|
||||
echo "$exit_code" >"/root/.install-${sid}.failed" 2>/dev/null || true
|
||||
|
||||
# Keep the install log where the host expects it
|
||||
if [[ -n "${INSTALL_LOG:-}" && -f "${INSTALL_LOG}" && "${INSTALL_LOG}" != "/root/.install-${sid}.log" ]]; then
|
||||
cp "${INSTALL_LOG}" "/root/.install-${sid}.log" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Structured error capture (skip if silent() already wrote the exact
|
||||
# output segment of the failing command - that one is always better).
|
||||
# Self-contained: api.func is NOT sourced inside containers.
|
||||
local errinfo="${INSTALL_LOG:-/root/.install-${sid}.log}.errinfo"
|
||||
if [[ ! -s "$errinfo" ]]; then
|
||||
local flat_cmd
|
||||
flat_cmd=$(printf '%s' "${command:-unknown}" | tr '\n' ' ' | head -c 300)
|
||||
{
|
||||
echo "EXIT_CODE=${exit_code}"
|
||||
echo "LINE=${line:-0}"
|
||||
echo "COMMAND=${flat_cmd}"
|
||||
echo "--- OUTPUT ---"
|
||||
if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then
|
||||
tail -n 80 "${INSTALL_LOG}" 2>/dev/null |
|
||||
sed 's/\r$//' |
|
||||
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' |
|
||||
grep -avE '^(Get:|Hit:|Ign:|Fetched |Reading package lists|Reading state information|Building dependency tree|Selecting previously|Preparing to unpack|Unpacking |Processing triggers for|\(Reading database|[0-9]+%[[:space:]]*\[)' |
|
||||
grep -avE '^[[:space:]]*$' |
|
||||
tail -n 60 | head -c 10240
|
||||
fi
|
||||
} >"$errinfo" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 3: ERROR HANDLER
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# error_handler()
|
||||
#
|
||||
# - Main error handler triggered by ERR trap
|
||||
# - Displays error message with line number, exit code, explanation, command
|
||||
# - Shows last 20 lines of the active log
|
||||
# - Emits actionable hints for common failure patterns (OOM, APT, network...)
|
||||
# - HOST: reports "failed" to telemetry (full payload via api.func)
|
||||
# - CONTAINER: writes failure artifacts, sends nothing
|
||||
# - Exits with original exit code
|
||||
# ------------------------------------------------------------------------------
|
||||
error_handler() {
|
||||
local exit_code=${1:-$?}
|
||||
local command=${2:-${BASH_COMMAND:-unknown}}
|
||||
local line_number=${BASH_LINENO[0]:-unknown}
|
||||
|
||||
command="${command//\$STD/}"
|
||||
|
||||
# If error originated from silent(), use its captured metadata
|
||||
if [[ -n "${_SILENT_FAILED_RC:-}" ]]; then
|
||||
exit_code="$_SILENT_FAILED_RC"
|
||||
command="$_SILENT_FAILED_CMD"
|
||||
line_number="$_SILENT_FAILED_LINE"
|
||||
unset _SILENT_FAILED_RC _SILENT_FAILED_CMD _SILENT_FAILED_LINE
|
||||
fi
|
||||
|
||||
if [[ "$exit_code" -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Export the failure location so telemetry can include the "where"
|
||||
FAILED_COMMAND="$command"
|
||||
FAILED_LINE="$line_number"
|
||||
export FAILED_COMMAND FAILED_LINE
|
||||
|
||||
# Stop spinner and restore cursor FIRST - before any output
|
||||
if declare -f stop_spinner >/dev/null 2>&1; then
|
||||
stop_spinner 2>/dev/null || true
|
||||
fi
|
||||
printf "\e[?25h"
|
||||
|
||||
local explanation
|
||||
explanation="$(explain_exit_code "$exit_code")"
|
||||
if [[ "$explanation" == curl:* && ! "$command" =~ (^|[[:space:]])([^[:space:]]*/)?curl([[:space:]]|$) ]]; then
|
||||
explanation="Command failed with exit status ${exit_code}"
|
||||
fi
|
||||
|
||||
# ── Telemetry / failure artifacts ──
|
||||
if _is_container_context; then
|
||||
_container_write_failure "$exit_code" "$command" "$line_number"
|
||||
elif declare -f post_update_to_api &>/dev/null; then
|
||||
post_update_to_api "failed" "$exit_code" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── Display ──
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "in line ${line_number}: exit code ${exit_code} (${explanation}): while executing command ${command}"
|
||||
else
|
||||
echo -e "\n${RD}[ERROR]${CL} in line ${RD}${line_number}${CL}: exit code ${RD}${exit_code}${CL} (${explanation}): while executing command ${YWB}${command}${CL}\n"
|
||||
fi
|
||||
|
||||
if [[ -n "${DEBUG_LOGFILE:-}" ]]; then
|
||||
{
|
||||
echo "------ ERROR ------"
|
||||
echo "Timestamp : $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "Exit Code : $exit_code ($explanation)"
|
||||
echo "Line : $line_number"
|
||||
echo "Command : $command"
|
||||
echo "-------------------"
|
||||
} >>"$DEBUG_LOGFILE"
|
||||
fi
|
||||
|
||||
# Get active log file (prefer silent()'s logfile when available)
|
||||
local active_log=""
|
||||
if [[ -n "${_SILENT_FAILED_LOG:-}" && -s "${_SILENT_FAILED_LOG}" ]]; then
|
||||
active_log="$_SILENT_FAILED_LOG"
|
||||
unset _SILENT_FAILED_LOG
|
||||
elif declare -f get_active_logfile >/dev/null 2>&1; then
|
||||
active_log="$(get_active_logfile)"
|
||||
elif [[ -n "${SILENT_LOGFILE:-}" ]]; then
|
||||
active_log="$SILENT_LOGFILE"
|
||||
fi
|
||||
|
||||
if [[ -n "$active_log" && ! -s "$active_log" && -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then
|
||||
active_log="$BUILD_LOG"
|
||||
fi
|
||||
|
||||
# stderr: the trap can fire inside a $(...) capture that would eat this as data
|
||||
if [[ -n "$active_log" && -s "$active_log" ]]; then
|
||||
echo -e "\n${TAB}--- Last 20 lines of log ---" >&2
|
||||
tail -n 20 "$active_log" >&2
|
||||
echo -e "${TAB}-----------------------------------\n" >&2
|
||||
fi
|
||||
|
||||
# ── Node.js heap OOM detection with actionable guidance ──
|
||||
local node_oom_detected="false"
|
||||
local node_build_context="false"
|
||||
if [[ "$command" =~ (npm|pnpm|yarn|node|vite|turbo) ]]; then
|
||||
node_build_context="true"
|
||||
fi
|
||||
if [[ "$exit_code" == "243" ]]; then
|
||||
node_oom_detected="true"
|
||||
elif [[ -n "$active_log" && -s "$active_log" ]]; then
|
||||
if tail -n 200 "$active_log" 2>/dev/null | grep -Eqi 'Reached heap limit|JavaScript heap out of memory|Allocation failed - JavaScript heap out of memory|FATAL ERROR: Reached heap limit'; then
|
||||
node_oom_detected="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$node_oom_detected" == "true" ]] || { [[ "$node_build_context" == "true" ]] && [[ "$exit_code" =~ ^(134|137)$ ]]; }; then
|
||||
local heap_hint_mb=""
|
||||
|
||||
if [[ -n "${NODE_OPTIONS:-}" ]] && [[ "${NODE_OPTIONS}" =~ max-old-space-size=([0-9]+) ]]; then
|
||||
heap_hint_mb="${BASH_REMATCH[1]}"
|
||||
elif [[ -n "${var_ram:-}" ]] && [[ "${var_ram}" =~ ^[0-9]+$ ]]; then
|
||||
heap_hint_mb=$((var_ram * 75 / 100))
|
||||
else
|
||||
local mem_kb=""
|
||||
mem_kb=$(awk '/^MemTotal:/ {print $2; exit}' /proc/meminfo 2>/dev/null || echo "")
|
||||
if [[ "$mem_kb" =~ ^[0-9]+$ ]]; then
|
||||
local mem_mb=$((mem_kb / 1024))
|
||||
heap_hint_mb=$((mem_mb * 75 / 100))
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$heap_hint_mb" ]] || ((heap_hint_mb < 1024)); then
|
||||
heap_hint_mb=1024
|
||||
elif ((heap_hint_mb > 12288)); then
|
||||
heap_hint_mb=12288
|
||||
fi
|
||||
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "Possible Node.js heap OOM. Try: export NODE_OPTIONS=\"--max-old-space-size=${heap_hint_mb}\" and rerun the build."
|
||||
else
|
||||
echo -e "${YW}Possible Node.js heap OOM. Try: export NODE_OPTIONS=\"--max-old-space-size=${heap_hint_mb}\" and rerun the build.${CL}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Log-pattern analysis: actionable hints for common failure causes ──
|
||||
if [[ -n "$active_log" && -s "$active_log" ]]; then
|
||||
local _log_tail
|
||||
_log_tail=$(tail -n 60 "$active_log" 2>/dev/null || true)
|
||||
|
||||
# 1. APT/dpkg dependency conflict
|
||||
if echo "$_log_tail" | grep -qE "Depends:|depends on.*but.*not installed|broken packages|unmet dep|dependency problems"; then
|
||||
local _pg_conflict
|
||||
_pg_conflict=$(echo "$_log_tail" | grep -oE 'postgresql-[0-9]+ but.*installed' | head -1 || true)
|
||||
if [[ -n "$_pg_conflict" ]]; then
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "PostgreSQL version conflict: ${_pg_conflict}"
|
||||
msg_warn "Hint: A package requires a specific PostgreSQL version that is not installed. Your distribution may have installed a different PG version than expected."
|
||||
fi
|
||||
else
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "APT dependency conflict detected. A required package is not available or is the wrong version for this system."
|
||||
msg_warn "Hint: Run 'apt-get install -f' inside the container or check that all required repositories are configured for your distribution."
|
||||
fi
|
||||
fi
|
||||
# 2. APT/GPG signature verification failure
|
||||
elif echo "$_log_tail" | grep -qE "sqv|KEYEXPIRED|NO_PUBKEY|key is not certified|signature verification failed|is not signed|Sub-process.*sqv"; then
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "APT repository signature error detected."
|
||||
msg_warn "Hint: A repository GPG key may be missing, expired, or the keyring file is not yet present (/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc etc.)."
|
||||
msg_warn "Hint: Install the 'postgresql-common' package first, or re-add the repository with its correct signing key."
|
||||
fi
|
||||
# 3. Network / DNS failure
|
||||
elif echo "$_log_tail" | grep -qE "Could not resolve|Failed to fetch|Unable to connect|Name or service not known|Network is unreachable|curl.*resolve"; then
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "Network or DNS failure detected."
|
||||
msg_warn "Hint: Check the container's network connectivity, DNS settings, and whether any firewall or ad-blocker is intercepting traffic."
|
||||
fi
|
||||
# 4. APT lock held by another process
|
||||
elif echo "$_log_tail" | grep -qE "Could not get lock|dpkg frontend lock|waiting for it to exit|E: Unable to lock"; then
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "APT or dpkg lock conflict detected."
|
||||
msg_warn "Hint: Another package manager process may be running. Try 'rm /var/lib/dpkg/lock-frontend && dpkg --configure -a' inside the container."
|
||||
fi
|
||||
# 5. Disk space exhaustion
|
||||
elif echo "$_log_tail" | grep -qE "No space left on device|disk quota exceeded|ENOSPC"; then
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "Disk space exhausted during installation."
|
||||
msg_warn "Hint: Increase the container's disk size (pct resize <ctid> rootfs +2G) or clean up space first."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Context-specific cleanup ──
|
||||
if _is_container_context; then
|
||||
# Container: artifacts already written above; nothing more to do
|
||||
:
|
||||
else
|
||||
# HOST: show log path and offer container cleanup
|
||||
if [[ -n "$active_log" && -s "$active_log" ]]; then
|
||||
if declare -f msg_custom >/dev/null 2>&1; then
|
||||
msg_custom "📋" "${YW}" "Full log: ${active_log}"
|
||||
else
|
||||
echo -e "${YW}Full log:${CL} ${BL}${active_log}${CL}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Offer to remove container if it exists (build errors after container creation)
|
||||
if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then
|
||||
echo ""
|
||||
if declare -f msg_custom >/dev/null 2>&1; then
|
||||
echo -en "${TAB}❓${TAB}${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
|
||||
else
|
||||
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
|
||||
fi
|
||||
|
||||
local response=""
|
||||
local read_rc
|
||||
declare -f reclaim_tty >/dev/null 2>&1 && reclaim_tty
|
||||
read -t 60 -r response </dev/tty
|
||||
read_rc=$?
|
||||
if [[ $read_rc -eq 0 ]]; then
|
||||
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
if declare -f msg_info >/dev/null 2>&1; then
|
||||
msg_info "Removing container ${CTID}"
|
||||
else
|
||||
echo -e "${YW}Removing container ${CTID}${CL}"
|
||||
fi
|
||||
pct stop "$CTID" &>/dev/null || true
|
||||
pct destroy "$CTID" &>/dev/null || true
|
||||
if declare -f msg_ok >/dev/null 2>&1; then
|
||||
msg_ok "Container ${CTID} removed"
|
||||
else
|
||||
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||
fi
|
||||
elif [[ "$response" =~ ^[Nn]$ ]]; then
|
||||
echo ""
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "Container ${CTID} kept for debugging"
|
||||
else
|
||||
echo -e "${YW}Container ${CTID} kept for debugging${CL}"
|
||||
fi
|
||||
fi
|
||||
elif [[ $read_rc -gt 128 ]]; then
|
||||
# Timeout - auto-remove
|
||||
echo ""
|
||||
if declare -f msg_info >/dev/null 2>&1; then
|
||||
msg_info "No response - removing container ${CTID}"
|
||||
else
|
||||
echo -e "${YW}No response - removing container ${CTID}${CL}"
|
||||
fi
|
||||
pct stop "$CTID" &>/dev/null || true
|
||||
pct destroy "$CTID" &>/dev/null || true
|
||||
if declare -f msg_ok >/dev/null 2>&1; then
|
||||
msg_ok "Container ${CTID} removed"
|
||||
else
|
||||
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||
fi
|
||||
else
|
||||
# read itself failed (e.g. broken/closed stdin, I/O error) rather than
|
||||
# timing out - don't guess and destroy the container on a read we
|
||||
# couldn't actually capture; keep it since that's the reversible choice.
|
||||
while read -r -t 0.1 -n 1 _ </dev/tty 2>/dev/null; do :; done
|
||||
echo ""
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "Could not read your response (stdin error) - keeping container ${CTID} for safety."
|
||||
msg_error "Remove it manually if not needed: pct destroy ${CTID}"
|
||||
else
|
||||
echo -e "${YW}Could not read your response (stdin error) - keeping container ${CTID} for safety.${CL}"
|
||||
echo -e "${YW}Remove it manually if not needed: pct destroy ${CTID}${CL}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 4: TELEMETRY & CLEANUP HELPERS FOR SIGNAL HANDLERS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _send_abort_telemetry() (compatibility name)
|
||||
#
|
||||
# - HOST: reports via post_update_to_api (signals map to "aborted" there)
|
||||
# - CONTAINER: writes failure artifacts instead of sending anything
|
||||
# - Arguments: $1 = exit_code
|
||||
# ------------------------------------------------------------------------------
|
||||
_send_abort_telemetry() {
|
||||
local exit_code="${1:-1}"
|
||||
if _is_container_context; then
|
||||
_container_write_failure "$exit_code"
|
||||
return 0
|
||||
fi
|
||||
if declare -f post_update_to_api &>/dev/null; then
|
||||
post_update_to_api "failed" "$exit_code" 2>/dev/null || true
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _stop_container_if_installing()
|
||||
#
|
||||
# - Stops the LXC container if we're in the install phase (host only)
|
||||
# - Prevents orphaned container processes when the host exits due to a signal
|
||||
# ------------------------------------------------------------------------------
|
||||
_stop_container_if_installing() {
|
||||
[[ "${CONTAINER_INSTALLING:-}" == "true" ]] || return 0
|
||||
[[ -n "${CTID:-}" ]] || return 0
|
||||
command -v pct &>/dev/null || return 0
|
||||
pct stop "$CTID" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 5: SIGNAL HANDLERS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_exit()
|
||||
#
|
||||
# - EXIT trap handler — runs on EVERY script termination
|
||||
# - CONTAINER: ensures failure artifacts exist on non-zero exit (this also
|
||||
# covers silent()'s direct `exit $rc`, which bypasses the ERR trap)
|
||||
# - HOST: catches executions that never sent a final status:
|
||||
# * non-zero exit → "failed" (signal codes map to "aborted")
|
||||
# * zero exit with an "installing" record but no final → "aborted"
|
||||
# (e.g. user cancelled a whiptail dialog, script exited cleanly)
|
||||
# ------------------------------------------------------------------------------
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
|
||||
if _is_container_context; then
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
_container_write_failure "$exit_code" "${FAILED_COMMAND:-}" "${FAILED_LINE:-}"
|
||||
fi
|
||||
else
|
||||
if [[ "${POST_UPDATE_DONE:-}" != "true" ]] && declare -f post_update_to_api >/dev/null 2>&1; then
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
post_update_to_api "failed" "$exit_code" 2>/dev/null || true
|
||||
elif [[ "${POST_TO_API_DONE:-}" == "true" ]]; then
|
||||
# Clean exit but no success was ever reported: the user backed out
|
||||
# somewhere. Report as aborted so the record doesn't stay "installing".
|
||||
post_update_to_api "aborted" "0" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Best-effort log collection on failure
|
||||
if [[ $exit_code -ne 0 ]] && declare -f ensure_log_on_host >/dev/null 2>&1; then
|
||||
ensure_log_on_host 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Stop orphaned container if we're in the install phase
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
_stop_container_if_installing
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_interrupt() - SIGINT (Ctrl+C)
|
||||
# ------------------------------------------------------------------------------
|
||||
on_interrupt() {
|
||||
if declare -f stop_spinner >/dev/null 2>&1; then
|
||||
stop_spinner 2>/dev/null || true
|
||||
fi
|
||||
printf "\e[?25h" 2>/dev/null || true
|
||||
|
||||
if _is_container_context; then
|
||||
_container_write_failure "130"
|
||||
elif declare -f post_update_to_api &>/dev/null; then
|
||||
post_update_to_api "aborted" "130" 2>/dev/null || true
|
||||
fi
|
||||
_stop_container_if_installing
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "Interrupted by user (SIGINT)" 2>/dev/null || true
|
||||
else
|
||||
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}" 2>/dev/null || true
|
||||
fi
|
||||
exit 130
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_terminate() - SIGTERM
|
||||
# ------------------------------------------------------------------------------
|
||||
on_terminate() {
|
||||
if declare -f stop_spinner >/dev/null 2>&1; then
|
||||
stop_spinner 2>/dev/null || true
|
||||
fi
|
||||
printf "\e[?25h" 2>/dev/null || true
|
||||
|
||||
if _is_container_context; then
|
||||
_container_write_failure "143"
|
||||
elif declare -f post_update_to_api &>/dev/null; then
|
||||
post_update_to_api "aborted" "143" 2>/dev/null || true
|
||||
fi
|
||||
_stop_container_if_installing
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "Terminated by signal (SIGTERM)" 2>/dev/null || true
|
||||
else
|
||||
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}" 2>/dev/null || true
|
||||
fi
|
||||
exit 143
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_hangup() - SIGHUP (SSH disconnect, terminal closed)
|
||||
# ------------------------------------------------------------------------------
|
||||
on_hangup() {
|
||||
if declare -f stop_spinner >/dev/null 2>&1; then
|
||||
stop_spinner 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if _is_container_context; then
|
||||
_container_write_failure "129"
|
||||
elif declare -f post_update_to_api &>/dev/null; then
|
||||
post_update_to_api "aborted" "129" 2>/dev/null || true
|
||||
fi
|
||||
_stop_container_if_installing
|
||||
exit 129
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 6: INITIALIZATION
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# catch_errors()
|
||||
#
|
||||
# - Initializes error handling and signal traps
|
||||
# - set -Ee -o pipefail (+ set -u when STRICT_UNSET=1)
|
||||
# - Traps: ERR → error_handler, EXIT → on_exit, INT/TERM/HUP → signal handlers
|
||||
# - Call this function early in every script
|
||||
# ------------------------------------------------------------------------------
|
||||
catch_errors() {
|
||||
set -Ee -o pipefail
|
||||
if [ "${STRICT_UNSET:-0}" = "1" ]; then
|
||||
set -u
|
||||
fi
|
||||
|
||||
trap 'error_handler' ERR
|
||||
trap on_exit EXIT
|
||||
trap on_interrupt INT
|
||||
trap on_terminate TERM
|
||||
trap on_hangup HUP
|
||||
}
|
||||
@@ -1,552 +0,0 @@
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: tteck (tteckster)
|
||||
# Co-Author: MickLesk
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/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 <app>-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 update >/dev/null 2>&1
|
||||
apt install -y curl >/dev/null 2>&1
|
||||
fi
|
||||
# Mark container context BEFORE error handling starts: error_handler/on_exit
|
||||
# must write local failure artifacts instead of talking to the telemetry API
|
||||
# (the host is the single telemetry reporter).
|
||||
export TELEMETRY_CONTEXT="container"
|
||||
|
||||
# A freshly booted container may not have DNS up yet. `source <(curl ...)` hides
|
||||
# curl's exit code, so a failed download would silently source an empty stream.
|
||||
_bootstrap_die() {
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "$1"
|
||||
else
|
||||
echo "FATAL: $1" >&2
|
||||
fi
|
||||
exit 115
|
||||
}
|
||||
|
||||
_bootstrap_source() {
|
||||
local url="$1" probe="$2" content
|
||||
content=$(curl -fsSL --connect-timeout 10 --retry 5 --retry-connrefused --retry-delay 2 "$url") ||
|
||||
_bootstrap_die "Failed to download ${url##*/}"
|
||||
source /dev/stdin <<<"$content"
|
||||
declare -f "$probe" >/dev/null 2>&1 ||
|
||||
_bootstrap_die "${url##*/} loaded but incomplete (missing ${probe})"
|
||||
}
|
||||
|
||||
_FUNC_BASE="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc"
|
||||
_bootstrap_source "$_FUNC_BASE/core.func" load_functions
|
||||
_bootstrap_source "$_FUNC_BASE/error_handler.func" catch_errors
|
||||
load_functions
|
||||
catch_errors
|
||||
|
||||
# Persist diagnostics setting inside container (exported from build.func)
|
||||
# so addon scripts running later can find the user's choice
|
||||
if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then
|
||||
mkdir -p /usr/local/community-scripts
|
||||
echo "DIAGNOSTICS=${DIAGNOSTICS:-no}" >/usr/local/community-scripts/diagnostics
|
||||
fi
|
||||
|
||||
# Get LXC IP address (must be called INSIDE container, after network is up)
|
||||
get_lxc_ip
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# post_progress_to_api()
|
||||
#
|
||||
# - Lightweight progress ping from inside the container
|
||||
# - Updates the existing telemetry record status
|
||||
# - Arguments:
|
||||
# * $1: status (optional, default: "configuring")
|
||||
# - Signals that the installation is actively progressing (not stuck)
|
||||
# - Fire-and-forget: never blocks or fails the script
|
||||
# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
|
||||
# ------------------------------------------------------------------------------
|
||||
post_progress_to_api() {
|
||||
command -v curl &>/dev/null || return 0
|
||||
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
||||
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
||||
|
||||
local progress_status="${1:-configuring}"
|
||||
|
||||
# Progress pings are the ONLY telemetry a container sends (terminal statuses
|
||||
# are reported by the host). Include execution_id + platform + repo
|
||||
# attribution (exported by the host) so the server can correlate and filter.
|
||||
curl -fsS -m 5 -X POST "https://telemetry.community-scripts.org/telemetry" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"lxc\",\"nsapp\":\"${app:-unknown}\",\"status\":\"${progress_status}\",\"platform\":\"${TELEMETRY_PLATFORM:-}\",\"repo_source\":\"${REPO_SOURCE:-}\",\"repo_slug\":\"${REPO_SLUG:-}\"}" &>/dev/null || true
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 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 [ "${IPV6_METHOD:-}" = "disable" ]; then
|
||||
msg_info "Disabling IPv6 (this may affect some services)"
|
||||
mkdir -p /etc/sysctl.d
|
||||
$STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null <<EOF
|
||||
# Disable IPv6 (set by community-scripts)
|
||||
net.ipv6.conf.all.disable_ipv6 = 1
|
||||
net.ipv6.conf.default.disable_ipv6 = 1
|
||||
net.ipv6.conf.lo.disable_ipv6 = 1
|
||||
EOF
|
||||
$STD sysctl -p /etc/sysctl.d/99-disable-ipv6.conf
|
||||
msg_ok "Disabled IPv6"
|
||||
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"
|
||||
|
||||
# Fix Debian 13 LXC template bug where / is owned by nobody
|
||||
# Only attempt in privileged containers (unprivileged cannot chown /)
|
||||
if [[ "$(stat -c '%U' /)" != "root" ]]; then
|
||||
(chown root:root / 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
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 121
|
||||
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)"
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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? <y/N> " prompt </dev/tty
|
||||
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||
echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
|
||||
else
|
||||
echo -e "${NETWORK}Check Network Settings"
|
||||
exit 122
|
||||
fi
|
||||
fi
|
||||
|
||||
# DNS resolution checks for GitHub-related domains (IPv4 and/or IPv6)
|
||||
GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com")
|
||||
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' ERR
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 3: OS UPDATE & PACKAGE MANAGEMENT
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# apt_update_safe()
|
||||
#
|
||||
# - Runs apt-get update with CDN mirror fallback
|
||||
# - On failure, detects distro (Debian/Ubuntu) and tries alternate mirrors
|
||||
# - Three-phase approach: global mirrors → primary mirror → regional mirrors
|
||||
# - Falls back to manual user prompt if all auto mirrors fail
|
||||
# - Detects hash mismatch, SSL errors, and generic apt failures
|
||||
# ------------------------------------------------------------------------------
|
||||
apt_update_safe() {
|
||||
if $STD apt-get update; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local failed_mirror
|
||||
failed_mirror=$(grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null || echo "unknown")
|
||||
msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..."
|
||||
|
||||
local distro
|
||||
distro=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")
|
||||
|
||||
local eu_mirrors us_mirrors ap_mirrors
|
||||
if [[ "$distro" == "ubuntu" ]]; then
|
||||
eu_mirrors="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de"
|
||||
us_mirrors="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu"
|
||||
ap_mirrors="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au"
|
||||
else
|
||||
eu_mirrors="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
|
||||
us_mirrors="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
|
||||
ap_mirrors="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
|
||||
fi
|
||||
|
||||
local tz regional others
|
||||
tz=$(cat /etc/timezone 2>/dev/null || echo "UTC")
|
||||
case "$tz" in
|
||||
Europe/* | Arctic/*)
|
||||
regional="$eu_mirrors"
|
||||
others="$us_mirrors $ap_mirrors"
|
||||
;;
|
||||
America/*)
|
||||
regional="$us_mirrors"
|
||||
others="$eu_mirrors $ap_mirrors"
|
||||
;;
|
||||
Asia/* | Australia/* | Pacific/*)
|
||||
regional="$ap_mirrors"
|
||||
others="$eu_mirrors $us_mirrors"
|
||||
;;
|
||||
*)
|
||||
regional=""
|
||||
others="$eu_mirrors $us_mirrors $ap_mirrors"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo 'Acquire::By-Hash "no";' >/etc/apt/apt.conf.d/99no-by-hash
|
||||
|
||||
_try_apt_mirror() {
|
||||
local m=$1
|
||||
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
|
||||
[[ -f "$src" ]] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${m}/|g; s|deb http[s]*://[^/]*/|deb http://${m}/|g" "$src"
|
||||
done
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
local out
|
||||
out=$(apt-get update 2>&1)
|
||||
if echo "$out" | grep -qi "hashsum\|hash sum"; then
|
||||
msg_warn "Mirror ${m} failed (hash mismatch)"
|
||||
return 1
|
||||
elif echo "$out" | grep -qi "SSL\|certificate"; then
|
||||
msg_warn "Mirror ${m} failed (SSL/certificate error)"
|
||||
return 1
|
||||
elif echo "$out" | grep -q "^E:"; then
|
||||
msg_warn "Mirror ${m} failed (apt-get update error)"
|
||||
return 1
|
||||
else
|
||||
msg_ok "CDN set to ${m}: tests passed"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
_scan_reachable() {
|
||||
local result=""
|
||||
for m in $1; do
|
||||
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
|
||||
result="$result $m"
|
||||
fi
|
||||
done
|
||||
echo "$result" | xargs
|
||||
}
|
||||
|
||||
local apt_ok=false
|
||||
|
||||
# Phase 1: Scan global mirrors first (independent of local CDN issues)
|
||||
local others_ok
|
||||
others_ok=$(_scan_reachable "$others")
|
||||
local others_pick
|
||||
others_pick=$(printf '%s\n' $others_ok | shuf | head -3 | xargs)
|
||||
|
||||
for mirror in $others_pick; do
|
||||
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}"
|
||||
if _try_apt_mirror "$mirror"; then
|
||||
apt_ok=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Phase 2: Try primary mirror
|
||||
if [[ "$apt_ok" != true ]]; then
|
||||
local primary
|
||||
if [[ "$distro" == "ubuntu" ]]; then
|
||||
primary="archive.ubuntu.com"
|
||||
else
|
||||
primary="ftp.debian.org"
|
||||
fi
|
||||
if timeout 2 bash -c "echo >/dev/tcp/$primary/80" 2>/dev/null; then
|
||||
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${primary}"
|
||||
if _try_apt_mirror "$primary"; then
|
||||
apt_ok=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Phase 3: Fall back to regional mirrors
|
||||
if [[ "$apt_ok" != true ]]; then
|
||||
local regional_ok
|
||||
regional_ok=$(_scan_reachable "$regional")
|
||||
local regional_pick
|
||||
regional_pick=$(printf '%s\n' $regional_ok | shuf | head -3 | xargs)
|
||||
|
||||
for mirror in $regional_pick; do
|
||||
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}"
|
||||
if _try_apt_mirror "$mirror"; then
|
||||
apt_ok=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Phase 4: All auto mirrors failed, prompt user
|
||||
if [[ "$apt_ok" != true ]]; then
|
||||
msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
|
||||
if [[ "$distro" == "ubuntu" ]]; then
|
||||
msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors"
|
||||
else
|
||||
msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list"
|
||||
fi
|
||||
local custom_mirror
|
||||
while true; do
|
||||
read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror </dev/tty
|
||||
[[ -z "$custom_mirror" ]] && continue
|
||||
[[ "$custom_mirror" == "skip" ]] && break
|
||||
[[ ! "$custom_mirror" =~ ^[a-zA-Z0-9._-]+$ ]] && {
|
||||
msg_warn "Invalid hostname format."
|
||||
continue
|
||||
}
|
||||
if _try_apt_mirror "$custom_mirror"; then
|
||||
apt_ok=true
|
||||
break
|
||||
fi
|
||||
msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$apt_ok" != true ]]; then
|
||||
msg_error "All mirrors failed. Check network or try again later."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# update_os()
|
||||
#
|
||||
# - 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"
|
||||
configure_http_proxy
|
||||
if [[ "$CACHER" == "yes" && -z "${HTTP_PROXY:-${http_proxy:-}}" ]]; then
|
||||
echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy
|
||||
local _proxy_raw="${CACHER_IP}"
|
||||
local _proxy_host _proxy_port _proxy_url
|
||||
# Parse host and port from URL or plain IP/hostname
|
||||
_proxy_host=$(echo "$_proxy_raw" | sed -e 's|https\?://||' -e 's|/.*||' | cut -d: -f1)
|
||||
_proxy_port=$(echo "$_proxy_raw" | sed -e 's|https\?://||' -e 's|/.*||' | cut -s -d: -f2)
|
||||
if [[ "$_proxy_raw" =~ ^https?:// ]]; then
|
||||
# Full URL provided — use as-is for proxy output, extract port for nc check
|
||||
_proxy_url="$_proxy_raw"
|
||||
_proxy_port="${_proxy_port:-80}"
|
||||
else
|
||||
# Legacy: plain IP or hostname — default to http + port 3142
|
||||
_proxy_port="${_proxy_port:-3142}"
|
||||
_proxy_url="http://${_proxy_raw}:${_proxy_port}"
|
||||
fi
|
||||
cat <<EOF >/usr/local/bin/apt-proxy-detect.sh
|
||||
#!/bin/bash
|
||||
if nc -w1 -z "${_proxy_host}" ${_proxy_port}; then
|
||||
echo -n "${_proxy_url}"
|
||||
else
|
||||
echo -n "DIRECT"
|
||||
fi
|
||||
EOF
|
||||
chmod +x /usr/local/bin/apt-proxy-detect.sh
|
||||
fi
|
||||
apt_update_safe
|
||||
$STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade
|
||||
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||
msg_ok "Updated Container OS"
|
||||
post_progress_to_api
|
||||
|
||||
local tools_content
|
||||
tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) || {
|
||||
msg_error "Failed to download tools.func"
|
||||
exit 115
|
||||
}
|
||||
source /dev/stdin <<<"$tools_content"
|
||||
if ! declare -f fetch_and_deploy_gh_release >/dev/null 2>&1; then
|
||||
msg_error "tools.func loaded but incomplete — missing expected functions"
|
||||
exit 115
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 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() {
|
||||
# Set terminal to 256-color mode
|
||||
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
|
||||
|
||||
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
|
||||
|
||||
echo "[ -t 1 ] || return 0" >"$PROFILE_FILE"
|
||||
echo "echo -e \"\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
|
||||
echo "echo \"\"" >>"$PROFILE_FILE"
|
||||
cat <<EOF >>"$PROFILE_FILE"
|
||||
os_display="Unknown OS"
|
||||
if [ -r /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
os_display="\${PRETTY_NAME:-\${NAME:-Unknown OS}}"
|
||||
fi
|
||||
echo -e "${TAB}${OS}${YW} OS: ${GN}\${os_display}${CL}"
|
||||
EOF
|
||||
echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE"
|
||||
|
||||
# Disable default MOTD scripts
|
||||
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
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# 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 <<EOF >"$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 'set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/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
|
||||
post_progress_to_api
|
||||
}
|
||||
-10293
File diff suppressed because it is too large.
Load diff
-1138
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
# This file is NOT meant to be executed as-is.
|
||||
# It is a collection of complete, copy-pasteable example hooks for the
|
||||
# optional `var_post_install` feature in build.func.
|
||||
# optional `var_post_install` feature in the core engine.
|
||||
#
|
||||
# HOW IT WORKS
|
||||
# ------------
|
||||
@@ -118,7 +118,7 @@ TS="$(date -Iseconds)"
|
||||
read -r -d '' DISCORD_PAYLOAD <<JSON || true
|
||||
{
|
||||
"username": "Proxmox - ${NODE}",
|
||||
"avatar_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png",
|
||||
"avatar_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/.github/assets/logo-81x112.png",
|
||||
"embeds": [{
|
||||
"title": "✅ ${APP} LXC created",
|
||||
"description": "A new community-script LXC has been provisioned on **${NODE}**.",
|
||||
@@ -173,7 +173,7 @@ set -euo pipefail
|
||||
|
||||
# --- CONFIG (edit me) -------------------------------------------------------
|
||||
TARGET_POOL="auto-lxc"
|
||||
EXTRA_TAGS=("auto-provisioned" "${NSAPP}") # community-script tag is set by build.func
|
||||
EXTRA_TAGS=("auto-provisioned" "${NSAPP}") # community-script tag is set by the engine
|
||||
BACKUP_STORAGE="pbs-main" # set to "" to skip initial backup
|
||||
PIHOLE_HOST="192.168.1.5"
|
||||
PIHOLE_PASSWORD="changeme" # web-UI password
|
||||
|
||||
+2
-19
@@ -391,24 +391,7 @@ msg_ok "Created VM shell"
|
||||
# ==============================================================================
|
||||
# DISK IMPORT
|
||||
# ==============================================================================
|
||||
msg_info "Importing disk into storage ($STORAGE)"
|
||||
|
||||
if qm disk import --help >/dev/null 2>&1; then
|
||||
IMPORT_CMD=(qm disk import)
|
||||
else
|
||||
IMPORT_CMD=(qm importdisk)
|
||||
fi
|
||||
|
||||
IMPORT_OUT="$("${IMPORT_CMD[@]}" "$VMID" "$WORK_FILE" "$STORAGE" ${DISK_IMPORT:-} 2>&1 || true)"
|
||||
DISK_REF_IMPORTED="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p" | tr -d "\r\"'")"
|
||||
[[ -z "$DISK_REF_IMPORTED" ]] && DISK_REF_IMPORTED="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$5 ~ ("vm-"id"-disk-") {print $1":"$5}' | sort | tail -n1)"
|
||||
[[ -z "$DISK_REF_IMPORTED" ]] && {
|
||||
msg_error "Unable to determine imported disk reference."
|
||||
echo "$IMPORT_OUT"
|
||||
exit 226
|
||||
}
|
||||
|
||||
msg_ok "Imported disk (${CL}${BL}${DISK_REF_IMPORTED}${CL})"
|
||||
vm_import_disk "$VMID" "$WORK_FILE" "$STORAGE" "${DISK_IMPORT#--format }" || exit 226
|
||||
|
||||
# Clean up work file
|
||||
rm -f "$WORK_FILE"
|
||||
@@ -420,7 +403,7 @@ msg_info "Attaching EFI and root disk"
|
||||
|
||||
qm set "$VMID" \
|
||||
--efidisk0 "${STORAGE}:0,efitype=4m" \
|
||||
--scsi0 "${DISK_REF_IMPORTED},${DISK_CACHE}${THIN%,}" \
|
||||
--scsi0 "${VM_IMPORTED_DISK},${DISK_CACHE}${THIN%,}" \
|
||||
--boot order=scsi0 \
|
||||
--serial0 socket >/dev/null
|
||||
|
||||
|
||||
Reference in new issue
Block a user