Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b135f95e9 | |||
| c566b53552 | |||
| 0e65b9469e | |||
| ef7f811a7a | |||
| b357b7843d | |||
| daf1a50364 | |||
| 1d190bdf25 | |||
| 2ace018034 | |||
| 9b65262e42 | |||
| 3faab0c7aa | |||
| cff71279aa | |||
| 9b56da1411 | |||
| cfd741c322 | |||
| 2372c4b4b0 |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 997 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@@ -77,17 +77,6 @@
|
|||||||
"excludeGlobs": []
|
"excludeGlobs": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"core": [
|
|
||||||
{
|
|
||||||
"fileStatus": null,
|
|
||||||
"includeGlobs": [
|
|
||||||
"misc/*.func"
|
|
||||||
],
|
|
||||||
"excludeGlobs": [
|
|
||||||
"misc/api.func"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"documentation": [
|
"documentation": [
|
||||||
{
|
{
|
||||||
"fileStatus": null,
|
"fileStatus": null,
|
||||||
@@ -114,8 +103,7 @@
|
|||||||
{
|
{
|
||||||
"fileStatus": null,
|
"fileStatus": null,
|
||||||
"includeGlobs": [
|
"includeGlobs": [
|
||||||
"api/**",
|
"api/**"
|
||||||
"misc/api.func"
|
|
||||||
],
|
],
|
||||||
"excludeGlobs": []
|
"excludeGlobs": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,15 +267,14 @@ jobs:
|
|||||||
const stubPath = path.join('ct', slug + '.sh');
|
const stubPath = path.join('ct', slug + '.sh');
|
||||||
const content = [
|
const content = [
|
||||||
"#!/usr/bin/env bash",
|
"#!/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",
|
"# Copyright (c) 2021-2026 community-scripts ORG",
|
||||||
"# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE",
|
"# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE",
|
||||||
"",
|
"",
|
||||||
`APP="${appName.replace(/"/g, '\\"')}"`,
|
`APP="${appName.replace(/"/g, '\\"')}"`,
|
||||||
"",
|
"",
|
||||||
'header_info "$APP"',
|
|
||||||
'variables',
|
|
||||||
'color',
|
'color',
|
||||||
|
'header_info',
|
||||||
"",
|
"",
|
||||||
'msg_error "This script is no longer available in community-scripts."',
|
'msg_error "This script is no longer available in community-scripts."',
|
||||||
`msg_error "${deletedMessage.replace(/"/g, '\\"')}"`,
|
`msg_error "${deletedMessage.replace(/"/g, '\\"')}"`,
|
||||||
|
|||||||
@@ -184,8 +184,8 @@ jobs:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
'Only the script itself comes from this branch. Whatever it loads, the engine',
|
'Only the script itself comes from this branch. The engine it loads still',
|
||||||
'or the `misc/` helpers, still comes from `main`.',
|
'comes from community-scripts/core on `main`.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ name: Sync ct/install to Incus
|
|||||||
#
|
#
|
||||||
# The actual mirroring + transformation lives in community-scripts/Incus
|
# The actual mirroring + transformation lives in community-scripts/Incus
|
||||||
# (.github/workflows/sync-scripts.yml): it rewrites each script's bootstrap line
|
# (.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
|
# to load the engine from community-scripts/core (ct scripts here load its PVE
|
||||||
# a monolith with no Incus backend), so a plain 1:1 copy would break every Incus
|
# 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
|
# 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).
|
# source of truth do the work (and auto-approve + merge its own PR).
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="#">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<h1 align="center">Changelog</h1>
|
<h1 align="center">Changelog</h1>
|
||||||
@@ -568,6 +568,31 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
|||||||
|
|
||||||
</details>
|
</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
|
## 2026-10-29
|
||||||
|
|
||||||
### 🚀 Updated Scripts
|
### 🚀 Updated Scripts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div align="center">
|
<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>
|
<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: misc/images/community-scripts.svg; PNG export: 512 × 512. -->
|
<!-- Editable artwork: .github/assets/community-scripts.svg; PNG export: 512 × 512. -->
|
||||||
|
|
||||||
<h1>Community Scripts — Proxmox VE</h1>
|
<h1>Community Scripts — Proxmox VE</h1>
|
||||||
<p><strong>One-command installations for services, containers and VMs on Proxmox VE</strong><br/>
|
<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 |
|
| `vm/` | Virtual machine scripts |
|
||||||
| `tools/pve/` | Host tooling: post-install, hardware passthrough, maintenance |
|
| `tools/pve/` | Host tooling: post-install, hardware passthrough, maintenance |
|
||||||
| `turnkey/` | TurnKey Linux appliance helper |
|
| `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,
|
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
|
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}"
|
||||||
@@ -51,16 +51,31 @@ function update_script() {
|
|||||||
cd /opt/discourse
|
cd /opt/discourse
|
||||||
export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
|
export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
|
||||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||||
|
export BUNDLE_SILENCE_ROOT_WARNING=1
|
||||||
set -a
|
set -a
|
||||||
source /opt/discourse/.env
|
source /opt/discourse/.env
|
||||||
set +a
|
set +a
|
||||||
$STD git pull origin main
|
$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
|
$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 runuser -u postgres -- psql -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||||
$STD bundle exec rails db:migrate
|
$STD bundle exec rails db:migrate
|
||||||
|
msg_ok "Ran Database Migrations"
|
||||||
|
|
||||||
|
msg_info "Precompiling Assets"
|
||||||
$STD bundle exec rails assets:precompile
|
$STD bundle exec rails assets:precompile
|
||||||
msg_ok "Updated Discourse"
|
msg_ok "Precompiled Assets"
|
||||||
|
|
||||||
msg_info "Starting Services"
|
msg_info "Starting Services"
|
||||||
systemctl start discourse discourse-sidekiq
|
systemctl start discourse discourse-sidekiq
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ function update_script() {
|
|||||||
echo "Seerr is the new unified Jellyseerr and Overseerr."
|
echo "Seerr is the new unified Jellyseerr and Overseerr."
|
||||||
echo "More info: https://docs.seerr.dev/blog/seerr-release"
|
echo "More info: https://docs.seerr.dev/blog/seerr-release"
|
||||||
echo
|
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
|
read -rp "Do you want to migrate to Seerr now? (y/N): " MIGRATE
|
||||||
echo
|
echo
|
||||||
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ function update_script() {
|
|||||||
|
|
||||||
NODE_VERSION="24" setup_nodejs
|
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/')
|
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)
|
LATEST=$(npm view matter-server version 2>/dev/null)
|
||||||
if [[ $CURRENT != "$LATEST" ]]; then
|
if [[ $CURRENT != "$LATEST" ]]; then
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ function update_script() {
|
|||||||
echo "Seerr is the new unified Jellyseerr and Overseerr."
|
echo "Seerr is the new unified Jellyseerr and Overseerr."
|
||||||
echo "More info: https://docs.seerr.dev/blog/seerr-release"
|
echo "More info: https://docs.seerr.dev/blog/seerr-release"
|
||||||
echo
|
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
|
read -rp "Do you want to migrate to Seerr now? (y/N): " MIGRATE
|
||||||
echo
|
echo
|
||||||
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
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}"
|
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."
|
msg_warn "Before continuing make sure that you do not use encryption or have decrypted all documents."
|
||||||
echo ""
|
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
|
read -rp "Do you want to continue with the update? (y/N): " MIGRATE
|
||||||
echo
|
echo
|
||||||
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
||||||
@@ -71,6 +75,11 @@ function update_script() {
|
|||||||
fi
|
fi
|
||||||
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"
|
msg_info "Stopping all Paperless-ngx Services"
|
||||||
systemctl stop paperless-consumer paperless-webserver paperless-scheduler paperless-task-queue
|
systemctl stop paperless-consumer paperless-webserver paperless-scheduler paperless-task-queue
|
||||||
msg_ok "Stopped all Paperless-ngx Services"
|
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}"
|
||||||
@@ -36,6 +36,10 @@ function update_script() {
|
|||||||
msg_warn "This requires MANUAL config changes in /etc/vikunja/config.yml."
|
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"
|
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
|
read -rp "Continue with update? (y to proceed): " -t 30 CONFIRM1 || exit 254
|
||||||
[[ "$CONFIRM1" =~ ^[yY]$ ]] || exit 0
|
[[ "$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}${YW}How do you want to handle /etc/vikunja/config.yml?${CL}"
|
||||||
echo -e "${TAB} 1) Keep your current config"
|
echo -e "${TAB} 1) Keep your current config"
|
||||||
echo -e "${TAB} 2) Install the new package maintainer's 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"
|
[[ -z "$CONFIG_CHOICE" ]] && CONFIG_CHOICE="1"
|
||||||
|
|
||||||
if [[ "$CONFIG_CHOICE" == "2" ]]; then
|
if [[ "$CONFIG_CHOICE" == "2" ]]; then
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ service:
|
|||||||
url: community-scripts/ProxmoxVE
|
url: community-scripts/ProxmoxVE
|
||||||
use_prerelease: false
|
use_prerelease: false
|
||||||
dashboard:
|
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/
|
icon_link_to: https://community-scripts.org/
|
||||||
web_url: https://github.com/community-scripts/ProxmoxVE/releases
|
web_url: https://github.com/community-scripts/ProxmoxVE/releases
|
||||||
EOF
|
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
|
redis-server
|
||||||
msg_ok "Installed Dependencies"
|
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
|
PG_VERSION="17" PG_MODULES="pgvector" setup_postgresql
|
||||||
NODE_VERSION="24" setup_nodejs
|
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"
|
msg_info "Configuring PostgreSQL for Discourse"
|
||||||
DISCOURSE_DB_PASS=$(random_password 13)
|
DISCOURSE_DB_PASS=$(random_password 13)
|
||||||
@@ -42,7 +47,6 @@ msg_ok "Configured PostgreSQL for Discourse"
|
|||||||
|
|
||||||
msg_info "Configuring Discourse"
|
msg_info "Configuring Discourse"
|
||||||
DISCOURSE_SECRET_KEY=$(openssl rand -hex 64)
|
DISCOURSE_SECRET_KEY=$(openssl rand -hex 64)
|
||||||
$STD git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse
|
|
||||||
cd /opt/discourse
|
cd /opt/discourse
|
||||||
cat <<EOF >/opt/discourse/.env
|
cat <<EOF >/opt/discourse/.env
|
||||||
RAILS_ENV=production
|
RAILS_ENV=production
|
||||||
@@ -77,6 +81,7 @@ export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
|
|||||||
eval "$(rbenv init - bash)" 2>/dev/null || true
|
eval "$(rbenv init - bash)" 2>/dev/null || true
|
||||||
export RAILS_ENV=production
|
export RAILS_ENV=production
|
||||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||||
|
export BUNDLE_SILENCE_ROOT_WARNING=1
|
||||||
$STD corepack enable
|
$STD corepack enable
|
||||||
$STD bundle config set --local deployment true
|
$STD bundle config set --local deployment true
|
||||||
$STD bundle config set --local without 'test development'
|
$STD bundle config set --local without 'test development'
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
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
|
ExecStart=/usr/bin/node /opt/matter-server/node_modules/matter-server/dist/esm/MatterServer.js --storage-path /var/lib/matterjs-server
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
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)"
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# This file is NOT meant to be executed as-is.
|
# This file is NOT meant to be executed as-is.
|
||||||
# It is a collection of complete, copy-pasteable example hooks for the
|
# 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
|
# HOW IT WORKS
|
||||||
# ------------
|
# ------------
|
||||||
@@ -118,7 +118,7 @@ TS="$(date -Iseconds)"
|
|||||||
read -r -d '' DISCORD_PAYLOAD <<JSON || true
|
read -r -d '' DISCORD_PAYLOAD <<JSON || true
|
||||||
{
|
{
|
||||||
"username": "Proxmox - ${NODE}",
|
"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": [{
|
"embeds": [{
|
||||||
"title": "✅ ${APP} LXC created",
|
"title": "✅ ${APP} LXC created",
|
||||||
"description": "A new community-script LXC has been provisioned on **${NODE}**.",
|
"description": "A new community-script LXC has been provisioned on **${NODE}**.",
|
||||||
@@ -173,7 +173,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
# --- CONFIG (edit me) -------------------------------------------------------
|
# --- CONFIG (edit me) -------------------------------------------------------
|
||||||
TARGET_POOL="auto-lxc"
|
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
|
BACKUP_STORAGE="pbs-main" # set to "" to skip initial backup
|
||||||
PIHOLE_HOST="192.168.1.5"
|
PIHOLE_HOST="192.168.1.5"
|
||||||
PIHOLE_PASSWORD="changeme" # web-UI password
|
PIHOLE_PASSWORD="changeme" # web-UI password
|
||||||
|
|||||||