diff --git a/.editorconfig b/.editorconfig index f79a823d7..5886a1c76 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ indent_style = space insert_final_newline = true max_line_length = 120 tab_width = 2 -; trim_trailing_whitespace = true ; disabled until files are cleaned up +trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false diff --git a/.github/workflows/auto-update-app-headers.yml.bak b/.github/workflows/bak/auto-update-app-headers.yml.bak similarity index 100% rename from .github/workflows/auto-update-app-headers.yml.bak rename to .github/workflows/bak/auto-update-app-headers.yml.bak diff --git a/.github/workflows/get-versions-from-gh.yaml b/.github/workflows/bak/get-versions-from-gh.yaml similarity index 100% rename from .github/workflows/get-versions-from-gh.yaml rename to .github/workflows/bak/get-versions-from-gh.yaml diff --git a/.github/workflows/get-versions-from-newreleases.yaml b/.github/workflows/bak/get-versions-from-newreleases.yaml similarity index 100% rename from .github/workflows/get-versions-from-newreleases.yaml rename to .github/workflows/bak/get-versions-from-newreleases.yaml diff --git a/.github/workflows/revision-bump.yml.bak b/.github/workflows/bak/revision-bump.yml.bak similarity index 100% rename from .github/workflows/revision-bump.yml.bak rename to .github/workflows/bak/revision-bump.yml.bak diff --git a/.github/workflows/script-test.yaml b/.github/workflows/bak/script-test.yaml similarity index 100% rename from .github/workflows/script-test.yaml rename to .github/workflows/bak/script-test.yaml diff --git a/.github/workflows/update-versions-github.yml b/.github/workflows/bak/update-versions-github.yml similarity index 98% rename from .github/workflows/update-versions-github.yml rename to .github/workflows/bak/update-versions-github.yml index 5c18ef2e5..9e94b9686 100644 --- a/.github/workflows/update-versions-github.yml +++ b/.github/workflows/bak/update-versions-github.yml @@ -25,13 +25,6 @@ jobs: with: ref: main - - name: Generate GitHub App Token - id: generate-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Extract version sources from install scripts run: | set -euo pipefail @@ -330,7 +323,7 @@ jobs: - name: Fetch versions for all sources env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail @@ -453,7 +446,7 @@ jobs: - name: Create Pull Request if: steps.check-changes.outputs.changed == 'true' env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | BRANCH_NAME="automated/update-versions-$(date +%Y%m%d)" diff --git a/.github/workflows/update_issue.yml b/.github/workflows/bak/update_issue.yml similarity index 100% rename from .github/workflows/update_issue.yml rename to .github/workflows/bak/update_issue.yml diff --git a/.github/workflows/update-github-versions.yml b/.github/workflows/update-github-versions.yml new file mode 100644 index 000000000..9f3680a8c --- /dev/null +++ b/.github/workflows/update-github-versions.yml @@ -0,0 +1,218 @@ +name: Update GitHub Versions (New) + +on: + workflow_dispatch: + schedule: + # Runs 4x daily: 00:00, 06:00, 12:00, 18:00 UTC + - cron: "0 0,6,12,18 * * *" + +permissions: + contents: write + pull-requests: write + +env: + VERSIONS_FILE: frontend/public/json/github-versions.json + +jobs: + update-github-versions: + if: github.repository == 'community-scripts/ProxmoxVED' + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Extract GitHub versions from install scripts + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + echo "=========================================" + echo " Extracting GitHub versions from scripts" + echo "=========================================" + + # Initialize versions array + versions_json="[]" + + # Function to add a version entry + add_version() { + local slug="$1" + local repo="$2" + local version="$3" + local pinned="$4" + local date="$5" + + versions_json=$(echo "$versions_json" | jq \ + --arg slug "$slug" \ + --arg repo "$repo" \ + --arg version "$version" \ + --argjson pinned "$pinned" \ + --arg date "$date" \ + '. += [{"slug": $slug, "repo": $repo, "version": $version, "pinned": $pinned, "date": $date}]') + } + + # Get list of slugs from JSON files + echo "" + echo "=== Scanning JSON files for slugs ===" + + for json_file in frontend/public/json/*.json; do + [[ ! -f "$json_file" ]] && continue + + # Skip non-app JSON files + basename_file=$(basename "$json_file") + case "$basename_file" in + metadata.json|versions.json|github-versions.json|dependency-check.json|update-apps.json) + continue + ;; + esac + + # Extract slug from JSON + slug=$(jq -r '.slug // empty' "$json_file" 2>/dev/null) + [[ -z "$slug" ]] && continue + + # Find corresponding install script + install_script="install/${slug}-install.sh" + [[ ! -f "$install_script" ]] && continue + + # Look for fetch_and_deploy_gh_release calls + # Pattern: fetch_and_deploy_gh_release "app" "owner/repo" ["mode"] ["version"] + while IFS= read -r line; do + # Skip commented lines + [[ "$line" =~ ^[[:space:]]*# ]] && continue + + # Extract repo and version from fetch_and_deploy_gh_release + if [[ "$line" =~ fetch_and_deploy_gh_release[[:space:]]+\"[^\"]*\"[[:space:]]+\"([^\"]+)\"([[:space:]]+\"([^\"]+)\")?([[:space:]]+\"([^\"]+)\")? ]]; then + repo="${BASH_REMATCH[1]}" + mode="${BASH_REMATCH[3]:-tarball}" + pinned_version="${BASH_REMATCH[5]:-latest}" + + # Check if version is pinned (not "latest" and not empty) + is_pinned=false + target_version="" + + if [[ -n "$pinned_version" && "$pinned_version" != "latest" ]]; then + is_pinned=true + target_version="$pinned_version" + fi + + # Fetch version from GitHub + if [[ "$is_pinned" == "true" ]]; then + # For pinned versions, verify it exists and get date + response=$(gh api "repos/${repo}/releases/tags/${target_version}" 2>/dev/null || echo '{}') + if echo "$response" | jq -e '.tag_name' > /dev/null 2>&1; then + version=$(echo "$response" | jq -r '.tag_name') + date=$(echo "$response" | jq -r '.published_at // empty') + add_version "$slug" "$repo" "$version" "true" "$date" + echo "[$slug] ✓ $version (pinned)" + else + echo "[$slug] ⚠ pinned version $target_version not found" + fi + else + # Fetch latest release + response=$(gh api "repos/${repo}/releases/latest" 2>/dev/null || echo '{}') + if echo "$response" | jq -e '.tag_name' > /dev/null 2>&1; then + version=$(echo "$response" | jq -r '.tag_name') + date=$(echo "$response" | jq -r '.published_at // empty') + add_version "$slug" "$repo" "$version" "false" "$date" + echo "[$slug] ✓ $version" + else + # Try tags as fallback + version=$(gh api "repos/${repo}/tags" --jq '.[0].name // empty' 2>/dev/null || echo "") + if [[ -n "$version" ]]; then + add_version "$slug" "$repo" "$version" "false" "" + echo "[$slug] ✓ $version (from tags)" + else + echo "[$slug] ⚠ no version found" + fi + fi + fi + + break # Only first match per script + fi + done < <(grep 'fetch_and_deploy_gh_release' "$install_script" 2>/dev/null || true) + + done + + # Save versions file + echo "$versions_json" | jq --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + '{generated: $date, versions: (. | sort_by(.slug))}' > "$VERSIONS_FILE" + + total=$(echo "$versions_json" | jq 'length') + echo "" + echo "=========================================" + echo " Total versions extracted: $total" + echo "=========================================" + + - name: Check for changes + id: check-changes + run: | + # Check if file is new (untracked) or has changes + if [[ ! -f "$VERSIONS_FILE" ]]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "Versions file was not created" + elif ! git ls-files --error-unmatch "$VERSIONS_FILE" &>/dev/null; then + # File exists but is not tracked - it's new + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "New file created: $VERSIONS_FILE" + elif git diff --quiet "$VERSIONS_FILE" 2>/dev/null; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No changes detected" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "Changes detected:" + git diff --stat "$VERSIONS_FILE" 2>/dev/null || true + fi + + - name: Create Pull Request + if: steps.check-changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH_NAME="automated/update-github-versions-$(date +%Y%m%d)" + + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "GitHub Actions[bot]" + + # Check if branch exists and delete it + git push origin --delete "$BRANCH_NAME" 2>/dev/null || true + + git checkout -b "$BRANCH_NAME" + git add "$VERSIONS_FILE" + git commit -m "chore: update github-versions.json + + Total versions: $(jq '.versions | length' "$VERSIONS_FILE") + Pinned versions: $(jq '[.versions[] | select(.pinned == true)] | length' "$VERSIONS_FILE") + Generated: $(jq -r '.generated' "$VERSIONS_FILE")" + + git push origin "$BRANCH_NAME" --force + + # Check if PR already exists + existing_pr=$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number // empty') + + if [[ -n "$existing_pr" ]]; then + echo "PR #$existing_pr already exists, updating..." + else + gh pr create \ + --title "[Automated] Update GitHub versions" \ + --body "This PR updates version information from GitHub releases. + + ## How it works + 1. Scans all JSON files in \`frontend/public/json/\` for slugs + 2. Finds corresponding \`install/{slug}-install.sh\` scripts + 3. Extracts \`fetch_and_deploy_gh_release\` calls + 4. Fetches latest (or pinned) version from GitHub + + ## Stats + - Total versions: $(jq '.versions | length' "$VERSIONS_FILE") + - Pinned versions: $(jq '[.versions[] | select(.pinned == true)] | length' "$VERSIONS_FILE") + - Latest versions: $(jq '[.versions[] | select(.pinned == false)] | length' "$VERSIONS_FILE") + + --- + *Automatically generated from install scripts*" \ + --base main \ + --head "$BRANCH_NAME" \ + --label "automated pr" + fi diff --git a/.vscode/.shellcheckrc b/.vscode/.shellcheckrc deleted file mode 100644 index 4631bd3af..000000000 --- a/.vscode/.shellcheckrc +++ /dev/null @@ -1 +0,0 @@ -disable=SC2034,SC1091,SC2155,SC2086,SC2317,SC2181,SC2164 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1949e6fc5..5749d3b1d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,7 @@ { "recommendations": [ - "bmalehorn.shell-syntax", "timonwong.shellcheck", - "foxundermoon.shell-format" + "mkhl.shfmt" ], "unwantedRecommendations": [] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 8f17c7ff9..fed1b294f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,33 +1,12 @@ { - "files.associations": { - "*.func": "shellscript" - }, - "files.eol": "\n", - "files.encoding": "utf8", - "files.trimTrailingWhitespace": true, - "files.insertFinalNewline": true, - "files.autoSave": "afterDelay", - "files.autoGuessEncoding": false, - "editor.detectIndentation": false, - "editor.tabSize": 4, - "editor.insertSpaces": true, - "editor.wordWrap": "off", - "editor.renderWhitespace": "boundary", - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "editor.minimap.enabled": false, - "terminal.integrated.scrollback": 10000, - "[shellscript]": { - "editor.defaultFormatter": "foxundermoon.shell-format", - "editor.tabSize": 4, - "editor.insertSpaces": true, - }, - "shellcheck.customArgs": [ - "--rcfile", - ".vscode/.shellcheckrc" - ], - "git.autofetch": true, - "git.confirmSync": false, - "git.enableSmartCommit": true, - "extensions.ignoreRecommendations": false + "files.associations": { + "*.func": "shellscript" + }, + "[shellscript]": { + "editor.defaultFormatter": "mkhl.shfmt" + }, + "editor.codeActionsOnSave": { + "source.fixAll": "never" + }, + "shellcheck.useWorkspaceRootAsCwd": true, } diff --git a/ct/alpine-rustypaste.sh b/ct/alpine-powerdns.sh similarity index 61% rename from ct/alpine-rustypaste.sh rename to ct/alpine-powerdns.sh index 893d0d05d..21ce7f0b5 100644 --- a/ct/alpine-rustypaste.sh +++ b/ct/alpine-powerdns.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/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/orhun/rustypaste +# Author: Slaviša Arežina (tremor021) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://www.powerdns.com/ -APP="Alpine-RustyPaste" -var_tags="${var_tags:-alpine;pastebin;storage}" +APP="Alpine-PowerDNS" +var_tags="${var_tags:-os;alpine;dns}" var_cpu="${var_cpu:-1}" var_ram="${var_ram:-256}" var_disk="${var_disk:-4}" @@ -24,18 +24,17 @@ function update_script() { check_container_storage check_container_resources - if ! apk info -e rustypaste >/dev/null 2>&1; then + if ! apk info -e pdns >/dev/null 2>&1; then msg_error "No ${APP} Installation Found!" exit fi - msg_info "Updating RustyPaste" - $STD apk update - $STD apk upgrade rustypaste --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community - msg_ok "Updated RustyPaste" + msg_info "Updating PowerDNS" + $STD apk -U upgrade + msg_ok "Updated PowerDNS" msg_info "Restarting Services" - $STD rc-service rustypaste restart + $STD rc-service pdns restart msg_ok "Restarted Services" msg_ok "Updated successfully!" exit @@ -48,4 +47,4 @@ description msg_ok "Completed successfully!\n" echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8000${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:53${CL}" diff --git a/ct/alpine-valkey.sh b/ct/alpine-valkey.sh deleted file mode 100644 index 2765aff7f..000000000 --- a/ct/alpine-valkey.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: pshankinclarke (lazarillo) -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Source: https://valkey.io/ - -APP="Alpine-Valkey" -var_tags="${var_tags:-alpine;database}" -var_cpu="${var_cpu:-1}" -var_ram="${var_ram:-256}" -var_disk="${var_disk:-1}" -var_os="${var_os:-alpine}" -var_version="${var_version:-3.22}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - if ! apk -e info newt >/dev/null 2>&1; then - apk add -q newt - fi - LXCIP=$(ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - while true; do - CHOICE=$( - whiptail --backtitle "Proxmox VE Helper Scripts" --title "Valkey Management" --menu "Select option" 11 58 3 \ - "1" "Update Valkey" \ - "2" "Allow 0.0.0.0 for listening" \ - "3" "Allow only ${LXCIP} for listening" 3>&2 2>&1 1>&3 - ) - exit_status=$? - if [ $exit_status == 1 ]; then - clear - exit-script - fi - header_info - case $CHOICE in - 1) - msg_info "Updating Valkey" - apk update && apk upgrade valkey - rc-service valkey restart - msg_ok "Updated Valkey" - msg_ok "Updated successfully!" - exit - ;; - 2) - msg_info "Setting Valkey to listen on all interfaces" - sed -i 's/^bind .*/bind 0.0.0.0/' /etc/valkey/valkey.conf - rc-service valkey restart - msg_ok "Valkey now listens on all interfaces!" - exit - ;; - 3) - msg_info "Setting Valkey to listen only on ${LXCIP}" - sed -i "s/^bind .*/bind ${LXCIP}/" /etc/valkey/valkey.conf - rc-service valkey restart - msg_ok "Valkey now listens only on ${LXCIP}!" - exit - ;; - esac - done -} - -start -build_container -description - -msg_ok "Completed successfully!\n" -echo -e "${APP} should be reachable on port 6379. - ${BL}valkey-cli -h ${IP} -p 6379${CL} \n" diff --git a/ct/ampache.sh b/ct/ampache.sh deleted file mode 100644 index ef6bb7903..000000000 --- a/ct/ampache.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (Canbiz) -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Source: https://github.com/ampache/ampache - -APP="Ampache" -var_tags="${var_tags:-music}" -var_disk="${var_disk:-5}" -var_cpu="${var_cpu:-4}" -var_ram="${var_ram:-2048}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - - if [[ ! -d /opt/ampache ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - if check_for_gh_release "Ampache" "ampache/ampache"; then - import_local_ip - msg_info "Stopping Apache" - systemctl stop apache2 - msg_ok "Stopped Apache" - - msg_info "Backing up Configuration" - cp /opt/ampache/config/ampache.cfg.php /tmp/ampache.cfg.php.backup - cp /opt/ampache/public/rest/.htaccess /tmp/ampache_rest.htaccess.backup - cp /opt/ampache/public/play/.htaccess /tmp/ampache_play.htaccess.backup - msg_ok "Backed up Configuration" - - msg_info "Backup Ampache Folder" - rm -rf /opt/ampache_backup - mv /opt/ampache /opt/ampache_backup - msg_ok "Backed up Ampache" - - fetch_and_deploy_gh_release "Ampache" "ampache/ampache" "release" "latest" "/opt/ampache" "ampache-*_all_php8.4.zip" - - msg_info "Restoring Configuration" - cp /tmp/ampache.cfg.php.backup /opt/ampache/config/ampache.cfg.php - cp /tmp/ampache_rest.htaccess.backup /opt/ampache/public/rest/.htaccess - cp /tmp/ampache_play.htaccess.backup /opt/ampache/public/play/.htaccess - chmod 664 /opt/ampache/public/rest/.htaccess /opt/ampache/public/play/.htaccess - chown -R www-data:www-data /opt/ampache - msg_ok "Restored Configuration" - - msg_info "Cleaning up" - rm -f /tmp/ampache*.backup - msg_ok "Cleaned up" - - msg_info "Starting Apache" - systemctl start apache2 - msg_ok "Started Apache" - msg_ok "Updated successfully!" - msg_custom "⚠️" "${YW}" "Complete database update by visiting: http://${LOCAL_IP}/update.php" - fi - exit -} - -start -build_container -description - -msg_ok "Completed successfully!\n" -echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}/install.php${CL}" diff --git a/ct/anytype.sh b/ct/anytype.sh new file mode 100644 index 000000000..7525e1eaf --- /dev/null +++ b/ct/anytype.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://anytype.io + +APP="Anytype" +var_tags="${var_tags:-notes;productivity;sync}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-4096}" +var_disk="${var_disk:-16}" +var_os="${var_os:-ubuntu}" +var_version="${var_version:-24.04}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -f /opt/anytype/any-sync-bundle ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "anytype" "grishy/any-sync-bundle"; then + msg_info "Stopping Service" + systemctl stop anytype + msg_ok "Stopped Service" + + msg_info "Backing up Data" + cp -r /opt/anytype/data /opt/anytype_data_backup + msg_ok "Backed up Data" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz" + chmod +x /opt/anytype/any-sync-bundle + + msg_info "Restoring Data" + cp -r /opt/anytype_data_backup/. /opt/anytype/data + rm -rf /opt/anytype_data_backup + msg_ok "Restored Data" + + msg_info "Starting Service" + systemctl start anytype + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:33010${CL}" +echo -e "${INFO}${YW} Client config file:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}/opt/anytype/data/client-config.yml${CL}" diff --git a/ct/authelia.sh b/ct/authelia.sh new file mode 100644 index 000000000..0831c4db1 --- /dev/null +++ b/ct/authelia.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: thost96 (thost96) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://www.authelia.com/ + +APP="Authelia" +var_tags="${var_tags:-authenticator}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-512}" +var_disk="${var_disk:-2}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +base_settings + +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -d /etc/authelia/ ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "authelia" "authelia/authelia"; then + $STD apt update + $STD apt -y upgrade + fetch_and_deploy_gh_release "authelia" "authelia/authelia" "binary" + msg_ok "Updated successfully!" + fi + exit +} +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:9091 or https://auth.YOURDOMAIN ${CL}" diff --git a/ct/checkmate.sh b/ct/checkmate.sh new file mode 100644 index 000000000..a9fb4c442 --- /dev/null +++ b/ct/checkmate.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/bluewave-labs/Checkmate + +APP="Checkmate" +var_tags="${var_tags:-monitoring;uptime}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-4096}" +var_disk="${var_disk:-10}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /opt/checkmate ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "checkmate" "bluewave-labs/Checkmate"; then + msg_info "Stopping Services" + systemctl stop checkmate-server checkmate-client + msg_ok "Stopped Services" + + msg_info "Backing up Data" + cp /opt/checkmate/server/.env /opt/checkmate_server.env.bak + cp /opt/checkmate/client/.env /opt/checkmate_client.env.bak + msg_ok "Backed up Data" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "checkmate" "bluewave-labs/Checkmate" + + msg_info "Updating Checkmate" + cd /opt/checkmate/server + $STD npm install + cd /opt/checkmate/client + $STD npm install + $STD npm run build + msg_ok "Updated Checkmate" + + msg_info "Restoring Data" + mv /opt/checkmate_server.env.bak /opt/checkmate/server/.env + mv /opt/checkmate_client.env.bak /opt/checkmate/client/.env + msg_ok "Restored Data" + + msg_info "Starting Services" + systemctl start checkmate-server checkmate-client + msg_ok "Started Services" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:5173${CL}" diff --git a/ct/clawdbot.sh b/ct/clawdbot.sh new file mode 100644 index 000000000..7f147dff9 --- /dev/null +++ b/ct/clawdbot.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: michelroegl-brunner +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/clawdbot/clawdbot + +APP="Clawdbot" +var_tags="${var_tags:-ai;assistant}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-2048}" +var_disk="${var_disk:-8}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if ! command -v clawdbot >/dev/null 2>&1; then + msg_error "No ${APP} Installation Found!" + exit + fi + + msg_info "Backing up Data" + cp -r /opt/clawdbot/data /opt/clawdbot_data_backup 2>/dev/null || true + cp -r /root/.clawdbot /root/.clawdbot_backup 2>/dev/null || true + msg_ok "Backed up Data" + + msg_info "Updating Clawdbot" + $STD npm install -g clawdbot@latest + msg_ok "Updated Clawdbot" + + msg_info "Restoring Data" + cp -r /opt/clawdbot_data_backup/. /opt/clawdbot/data 2>/dev/null || true + cp -r /root/.clawdbot_backup/. /root/.clawdbot 2>/dev/null || true + rm -rf /opt/clawdbot_data_backup /root/.clawdbot_backup + msg_ok "Restored Data" + msg_ok "Updated successfully!" + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:18791${CL}" + diff --git a/ct/dawarich.sh b/ct/dawarich.sh deleted file mode 100644 index e4ba70a28..000000000 --- a/ct/dawarich.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/Freika/dawarich - -APP="Dawarich" -var_tags="${var_tags:-location;tracking;gps}" -var_cpu="${var_cpu:-4}" -var_ram="${var_ram:-4096}" -var_disk="${var_disk:-15}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - - if [[ ! -d /opt/dawarich ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - if check_for_gh_release "dawarich" "Freika/dawarich"; then - msg_info "Stopping Services" - systemctl stop dawarich-web dawarich-worker - msg_ok "Stopped Services" - - msg_info "Backing up Data" - cp -r /opt/dawarich/app/storage /opt/dawarich_storage_backup 2>/dev/null || true - cp /opt/dawarich/app/config/master.key /opt/dawarich_master.key 2>/dev/null || true - cp /opt/dawarich/app/config/credentials.yml.enc /opt/dawarich_credentials.yml.enc 2>/dev/null || true - msg_ok "Backed up Data" - - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "dawarich" "Freika/dawarich" "tarball" "latest" "/opt/dawarich/app" - - RUBY_VERSION=$(cat /opt/dawarich/app/.ruby-version 2>/dev/null || echo "3.4.6") - RUBY_VERSION=${RUBY_VERSION} RUBY_INSTALL_RAILS="false" setup_ruby - - msg_info "Running Migrations" - cd /opt/dawarich/app - source /root/.profile - export PATH="/root/.rbenv/shims:/root/.rbenv/bin:$PATH" - eval "$(/root/.rbenv/bin/rbenv init - bash)" - - set -a && source /opt/dawarich/.env && set +a - - $STD bundle config set --local deployment 'true' - $STD bundle config set --local without 'development test' - $STD bundle install - - if [[ -f /opt/dawarich/package.json ]]; then - cd /opt/dawarich - $STD npm install - cd /opt/dawarich/app - elif [[ -f /opt/dawarich/app/package.json ]]; then - $STD npm install - fi - - $STD bundle exec rake assets:precompile - $STD bundle exec rails db:migrate - $STD bundle exec rake data:migrate - msg_ok "Ran Migrations" - - msg_info "Restoring Data" - cp -r /opt/dawarich_storage_backup/. /opt/dawarich/app/storage/ 2>/dev/null || true - cp /opt/dawarich_master.key /opt/dawarich/app/config/master.key 2>/dev/null || true - cp /opt/dawarich_credentials.yml.enc /opt/dawarich/app/config/credentials.yml.enc 2>/dev/null || true - rm -rf /opt/dawarich_storage_backup /opt/dawarich_master.key /opt/dawarich_credentials.yml.enc - msg_ok "Restored Data" - - msg_info "Starting Services" - systemctl start dawarich-web dawarich-worker - msg_ok "Started Services" - msg_ok "Updated successfully!" - fi - exit -} - -start -build_container -description - -msg_ok "Completed Successfully!\n" -echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}" diff --git a/ct/ebusd.sh b/ct/ebusd.sh new file mode 100644 index 000000000..5aa0f9076 --- /dev/null +++ b/ct/ebusd.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Joerg Heinemann (heinemannj) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/john30/ebusd + +APP="ebusd" +var_tags="${var_tags:-automation}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-512}" +var_disk="${var_disk:-2}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -f /etc/apt/sources.list.d/ebusd.sources ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + msg_info "Updating ebusd" + $STD apt update + $STD apt upgrade -y ebusd + msg_ok "Updated ebusd" + msg_ok "Updated successfully!" + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" diff --git a/ct/forgejo-runner.sh b/ct/forgejo-runner.sh index 1dfe36ebc..045ea256f 100644 --- a/ct/forgejo-runner.sh +++ b/ct/forgejo-runner.sh @@ -1,12 +1,11 @@ #!/usr/bin/env bash source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) - # Copyright (c) 2021-2026 community-scripts ORG # Author: Simon Friedrich # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # Source: https://forgejo.org/ -APP="Forgejo Runner" +APP="Forgejo-Runner" var_tags="${var_tags:-ci}" var_cpu="${var_cpu:-2}" var_ram="${var_ram:-2048}" diff --git a/ct/freepbx.sh b/ct/freepbx.sh index e16a1c47a..5b141e78e 100644 --- a/ct/freepbx.sh +++ b/ct/freepbx.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/refs/heads/freepbx/misc/build.func) # Copyright (c) 2021-2026 community-scripts ORG -# Author: Arian Nasr (arian-nasr) -# Updated by: Javier Pastor (vsc55) +# Author: Arian Nasr (arian-nasr) | Co-Author: Javier Pastor (vsc55) # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # Source: https://www.freepbx.org/ diff --git a/ct/frigate.sh b/ct/frigate.sh index ecdc17126..108d1bfbe 100644 --- a/ct/frigate.sh +++ b/ct/frigate.sh @@ -13,6 +13,7 @@ var_disk="${var_disk:-20}" var_os="${var_os:-debian}" var_version="${var_version:-12}" var_unprivileged="${var_unprivileged:-0}" +var_gpu="${var_gpu:-yes}" header_info "$APP" variables diff --git a/ct/ghost.sh b/ct/ghost.sh new file mode 100644 index 000000000..31fa44237 --- /dev/null +++ b/ct/ghost.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: fabrice1236 +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://ghost.org/ + +APP="Ghost" +var_tags="${var_tags:-cms;blog}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-5}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + setup_mariadb + NODE_VERSION="22" setup_nodejs + + msg_info "Updating Ghost" + if command -v ghost &>/dev/null; then + current_version=$(ghost version | grep 'Ghost-CLI version' | awk '{print $3}') + latest_version=$(npm show ghost-cli version) + if [ "$current_version" != "$latest_version" ]; then + msg_info "Updating ${APP} from version v${current_version} to v${latest_version}" + $STD npm install -g ghost-cli@latest + msg_ok "Updated successfully!" + else + msg_ok "${APP} is already at v${current_version}" + fi + else + msg_error "No ${APP} Installation Found!" + exit + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:2368${CL}" diff --git a/ct/headers/ampache b/ct/headers/ampache deleted file mode 100644 index 68d2c033d..000000000 --- a/ct/headers/ampache +++ /dev/null @@ -1,6 +0,0 @@ - ___ __ - / | ____ ___ ____ ____ ______/ /_ ___ - / /| | / __ `__ \/ __ \/ __ `/ ___/ __ \/ _ \ - / ___ |/ / / / / / /_/ / /_/ / /__/ / / / __/ -/_/ |_/_/ /_/ /_/ .___/\__,_/\___/_/ /_/\___/ - /_/ diff --git a/ct/headers/ebusd b/ct/headers/ebusd new file mode 100644 index 000000000..fec79a3e3 --- /dev/null +++ b/ct/headers/ebusd @@ -0,0 +1,6 @@ + __ __ + ___ / /_ _ ___________/ / + / _ \/ __ \/ / / / ___/ __ / +/ __/ /_/ / /_/ (__ ) /_/ / +\___/_.___/\__,_/____/\__,_/ + diff --git a/ct/headers/kitchenowl b/ct/headers/kitchenowl deleted file mode 100644 index 6f20ad457..000000000 --- a/ct/headers/kitchenowl +++ /dev/null @@ -1,6 +0,0 @@ - __ __ _ __ __ ____ __ - / //_/(_) /______/ /_ ___ ____ / __ \_ __/ / - / ,< / / __/ ___/ __ \/ _ \/ __ \/ / / / | /| / / / - / /| |/ / /_/ /__/ / / / __/ / / / /_/ /| |/ |/ / / -/_/ |_/_/\__/\___/_/ /_/\___/_/ /_/\____/ |__/|__/_/ - diff --git a/ct/headers/rustypaste b/ct/headers/rustypaste deleted file mode 100644 index c691b0a30..000000000 --- a/ct/headers/rustypaste +++ /dev/null @@ -1,6 +0,0 @@ - __ __ - _______ _______/ /___ ______ ____ ______/ /____ - / ___/ / / / ___/ __/ / / / __ \/ __ `/ ___/ __/ _ \ - / / / /_/ (__ ) /_/ /_/ / /_/ / /_/ (__ ) /_/ __/ -/_/ \__,_/____/\__/\__, / .___/\__,_/____/\__/\___/ - /____/_/ diff --git a/ct/immich.sh b/ct/immich.sh new file mode 100644 index 000000000..d96bbd99d --- /dev/null +++ b/ct/immich.sh @@ -0,0 +1,407 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: vhsdream +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://immich.app + +APP="immich" +var_tags="${var_tags:-photos}" +var_disk="${var_disk:-20}" +var_cpu="${var_cpu:-4}" +var_ram="${var_ram:-6144}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" +var_gpu="${var_gpu:-yes}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -d /opt/immich ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + if [[ -f /etc/apt/sources.list.d/immich.list ]]; then + msg_error "Wrong Debian version detected!" + msg_error "You must upgrade your LXC to Debian Trixie before updating." + msg_error "Please visit https://github.com/community-scripts/ProxmoxVE/discussions/7726 for details." + echo "${TAB3} If you have upgraded your LXC to Trixie and you still see this message, please open an Issue in the Community-Scripts repo." + exit + fi + + setup_uv + PNPM_VERSION="$(curl -fsSL "https://raw.githubusercontent.com/immich-app/immich/refs/heads/main/package.json" | jq -r '.packageManager | split("@")[1]')" + NODE_VERSION="24" NODE_MODULE="pnpm@${PNPM_VERSION}" setup_nodejs + + if [[ ! -f /etc/apt/preferences.d/preferences ]]; then + msg_info "Adding Debian Testing repo" + sed -i 's/ trixie-updates/ trixie-updates testing/g' /etc/apt/sources.list.d/debian.sources + cat </etc/apt/preferences.d/preferences +Package: * +Pin: release a=unstable +Pin-Priority: 450 + +Package: * +Pin:release a=testing +Pin-Priority: 450 +EOF + if [[ -f /etc/apt/preferences.d/immich ]]; then + rm /etc/apt/preferences.d/immich + fi + $STD apt update + msg_ok "Added Debian Testing repo" + fi + + if ! dpkg -l "libmimalloc3" | grep -q '3.1' || ! dpkg -l "libde265-dev" | grep -q '1.0.16'; then + msg_info "Installing/upgrading Testing repo packages" + $STD apt install -t testing libmimalloc3 libde265-dev -y + msg_ok "Installed/upgraded Testing repo packages" + fi + + if [[ ! -f /etc/apt/sources.list.d/mise.list ]]; then + msg_info "Installing Mise" + curl -fSs https://mise.jdx.dev/gpg-key.pub | tee /etc/apt/keyrings/mise-archive-keyring.pub 1>/dev/null + echo "deb [signed-by=/etc/apt/keyrings/mise-archive-keyring.pub arch=amd64] https://mise.jdx.dev/deb stable main" >/etc/apt/sources.list.d/mise.list + $STD apt update + $STD apt install -y mise + msg_ok "Installed Mise" + fi + + STAGING_DIR=/opt/staging + BASE_DIR=${STAGING_DIR}/base-images + SOURCE_DIR=${STAGING_DIR}/image-source + cd /tmp + if [[ -f ~/.intel_version ]]; then + curl -fsSLO https://raw.githubusercontent.com/immich-app/base-images/refs/heads/main/server/Dockerfile + readarray -t INTEL_URLS < <( + sed -n "/intel-[igc|opencl]/p" ./Dockerfile | awk '{print $2}' + sed -n "/libigdgmm12/p" ./Dockerfile | awk '{print $3}' + ) + INTEL_RELEASE="$(grep "intel-opencl-icd_" ./Dockerfile | awk -F '_' '{print $2}')" + if [[ "$INTEL_RELEASE" != "$(cat ~/.intel_version)" ]]; then + msg_info "Updating Intel iGPU dependencies" + for url in "${INTEL_URLS[@]}"; do + curl -fsSLO "$url" + done + $STD apt-mark unhold libigdgmm12 + $STD apt install -y ./libigdgmm12*.deb + rm ./libigdgmm12*.deb + $STD apt install -y ./*.deb + rm ./*.deb + $STD apt-mark hold libigdgmm12 + dpkg-query -W -f='${Version}\n' intel-opencl-icd >~/.intel_version + msg_ok "Intel iGPU dependencies updated" + fi + rm ./Dockerfile + fi + if [[ -f ~/.immich_library_revisions ]]; then + libraries=("libjxl" "libheif" "libraw" "imagemagick" "libvips") + cd "$BASE_DIR" + msg_info "Checking for updates to custom image-processing libraries" + $STD git pull + for library in "${libraries[@]}"; do + compile_"$library" + done + msg_ok "Image-processing libraries up to date" + fi + + RELEASE="v2.5.2" + if check_for_gh_tag "immich" "immich-app/immich" "${RELEASE}"; then + msg_info "Stopping Services" + systemctl stop immich-web + systemctl stop immich-ml + msg_ok "Stopped Services" + VCHORD_RELEASE="0.5.3" + if [[ ! -f ~/.vchord_version ]] || [[ "$VCHORD_RELEASE" != "$(cat ~/.vchord_version)" ]]; then + msg_info "Upgrading VectorChord" + curl -fsSL "https://github.com/tensorchord/vectorchord/releases/download/${VCHORD_RELEASE}/postgresql-16-vchord_${VCHORD_RELEASE}-1_amd64.deb" -o vchord.deb + $STD apt install -y ./vchord.deb + systemctl restart postgresql + $STD sudo -u postgres psql -d immich -c "ALTER EXTENSION vector UPDATE;" + $STD sudo -u postgres psql -d immich -c "ALTER EXTENSION vchord UPDATE;" + $STD sudo -u postgres psql -d immich -c "REINDEX INDEX face_index;" + $STD sudo -u postgres psql -d immich -c "REINDEX INDEX clip_index;" + echo "$VCHORD_RELEASE" >~/.vchord_version + rm ./vchord.deb + msg_ok "Upgraded VectorChord to v${VCHORD_RELEASE}" + fi + if ! dpkg -l | grep -q ccache; then + $STD apt install -yqq ccache + fi + + INSTALL_DIR="/opt/${APP}" + UPLOAD_DIR="$(sed -n '/^IMMICH_MEDIA_LOCATION/s/[^=]*=//p' /opt/immich/.env)" + SRC_DIR="${INSTALL_DIR}/source" + APP_DIR="${INSTALL_DIR}/app" + PLUGIN_DIR="${APP_DIR}/corePlugin" + ML_DIR="${APP_DIR}/machine-learning" + GEO_DIR="${INSTALL_DIR}/geodata" + + cp "$ML_DIR"/ml_start.sh "$INSTALL_DIR" + if grep -qs "set -a" "$APP_DIR"/bin/start.sh; then + cp "$APP_DIR"/bin/start.sh "$INSTALL_DIR" + else + cat <"$INSTALL_DIR"/start.sh +#!/usr/bin/env bash + +set -a +. ${INSTALL_DIR}/.env +set +a + +/usr/bin/node ${APP_DIR}/dist/main.js "\$@" +EOF + chmod +x "$INSTALL_DIR"/start.sh + fi + + ( + shopt -s dotglob + rm -rf "${APP_DIR:?}"/* + ) + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "immich" "immich-app/immich" "tag" "${RELEASE}" "$SRC_DIR" + + msg_info "Updating Immich web and microservices" + cd "$SRC_DIR"/server + export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 + export CI=1 + corepack enable + + # server build + export SHARP_IGNORE_GLOBAL_LIBVIPS=true + $STD pnpm --filter immich --frozen-lockfile build + unset SHARP_IGNORE_GLOBAL_LIBVIPS + export SHARP_FORCE_GLOBAL_LIBVIPS=true + $STD pnpm --filter immich --frozen-lockfile --prod --no-optional deploy "$APP_DIR" + cp "$APP_DIR"/package.json "$APP_DIR"/bin + sed -i 's|^start|./start|' "$APP_DIR"/bin/immich-admin + + # openapi & web build + cd "$SRC_DIR" + echo "packageImportMethod: hardlink" >>./pnpm-workspace.yaml + $STD pnpm --filter @immich/sdk --filter immich-web --frozen-lockfile --force install + unset SHARP_FORCE_GLOBAL_LIBVIPS + export SHARP_IGNORE_GLOBAL_LIBVIPS=true + $STD pnpm --filter @immich/sdk --filter immich-web build + cp -a web/build "$APP_DIR"/www + cp LICENSE "$APP_DIR" + + # cli build + $STD pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install + $STD pnpm --filter @immich/sdk --filter @immich/cli build + $STD pnpm --filter @immich/cli --prod --no-optional deploy "$APP_DIR"/cli + cd "$APP_DIR" + mv "$INSTALL_DIR"/start.sh "$APP_DIR"/bin + + # plugins + cd "$SRC_DIR" + $STD mise trust --ignore ./mise.toml + $STD mise trust ./plugins/mise.toml + cd plugins + $STD mise install + $STD mise run build + mkdir -p "$PLUGIN_DIR" + cp -r ./dist "$PLUGIN_DIR"/dist + cp ./manifest.json "$PLUGIN_DIR" + msg_ok "Updated Immich server, web, cli and plugins" + + cd "$SRC_DIR"/machine-learning + mkdir -p "$ML_DIR" && chown -R immich:immich "$ML_DIR" + chown immich:immich ./uv.lock + export VIRTUAL_ENV="${ML_DIR}"/ml-venv + if [[ -f ~/.openvino ]]; then + msg_info "Updating HW-accelerated machine-learning" + # Remove old venv if Python version changed (3.12 -> 3.13) + if [[ -d "${VIRTUAL_ENV}" ]] && ! "${VIRTUAL_ENV}/bin/python3" --version 2>/dev/null | grep -q "3.13"; then + rm -rf "${VIRTUAL_ENV}" + fi + $STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p python3.13 --managed-python + patchelf --clear-execstack "${VIRTUAL_ENV}/lib/python3.13/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-313-x86_64-linux-gnu.so" + # Add workaround for onnxruntime-openvino 1.23.x crash if not present + if ! grep -q "MACHINE_LEARNING_OPENVINO_NUM_THREADS" "$INSTALL_DIR/.env" 2>/dev/null; then + sed -i '/MACHINE_LEARNING_CACHE_FOLDER/a ## - For OpenVINO only - workaround for onnxruntime-openvino 1.23.x crash\n## - See: https://github.com/immich-app/immich/pull/11240\nMACHINE_LEARNING_OPENVINO_NUM_THREADS=$(nproc)' "$INSTALL_DIR/.env" + fi + msg_ok "Updated HW-accelerated machine-learning" + else + msg_info "Updating machine-learning" + $STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p python3.11 --managed-python + msg_ok "Updated machine-learning" + fi + cd "$SRC_DIR" + cp -a machine-learning/{ann,immich_ml} "$ML_DIR" + mv "$INSTALL_DIR"/ml_start.sh "$ML_DIR" + if [[ -f ~/.openvino ]]; then + sed -i "/intra_op/s/int = 0/int = os.cpu_count() or 0/" "$ML_DIR"/immich_ml/config.py + fi + ln -sf "$APP_DIR"/resources "$INSTALL_DIR" + cd "$APP_DIR" + grep -rl /usr/src | xargs -n1 sed -i "s|\/usr/src|$INSTALL_DIR|g" + grep -rlE "'/build'" | xargs -n1 sed -i "s|'/build'|'$APP_DIR'|g" + sed -i "s@\"/cache\"@\"$INSTALL_DIR/cache\"@g" "$ML_DIR"/immich_ml/config.py + ln -s "${UPLOAD_DIR:-/opt/immich/upload}" "$APP_DIR"/upload + ln -s "${UPLOAD_DIR:-/opt/immich/upload}" "$ML_DIR"/upload + ln -s "$GEO_DIR" "$APP_DIR" + + chown -R immich:immich "$INSTALL_DIR" + systemctl restart immich-ml immich-web + msg_ok "Updated successfully!" + fi + exit +} + +function compile_libjxl() { + SOURCE=${SOURCE_DIR}/libjxl + JPEGLI_LIBJPEG_LIBRARY_SOVERSION="62" + JPEGLI_LIBJPEG_LIBRARY_VERSION="62.3.0" + : "${LIBJXL_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libjxl.json)}" + if [[ "$LIBJXL_REVISION" != "$(grep 'libjxl' ~/.immich_library_revisions | awk '{print $2}')" ]]; then + msg_info "Recompiling libjxl" + if [[ -d "$SOURCE" ]]; then rm -rf "$SOURCE"; fi + $STD git clone https://github.com/libjxl/libjxl.git "$SOURCE" + cd "$SOURCE" + $STD git reset --hard "$LIBJXL_REVISION" + $STD git submodule update --init --recursive --depth 1 --recommend-shallow + $STD git apply "$BASE_DIR"/server/sources/libjxl-patches/jpegli-empty-dht-marker.patch + $STD git apply "$BASE_DIR"/server/sources/libjxl-patches/jpegli-icc-warning.patch + mkdir build + cd build + $STD cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DJPEGXL_ENABLE_DOXYGEN=OFF \ + -DJPEGXL_ENABLE_MANPAGES=OFF \ + -DJPEGXL_ENABLE_PLUGIN_GIMP210=OFF \ + -DJPEGXL_ENABLE_BENCHMARK=OFF \ + -DJPEGXL_ENABLE_EXAMPLES=OFF \ + -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DJPEGXL_FORCE_SYSTEM_HWY=ON \ + -DJPEGXL_ENABLE_JPEGLI=ON \ + -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=ON \ + -DJPEGXL_INSTALL_JPEGLI_LIBJPEG=ON \ + -DJPEGXL_ENABLE_PLUGINS=ON \ + -DJPEGLI_LIBJPEG_LIBRARY_SOVERSION="$JPEGLI_LIBJPEG_LIBRARY_SOVERSION" \ + -DJPEGLI_LIBJPEG_LIBRARY_VERSION="$JPEGLI_LIBJPEG_LIBRARY_VERSION" \ + -DLIBJPEG_TURBO_VERSION_NUMBER=2001005 \ + .. + $STD cmake --build . -- -j"$(nproc)" + $STD cmake --install . + ldconfig /usr/local/lib + $STD make clean + cd "$STAGING_DIR" + rm -rf "$SOURCE"/{build,third_party} + sed -i "s/libjxl: .*$/libjxl: $LIBJXL_REVISION/" ~/.immich_library_revisions + msg_ok "Recompiled libjxl" + fi +} + +function compile_libheif() { + SOURCE=${SOURCE_DIR}/libheif + if ! dpkg -l | grep -q libaom; then + $STD apt install -y libaom-dev + local update="required" + fi + : "${LIBHEIF_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libheif.json)}" + if [[ "${update:-}" ]] || [[ "$LIBHEIF_REVISION" != "$(grep 'libheif' ~/.immich_library_revisions | awk '{print $2}')" ]]; then + msg_info "Recompiling libheif" + if [[ -d "$SOURCE" ]]; then rm -rf "$SOURCE"; fi + $STD git clone https://github.com/strukturag/libheif.git "$SOURCE" + cd "$SOURCE" + $STD git reset --hard "$LIBHEIF_REVISION" + mkdir build + cd build + $STD cmake --preset=release-noplugins \ + -DWITH_DAV1D=ON \ + -DENABLE_PARALLEL_TILE_DECODING=ON \ + -DWITH_LIBSHARPYUV=ON \ + -DWITH_LIBDE265=ON \ + -DWITH_AOM_DECODER=OFF \ + -DWITH_AOM_ENCODER=ON \ + -DWITH_X265=OFF \ + -DWITH_EXAMPLES=OFF \ + .. + $STD make install -j "$(nproc)" + ldconfig /usr/local/lib + $STD make clean + cd "$STAGING_DIR" + rm -rf "$SOURCE"/build + sed -i "s/libheif: .*$/libheif: $LIBHEIF_REVISION/" ~/.immich_library_revisions + msg_ok "Recompiled libheif" + fi +} + +function compile_libraw() { + SOURCE=${SOURCE_DIR}/libraw + : "${LIBRAW_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libraw.json)}" + if [[ "$LIBRAW_REVISION" != "$(grep 'libraw' ~/.immich_library_revisions | awk '{print $2}')" ]]; then + msg_info "Recompiling libraw" + if [[ -d "$SOURCE" ]]; then rm -rf "$SOURCE"; fi + $STD git clone https://github.com/libraw/libraw.git "$SOURCE" + cd "$SOURCE" + $STD git reset --hard "$LIBRAW_REVISION" + $STD autoreconf --install + $STD ./configure --disable-examples + $STD make -j"$(nproc)" + $STD make install + ldconfig /usr/local/lib + $STD make clean + cd "$STAGING_DIR" + sed -i "s/libraw: .*$/libraw: $LIBRAW_REVISION/" ~/.immich_library_revisions + msg_ok "Recompiled libraw" + fi +} + +function compile_imagemagick() { + SOURCE=$SOURCE_DIR/imagemagick + : "${IMAGEMAGICK_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/imagemagick.json)}" + if [[ "$IMAGEMAGICK_REVISION" != "$(grep 'imagemagick' ~/.immich_library_revisions | awk '{print $2}')" ]] || + ! grep -q 'DMAGICK_LIBRAW' /usr/local/lib/ImageMagick-7*/config-Q16HDRI/configure.xml; then + msg_info "Recompiling ImageMagick" + if [[ -d "$SOURCE" ]]; then rm -rf "$SOURCE"; fi + $STD git clone https://github.com/ImageMagick/ImageMagick.git "$SOURCE" + cd "$SOURCE" + $STD git reset --hard "$IMAGEMAGICK_REVISION" + $STD ./configure --with-modules CPPFLAGS="-DMAGICK_LIBRAW_VERSION_TAIL=202502" + $STD make -j"$(nproc)" + $STD make install + ldconfig /usr/local/lib + $STD make clean + cd "$STAGING_DIR" + sed -i "s/imagemagick: .*$/imagemagick: $IMAGEMAGICK_REVISION/" ~/.immich_library_revisions + msg_ok "Recompiled ImageMagick" + fi +} + +function compile_libvips() { + SOURCE=$SOURCE_DIR/libvips + : "${LIBVIPS_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libvips.json)}" + if [[ "$LIBVIPS_REVISION" != "$(grep 'libvips' ~/.immich_library_revisions | awk '{print $2}')" ]]; then + msg_info "Recompiling libvips" + if [[ -d "$SOURCE" ]]; then rm -rf "$SOURCE"; fi + $STD git clone https://github.com/libvips/libvips.git "$SOURCE" + cd "$SOURCE" + $STD git reset --hard "$LIBVIPS_REVISION" + $STD meson setup build --buildtype=release --libdir=lib -Dintrospection=disabled -Dtiff=disabled + cd build + $STD ninja install + ldconfig /usr/local/lib + cd "$STAGING_DIR" + rm -rf "$SOURCE"/build + sed -i "s/libvips: .*$/libvips: $LIBVIPS_REVISION/" ~/.immich_library_revisions + msg_ok "Recompiled libvips" + fi +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:2283${CL}" diff --git a/ct/isponsorblocktv.sh b/ct/isponsorblocktv.sh new file mode 100644 index 000000000..b6bd4e18a --- /dev/null +++ b/ct/isponsorblocktv.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Matthew Stern (sternma) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/dmunozv04/iSponsorBlockTV + +APP="iSponsorBlockTV" +var_tags="${var_tags:-media;automation}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /opt/isponsorblocktv ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV"; then + msg_info "Stopping Service" + systemctl stop isponsorblocktv + msg_ok "Stopped Service" + + if [[ -d /var/lib/isponsorblocktv ]]; then + msg_info "Backing up Data" + cp -r /var/lib/isponsorblocktv /var/lib/isponsorblocktv_data_backup + msg_ok "Backed up Data" + fi + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV" + + msg_info "Setting up iSponsorBlockTV" + $STD python3 -m venv /opt/isponsorblocktv/venv + $STD /opt/isponsorblocktv/venv/bin/pip install --upgrade pip + $STD /opt/isponsorblocktv/venv/bin/pip install /opt/isponsorblocktv + msg_ok "Set up iSponsorBlockTV" + + if [[ -d /var/lib/isponsorblocktv_data_backup ]]; then + msg_info "Restoring Data" + rm -rf /var/lib/isponsorblocktv + cp -r /var/lib/isponsorblocktv_data_backup /var/lib/isponsorblocktv + rm -rf /var/lib/isponsorblocktv_data_backup + msg_ok "Restored Data" + fi + + msg_info "Starting Service" + systemctl start isponsorblocktv + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Run the setup wizard inside the container with:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}iSponsorBlockTV setup${CL}" diff --git a/ct/kitchenowl.sh b/ct/kitchenowl.sh deleted file mode 100644 index 7517980d0..000000000 --- a/ct/kitchenowl.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) -# Copyright (c) 2021-2025 community-scripts ORG -# Author: snazzybean -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Source: https://github.com/TomBursch/kitchenowl - -APP="KitchenOwl" -var_tags="${var_tags:-food;recipes}" -var_cpu="${var_cpu:-1}" -var_ram="${var_ram:-2048}" -var_disk="${var_disk:-6}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - - if [[ ! -d /opt/kitchenowl ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - if check_for_gh_release "kitchenowl" "TomBursch/kitchenowl"; then - msg_info "Stopping Service" - systemctl stop kitchenowl - msg_ok "Stopped Service" - - msg_info "Backing up KitchenOwl" - mkdir -p /opt/kitchenowl_backup - cp -r /opt/kitchenowl/data /opt/kitchenowl_backup/ - cp -f /opt/kitchenowl/kitchenowl.env /opt/kitchenowl_backup/ - msg_ok "Backed up KitchenOwl" - - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "kitchenowl" "TomBursch/kitchenowl" "tarball" "latest" "/opt/kitchenowl" - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "kitchenowl-web" "TomBursch/kitchenowl" "prebuild" "latest" "/opt/kitchenowl/web" "kitchenowl_Web.tar.gz" - - msg_info "Restoring KitchenOwl data" - sed -i 's/default=True/default=False/' /opt/kitchenowl/backend/wsgi.py - cp -r /opt/kitchenowl_backup/data /opt/kitchenowl/ - cp -f /opt/kitchenowl_backup/kitchenowl.env /opt/kitchenowl/ - rm -rf /opt/kitchenowl_backup - msg_ok "Restored KitchenOwl data" - - msg_info "Updating KitchenOwl" - cd /opt/kitchenowl/backend - $STD uv sync --frozen - cd /opt/kitchenowl/backend - set -a - source /opt/kitchenowl/kitchenowl.env - set +a - $STD uv run flask db upgrade - msg_ok "Updated KitchenOwl" - - msg_info "Starting Service" - systemctl start kitchenowl - msg_ok "Started Service" - msg_ok "Updated successfully!" - fi - exit -} - -start -build_container -description - -msg_ok "Completed Successfully!\n" -echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:80${CL}" diff --git a/ct/languagetool.sh b/ct/languagetool.sh deleted file mode 100644 index 6f63178a1..000000000 --- a/ct/languagetool.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: Slaviša Arežina (tremor021) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://languagetool.org/ - -APP="LanguageTool" -var_tags="${var_tags:-spellcheck}" -var_cpu="${var_cpu:-2}" -var_ram="${var_ram:-4096}" -var_disk="${var_disk:-8}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - if [[ ! -d /opt/LanguageTool ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - RELEASE=$(curl -fsSL https://languagetool.org/download/ | grep -oP 'LanguageTool-\K[0-9]+\.[0-9]+(\.[0-9]+)?(?=\.zip)' | sort -V | tail -n1) - if [[ "${RELEASE}" != "$(cat ~/.languagetool 2>/dev/null)" ]] || [[ ! -f ~/.languagetool ]]; then - msg_info "Stopping LanguageTool" - systemctl stop language-tool - msg_ok "Stopped LanguageTool" - - msg_info "Creating Backup" - cp /opt/LanguageTool/server.properties /opt/server.properties - msg_ok "Backup Created" - - msg_info "Updating LanguageTool" - rm -rf /opt/LanguageTool - download_file "https://languagetool.org/download/LanguageTool-stable.zip" /tmp/LanguageTool-stable.zip - unzip -q /tmp/LanguageTool-stable.zip -d /opt - mv /opt/LanguageTool-*/ /opt/LanguageTool/ - mv /opt/server.properties /opt/LanguageTool/server.properties - echo "${RELEASE}" >~/.languagetool - msg_ok "Updated LanguageTool" - - msg_info "Starting LanguageTool" - systemctl start language-tool - msg_ok "Started LanguageTool" - msg_ok "Updated successfuly!" - else - msg_ok "No update required. ${APP} is already at v${RELEASE}" - fi - exit -} - -start -build_container -description - -msg_ok "Completed successfully!" -echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8081/v2${CL}" diff --git a/ct/nodecast-tv.sh b/ct/nodecast-tv.sh deleted file mode 100644 index 202e65ecc..000000000 --- a/ct/nodecast-tv.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: luismco -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/technomancer702/nodecast-tv - -APP="nodecast-tv" -var_tags="${var_tags:-media}" -var_cpu="${var_cpu:-2}" -var_ram="${var_ram:-2048}" -var_disk="${var_disk:-4}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - if [[ ! -d /opt/nodecast-tv ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - if check_for_gh_release "nodecast-tv" "technomancer702/nodecast-tv"; then - msg_info "Stopping Service" - systemctl stop nodecast-tv - msg_ok "Stopped Service" - - fetch_and_deploy_gh_release "nodecast-tv" "technomancer702/nodecast-tv" - - msg_info "Updating Modules" - cd /opt/nodecast-tv - $STD npm install - msg_ok "Updated Modules" - - msg_info "Starting Service" - systemctl start nodecast-tv - msg_ok "Started Service" - msg_ok "Updated successfully!" - fi - exit -} - -start -build_container -description - -msg_ok "Completed successfully!\n" -echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}" - diff --git a/ct/opencloud.sh b/ct/opencloud.sh index 6c047fa6d..639ee67db 100644 --- a/ct/opencloud.sh +++ b/ct/opencloud.sh @@ -29,7 +29,7 @@ function update_script() { exit fi - RELEASE="v4.1.0" + RELEASE="v5.0.1" if check_for_gh_release "opencloud" "opencloud-eu/opencloud" "${RELEASE}"; then msg_info "Stopping services" systemctl stop opencloud opencloud-wopi diff --git a/ct/rustypaste.sh b/ct/rustypaste.sh deleted file mode 100644 index 2da17f16e..000000000 --- a/ct/rustypaste.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -source <(curl -s https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: GoldenSpringness -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Source: https://github.com/orhun/rustypaste - -APP="rustypaste" -var_tags="${var_tags:-pastebin;storage}" -var_cpu="${var_cpu:-1}" -var_ram="${var_ram:-1024}" -var_disk="${var_disk:-20}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - - if [[ ! -f /opt/rustypaste/rustypaste ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - if check_for_gh_release "rustypaste" "orhun/rustypaste"; then - msg_info "Stopping Services" - systemctl stop rustypaste - msg_ok "Stopped Services" - - msg_info "Creating Backup" - tar -czf "/opt/rustypaste_backup_$(date +%F).tar.gz" /opt/rustypaste/upload 2>/dev/null || true - cp /opt/rustypaste/config.toml /tmp/rustypaste_config.toml.bak - msg_ok "Backup Created" - - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "rustypaste" "orhun/rustypaste" "prebuild" "latest" "/opt/rustypaste" "*x86_64-unknown-linux-gnu.tar.gz" - - msg_info "Restoring Data" - mv /tmp/rustypaste_config.toml.bak /opt/rustypaste/config.toml - tar -xzf "/opt/rustypaste_backup_$(date +%F).tar.gz" -C /opt/rustypaste/upload 2>/dev/null || true - rm -rf /opt/rustypaste_backup_$(date +%F).tar.gz - msg_ok "Restored Data" - - msg_info "Starting Services" - systemctl start rustypaste - msg_ok "Started Services" - msg_ok "Updated successfully!" - fi - - if check_for_gh_release "rustypaste-cli" "orhun/rustypaste-cli"; then - fetch_and_deploy_gh_release "rustypaste-cli" "orhun/rustypaste-cli" "prebuild" "latest" "/usr/local/bin" "*x86_64-unknown-linux-gnu.tar.gz" - fi - exit -} - -start -build_container -description - -msg_ok "Completed successfully!\n" -echo -e "${CREATING}${GN}rustypaste setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8000${CL}" diff --git a/ct/shelfmark.sh b/ct/shelfmark.sh deleted file mode 100644 index f69bca3f0..000000000 --- a/ct/shelfmark.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: vhsdream -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Source: https://github.com/calibrain/shelfmark - -APP="shelfmark" -var_tags="${var_tags:-ebooks}" -var_cpu="${var_cpu:-2}" -var_ram="${var_ram:-2048}" -var_disk="${var_disk:-8}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - - if [[ ! -d /opt/shelfmark ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - NODE_VERSION="22" setup_nodejs - PYTHON_VERSION="3.12" setup_uv - - if check_for_gh_release "shelfmark" "calibrain/shelfmark"; then - msg_info "Stopping Service" - systemctl stop shelfmark - msg_ok "Stopped Service" - - cp /opt/shelfmark/start.sh /opt/start.sh.bak - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "shelfmark" "calibrain/shelfmark" "tarball" "latest" "/opt/shelfmark" - - msg_info "Updating Shelfmark" - cd /opt/shelfmark/src/frontend - $STD npm ci - $STD npm run build - mv /opt/shelfmark/src/frontend/dist /opt/shelfmark/frontend-dist - cd /opt/shelfmark - $STD uv venv -c ./venv - $STD uv pip install -r requirements-base.txt - mv /opt/start.sh.bak /opt/start.sh - msg_ok "Updated Shelfmark" - - msg_info "Starting Service" - systemctl start shelfmark - msg_ok "Started Service" - msg_ok "Updated successfully!" - fi - exit -} - -start -build_container -description - -msg_ok "Completed successfully!\n" -echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8084${CL}" diff --git a/ct/sonobarr.sh b/ct/sonobarr.sh new file mode 100644 index 000000000..b8279a24d --- /dev/null +++ b/ct/sonobarr.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: GoldenSpringness +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://github.com/Dodelidoo-Labs/sonobarr + +APP="sonobarr" +var_tags="${var_tags:-storage}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-20}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d "/opt/sonobarr" ]]; then + msg_error "No sonobarr Installation Found!" + exit + fi + + PYTHON_VERSION="3.12" setup_uv + + if check_for_gh_release "sonobarr" "Dodelidoo-Labs/sonobarr"; then + msg_info "Stopping Service" + systemctl stop sonobarr + msg_ok "Stopped Service" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "sonobarr" "Dodelidoo-Labs/sonobarr" "tarball" + + msg_info "Updating sonobarr" + $STD uv venv -c /opt/sonobarr/venv + $STD source /opt/sonobarr/venv/bin/activate + $STD uv pip install --no-cache-dir -r /opt/sonobarr/requirements.txt + sed -i "/release_version/s/=.*/=$(cat ~/.sonobarr)/" /etc/sonobarr/.env + msg_ok "Updated sonobarr" + + msg_info "Starting Service" + systemctl start sonobarr + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${CREATING}${GN}sonobarr setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:5000${CL}" diff --git a/ct/vaultwarden.sh b/ct/vaultwarden.sh new file mode 100644 index 000000000..fc8c5db45 --- /dev/null +++ b/ct/vaultwarden.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://github.com/dani-garcia/vaultwarden + +APP="Vaultwarden" +var_tags="${var_tags:-password-manager}" +var_cpu="${var_cpu:-4}" +var_ram="${var_ram:-6144}" +var_disk="${var_disk:-20}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -f /etc/systemd/system/vaultwarden.service ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + VAULT=$(get_latest_github_release "dani-garcia/vaultwarden") + WVRELEASE=$(get_latest_github_release "dani-garcia/bw_web_builds") + + UPD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SUPPORT" --radiolist --cancel-button Exit-Script "Spacebar = Select" 11 58 3 \ + "1" "VaultWarden $VAULT" ON \ + "2" "Web-Vault $WVRELEASE" OFF \ + "3" "Set Admin Token" OFF \ + 3>&1 1>&2 2>&3) + + if [ "$UPD" == "1" ]; then + if check_for_gh_release "vaultwarden" "dani-garcia/vaultwarden"; then + msg_info "Stopping Service" + systemctl stop vaultwarden + msg_ok "Stopped Service" + + fetch_and_deploy_gh_release "vaultwarden" "dani-garcia/vaultwarden" "tarball" "latest" "/tmp/vaultwarden-src" + + msg_info "Updating VaultWarden to $VAULT (Patience)" + cd /tmp/vaultwarden-src + $STD cargo build --features "sqlite,mysql,postgresql" --release + if [[ -f /usr/bin/vaultwarden ]]; then + cp target/release/vaultwarden /usr/bin/ + else + cp target/release/vaultwarden /opt/vaultwarden/bin/ + fi + cd ~ && rm -rf /tmp/vaultwarden-src + msg_ok "Updated VaultWarden to ${VAULT}" + + msg_info "Starting Service" + systemctl start vaultwarden + msg_ok "Started Service" + msg_ok "Updated successfully!" + else + msg_ok "VaultWarden is already up-to-date" + fi + exit + fi + + if [ "$UPD" == "2" ]; then + if check_for_gh_release "vaultwarden_webvault" "dani-garcia/bw_web_builds"; then + msg_info "Stopping Service" + systemctl stop vaultwarden + msg_ok "Stopped Service" + + fetch_and_deploy_gh_release "vaultwarden_webvault" "dani-garcia/bw_web_builds" "prebuild" "latest" "/opt/vaultwarden" "bw_web_*.tar.gz" + + msg_info "Updating Web-Vault to $WVRELEASE" + rm -rf /opt/vaultwarden/web-vault + chown -R root:root /opt/vaultwarden/web-vault/ + msg_ok "Updated Web-Vault to ${WVRELEASE}" + + msg_info "Starting Service" + systemctl start vaultwarden + msg_ok "Started Service" + msg_ok "Updated successfully!" + else + msg_ok "Web-Vault is already up-to-date" + fi + exit + fi + + if [ "$UPD" == "3" ]; then + if NEWTOKEN=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "Set the ADMIN_TOKEN" 10 58 3>&1 1>&2 2>&3); then + if [[ -z "$NEWTOKEN" ]]; then exit; fi + ensure_dependencies argon2 + TOKEN=$(echo -n "${NEWTOKEN}" | argon2 "$(openssl rand -base64 32)" -t 2 -m 16 -p 4 -l 64 -e) + sed -i "s|ADMIN_TOKEN=.*|ADMIN_TOKEN='${TOKEN}'|" /opt/vaultwarden/.env + if [[ -f /opt/vaultwarden/data/config.json ]]; then + sed -i "s|\"admin_token\":.*|\"admin_token\": \"${TOKEN}\"|" /opt/vaultwarden/data/config.json + fi + systemctl restart vaultwarden + msg_ok "Admin token updated" + fi + exit + fi +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}https://${IP}:8000${CL}" diff --git a/ct/vikunja.sh b/ct/vikunja.sh new file mode 100644 index 000000000..f199e7721 --- /dev/null +++ b/ct/vikunja.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (Canbiz) | Co-Author: CrazyWolf13 +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://vikunja.io/ + +APP="Vikunja" +var_tags="${var_tags:-todo-app}" +var_cpu="${var_cpu:-1}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -d /opt/vikunja ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + RELEASE="$( [[ -f "$HOME/.vikunja" ]] && cat "$HOME/.vikunja" 2>/dev/null || [[ -f /opt/Vikunja_version ]] && cat /opt/Vikunja_version 2>/dev/null || true)" + if [[ -z "$RELEASE" ]] || [[ "$RELEASE" == "unstable" ]] || dpkg --compare-versions "${RELEASE:-0.0.0}" lt "1.0.0"; then + msg_warn "You are upgrading from Vikunja '$RELEASE'." + 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" + + read -rp "Continue with update? (y to proceed): " -t 30 CONFIRM1 || exit 1 + [[ "$CONFIRM1" =~ ^[yY]$ ]] || exit 0 + + echo + msg_warn "Vikunja may not start after the update until you manually adjust the config." + msg_warn "Details: https://vikunja.io/changelog/whats-new-in-vikunja-1.0.0/#config-changes" + + read -rp "Acknowledge and continue? (y): " -t 30 CONFIRM2 || exit 1 + [[ "$CONFIRM2" =~ ^[yY]$ ]] || exit 0 + fi + + if check_for_gh_release "vikunja" "go-vikunja/vikunja"; then + msg_info "Stopping Service" + systemctl stop vikunja + msg_ok "Stopped Service" + + fetch_and_deploy_gh_release "vikunja" "go-vikunja/vikunja" "binary" + + msg_info "Starting Service" + systemctl start vikunja + msg_ok "Started Service" + msg_ok "Updated successfully!" + fi + exit 0 +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3456${CL}" diff --git a/ct/writefreely.sh b/ct/writefreely.sh new file mode 100644 index 000000000..bb0c64c58 --- /dev/null +++ b/ct/writefreely.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: StellaeAlis +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://github.com/writefreely/writefreely + +# App Default Values +APP="WriteFreely" +var_tags="${var_tags:-writing}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-1024}" +var_disk="${var_disk:-4}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /opt/writefreely ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + if check_for_gh_release "writefreely" "writefreely/writefreely"; then + msg_info "Stopping Services" + systemctl stop writefreely + msg_ok "Stopped Services" + + msg_info "Creating Backup" + mkdir -p /tmp/writefreely_backup + cp /opt/writefreely/keys /tmp/writefreely_backup/ 2>/dev/null + cp /opt/writefreely/config.ini /tmp/writefreely_backup/ 2>/dev/null + msg_ok "Created Backup" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "writefreely" "writefreely/writefreely" "prebuild" "latest" "/opt/writefreely" "writefreely_*_linux_amd64.tar.gz" + + msg_info "Restoring Data" + cp /tmp/writefreely_backup/config.ini /opt/writefreely/ 2>/dev/null + cp /tmp/writefreely_backup/keys/* /opt/writefreely/keys/ 2>/dev/null + rm -rf /tmp/writefreely_backup + msg_ok "Restored Data" + + msg_info "Running Post-Update Tasks" + cd /opt/writefreely + $STD ./writefreely db migrate + msg_ok "Ran Post-Update Tasks" + + msg_info "Starting Services" + systemctl start writefreely + msg_ok "Started Services" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}" diff --git a/docs/AI.md b/docs/AI.md index 489a4b35d..9efebd689 100644 --- a/docs/AI.md +++ b/docs/AI.md @@ -224,9 +224,9 @@ CLEAN_INSTALL=1 fetch_and_deploy_gh_release "appname" "owner/repo" ### Helper Utilities -| Function | Description | Example | -|----------|-------------|----------| -| `import_local_ip` | Sets `$LOCAL_IP` variable | `import_local_ip` | +| Function/Variable | Description | Example | +|-------------------|-------------|----------| +| `$LOCAL_IP` | Always available - contains the container's IP address | `echo "Access: http://${LOCAL_IP}:3000"` | | `ensure_dependencies` | Checks/installs dependencies | `ensure_dependencies curl jq` | | `install_packages_with_retry` | APT install with retry | `install_packages_with_retry nginx redis` | diff --git a/docs/misc/core.func/CORE_FUNCTIONS_REFERENCE.md b/docs/misc/core.func/CORE_FUNCTIONS_REFERENCE.md index c89942083..8adf62a6f 100644 --- a/docs/misc/core.func/CORE_FUNCTIONS_REFERENCE.md +++ b/docs/misc/core.func/CORE_FUNCTIONS_REFERENCE.md @@ -634,4 +634,224 @@ silent() - `SILENT_LOGFILE`: Silent execution log file path - `SPINNER_PID`: Spinner process ID - `SPINNER_MSG`: Spinner message text -- `MSG_INFO_SHOWN`: Tracks shown info messages +- `MSG_INFO_SHOWN`: Tracks shown info messages- `PHS_SILENT`: Unattended mode flag (1 = silent) +- `var_unattended`: Unattended mode variable (yes/no) +- `UNATTENDED`: Alternative unattended mode variable + +## Unattended/Interactive Prompt Functions + +These functions provide a unified way to handle user prompts in both interactive and unattended modes. They automatically detect the execution context and either prompt the user (with timeout) or use default values silently. + +### `is_unattended()` +**Purpose**: Detect if script is running in unattended/silent mode +**Parameters**: None +**Returns**: +- `0` (true): Running unattended +- `1` (false): Running interactively +**Side Effects**: None +**Dependencies**: None +**Environment Variables Used**: `PHS_SILENT`, `var_unattended`, `UNATTENDED` + +**Usage Example**: +```bash +if is_unattended; then + echo "Running in unattended mode" +else + echo "Running interactively" +fi +``` + +### `prompt_confirm()` +**Purpose**: Prompt user for yes/no confirmation with timeout and unattended support +**Parameters**: +- `$1` - Prompt message (required) +- `$2` - Default value: "y" or "n" (optional, default: "n") +- `$3` - Timeout in seconds (optional, default: 60) +**Returns**: +- `0`: User confirmed (yes) +- `1`: User declined (no) or timeout with default "n" +**Side Effects**: Displays prompt to terminal +**Dependencies**: `is_unattended()` +**Environment Variables Used**: Color variables (`YW`, `CL`) + +**Behavior**: +- **Unattended mode**: Immediately returns default value +- **Non-TTY**: Immediately returns default value +- **Interactive**: Displays prompt with timeout countdown +- **Timeout**: Auto-applies default value after specified seconds + +**Usage Examples**: +```bash +# Basic confirmation (default: no) +if prompt_confirm "Proceed with installation?"; then + install_package +fi + +# Default to yes, 30 second timeout +if prompt_confirm "Continue?" "y" 30; then + continue_operation +fi + +# In unattended mode (will use default immediately) +var_unattended=yes +prompt_confirm "Delete files?" "n" && echo "Deleting" || echo "Skipped" +``` + +### `prompt_input()` +**Purpose**: Prompt user for text input with timeout and unattended support +**Parameters**: +- `$1` - Prompt message (required) +- `$2` - Default value (optional, default: "") +- `$3` - Timeout in seconds (optional, default: 60) +**Output**: Prints the user input or default value to stdout +**Returns**: `0` always +**Side Effects**: Displays prompt to stderr (keeps stdout clean for value) +**Dependencies**: `is_unattended()` +**Environment Variables Used**: Color variables (`YW`, `CL`) + +**Behavior**: +- **Unattended mode**: Returns default value immediately +- **Non-TTY**: Returns default value immediately +- **Interactive**: Waits for user input with timeout +- **Empty input**: Returns default value +- **Timeout**: Returns default value + +**Usage Examples**: +```bash +# Get username with default +username=$(prompt_input "Enter username:" "admin" 30) +echo "Using username: $username" + +# With validation loop +while true; do + port=$(prompt_input "Enter port:" "8080" 30) + [[ "$port" =~ ^[0-9]+$ ]] && break + echo "Invalid port number" +done + +# Capture value in unattended mode +var_unattended=yes +db_name=$(prompt_input "Database name:" "myapp_db") +``` + +### `prompt_select()` +**Purpose**: Prompt user to select from a list of options with timeout support +**Parameters**: +- `$1` - Prompt message (required) +- `$2` - Default option number, 1-based (optional, default: 1) +- `$3` - Timeout in seconds (optional, default: 60) +- `$4+` - Options to display (required, at least 1) +**Output**: Prints the selected option value to stdout +**Returns**: +- `0`: Success +- `1`: No options provided +**Side Effects**: Displays numbered menu to stderr +**Dependencies**: `is_unattended()` +**Environment Variables Used**: Color variables (`YW`, `GN`, `CL`) + +**Behavior**: +- **Unattended mode**: Returns default selection immediately +- **Non-TTY**: Returns default selection immediately +- **Interactive**: Displays numbered menu with timeout +- **Invalid selection**: Uses default +- **Timeout**: Auto-selects default + +**Usage Examples**: +```bash +# Simple selection +choice=$(prompt_select "Select database:" 1 30 "PostgreSQL" "MySQL" "SQLite") +echo "Selected: $choice" + +# Using array +options=("Production" "Staging" "Development") +env=$(prompt_select "Select environment:" 2 60 "${options[@]}") + +# In automation scripts +var_unattended=yes +db=$(prompt_select "Database:" 1 30 "postgres" "mysql" "sqlite") +# Returns "postgres" immediately without menu +``` + +### `prompt_password()` +**Purpose**: Prompt user for password input with hidden characters and auto-generation +**Parameters**: +- `$1` - Prompt message (required) +- `$2` - Default value or "generate" for auto-generation (optional) +- `$3` - Timeout in seconds (optional, default: 60) +- `$4` - Minimum length for validation (optional, default: 0) +**Output**: Prints the password to stdout +**Returns**: `0` always +**Side Effects**: Displays prompt to stderr with hidden input +**Dependencies**: `is_unattended()`, `openssl` (for generation) +**Environment Variables Used**: Color variables (`YW`, `CL`) + +**Behavior**: +- **"generate" default**: Creates random 16-character password +- **Unattended mode**: Returns default/generated password immediately +- **Non-TTY**: Returns default/generated password immediately +- **Interactive**: Hidden input with timeout +- **Min length validation**: Falls back to default if too short +- **Timeout**: Returns default/generated password + +**Usage Examples**: +```bash +# Auto-generate password if user doesn't provide one +password=$(prompt_password "Enter password:" "generate" 30) +echo "Password has been set" + +# Require minimum length +db_pass=$(prompt_password "Database password:" "" 60 12) + +# With default password +admin_pass=$(prompt_password "Admin password:" "changeme123" 30) + +# In unattended mode with auto-generation +var_unattended=yes +password=$(prompt_password "Password:" "generate") +# Returns randomly generated password +``` + +## Prompt Function Decision Flow + +``` +prompt_confirm() / prompt_input() / prompt_select() / prompt_password() +│ +├── is_unattended()? ─────────────────────┐ +│ └── PHS_SILENT=1? │ +│ └── var_unattended=yes? ├── YES → Return default immediately +│ └── UNATTENDED=yes? │ +│ │ +├── TTY available? ─────────────── NO ────┘ +│ +└── Interactive Mode + ├── Display prompt with timeout hint + ├── read -t $timeout + │ ├── User input received → Validate and return + │ ├── Empty input → Return default + │ └── Timeout → Return default with message + └── Return value +``` + +## Migration Guide: Converting read Commands + +To make existing scripts unattended-compatible, replace `read` commands with the appropriate prompt function: + +### Before (blocking): +```bash +read -rp "Continue? [y/N]: " confirm +[[ "$confirm" =~ ^[Yy]$ ]] && do_something + +read -p "Enter port: " port +port="${port:-8080}" + +read -p "Select (1-3): " choice +``` + +### After (unattended-safe): +```bash +prompt_confirm "Continue?" "n" && do_something + +port=$(prompt_input "Enter port:" "8080") + +choice=$(prompt_select "Select option:" 1 60 "Option 1" "Option 2" "Option 3") +``` diff --git a/frontend/public/json/ampache.json b/frontend/public/json/ampache.json deleted file mode 100644 index d16099011..000000000 --- a/frontend/public/json/ampache.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "Ampache", - "slug": "ampache", - "categories": [ - 13 - ], - "date_created": "2026-01-13", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 80, - "documentation": "https://github.com/ampache/ampache/wiki", - "website": "https://ampache.org/", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ampache.webp", - "config_path": "/opt/ampache/config/ampache.cfg.php", - "description": "Ampache is a web-based audio streaming application and file manager that allows you to access your music & videos from anywhere. It features a powerful music catalog, multiple user support, transcoding, streaming, and more.", - "install_methods": [ - { - "type": "default", - "script": "ct/ampache.sh", - "resources": { - "cpu": 4, - "ram": 2048, - "hdd": 5, - "os": "debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": null, - "password": null - }, - "notes": [ - { - "text": "Complete the web-based setup at http://IP/install.php", - "type": "info" - }, - { - "text": "Database credentials are stored in `~/ampache.creds` - use only the MySQL username and password from this file", - "type": "info" - }, - { - "text": "During installation, only check 'Create Tables' - leave 'Create Database' and 'Create Database User' unchecked", - "type": "info" - } - ] -} diff --git a/frontend/public/json/anytype.json b/frontend/public/json/anytype.json new file mode 100644 index 000000000..a89835cc9 --- /dev/null +++ b/frontend/public/json/anytype.json @@ -0,0 +1,48 @@ +{ + "name": "Anytype", + "slug": "anytype", + "categories": [ + 12 + ], + "date_created": "2026-01-29", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 33010, + "documentation": "https://doc.anytype.io/", + "website": "https://anytype.io/", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/anytype.webp", + "config_path": "/opt/anytype/.env", + "description": "Anytype is a local-first, privacy-focused alternative to Notion. This script deploys the any-sync-bundle which provides a self-hosted sync server for Anytype clients with external MongoDB and Redis Stack.", + "install_methods": [ + { + "type": "default", + "script": "ct/anytype.sh", + "resources": { + "cpu": 2, + "ram": 4096, + "hdd": 16, + "os": "Ubuntu", + "version": "24.04" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "After installation, import /opt/anytype/data/client-config.yml into your Anytype apps.", + "type": "info" + }, + { + "text": "This uses the community any-sync-bundle by grishy, not the official Anytype deployment.", + "type": "warning" + }, + { + "text": "Firewall: Open TCP 33010 (DRPC) and UDP 33020 (QUIC) for external access.", + "type": "info" + } + ] +} diff --git a/frontend/public/json/checkmate.json b/frontend/public/json/checkmate.json new file mode 100644 index 000000000..1febf47bc --- /dev/null +++ b/frontend/public/json/checkmate.json @@ -0,0 +1,48 @@ +{ + "name": "Checkmate", + "slug": "checkmate", + "categories": [ + 9 + ], + "date_created": "2026-02-02", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 5173, + "documentation": "https://github.com/bluewave-labs/Checkmate#readme", + "website": "https://github.com/bluewave-labs/Checkmate", + "logo": "https://raw.githubusercontent.com/bluewave-labs/Checkmate/develop/client/public/checkmate-logo-light.png", + "config_path": "/opt/checkmate/server/.env", + "description": "Checkmate is an open source uptime and infrastructure monitoring application that helps you track the availability and performance of your services.", + "install_methods": [ + { + "type": "default", + "script": "ct/checkmate.sh", + "resources": { + "cpu": 2, + "ram": 4096, + "hdd": 10, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Create your admin account on first login via the web interface.", + "type": "info" + }, + { + "text": "Server API runs on port 52345, Client UI on port 5173.", + "type": "info" + }, + { + "text": "For PageSpeed monitoring, add a Google PageSpeed API key to the server .env file.", + "type": "info" + } + ] +} diff --git a/frontend/public/json/clawdbot.json b/frontend/public/json/clawdbot.json new file mode 100644 index 000000000..cc5029067 --- /dev/null +++ b/frontend/public/json/clawdbot.json @@ -0,0 +1,40 @@ +{ + "name": "Clawdbot", + "slug": "clawdbot", + "categories": [ + 20 + ], + "date_created": "2026-01-26", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 18791, + "documentation": "https://docs.clawd.bot/", + "website": "https://clawd.bot/", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/clawdbot.webp", + "config_path": "/opt/clawdbot/.env", + "description": "Your own personal AI assistant. Any OS. Any Platform. The lobster way. Clawdbot is a powerful AI agent framework that can be configured to work across multiple platforms and channels.", + "install_methods": [ + { + "type": "default", + "script": "ct/clawdbot.sh", + "resources": { + "cpu": 2, + "ram": 2048, + "hdd": 8, + "os": "debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "type": "info", + "text": "After install run onboarding: clawdbot onboard --install-daemon" + } + ] +} diff --git a/frontend/public/json/dawarich.json b/frontend/public/json/dawarich.json deleted file mode 100644 index 85659bdaa..000000000 --- a/frontend/public/json/dawarich.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "Dawarich", - "slug": "dawarich", - "categories": [ - 9 - ], - "date_created": "2026-01-16", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 3000, - "documentation": "https://dawarich.app/docs", - "website": "https://dawarich.app/", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/dawarich.webp", - "config_path": "/opt/dawarich/.env", - "description": "Dawarich is a self-hosted alternative to Google Timeline (Google Maps Location History). It allows you to import your location history from Google Maps Timeline and Owntracks, view it on a map, and analyze your location data with statistics and visualizations.", - "install_methods": [ - { - "type": "default", - "script": "ct/dawarich.sh", - "resources": { - "cpu": 4, - "ram": 4096, - "hdd": 15, - "os": "Debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": "demo@dawarich.app", - "password": "password" - }, - "notes": [ - { - "text": "Default credentials: demo@dawarich.app / password - Change after first login!", - "type": "warning" - } - ] -} diff --git a/frontend/public/json/ebusd.json b/frontend/public/json/ebusd.json new file mode 100644 index 000000000..862d24153 --- /dev/null +++ b/frontend/public/json/ebusd.json @@ -0,0 +1,40 @@ +{ + "name": "ebusd", + "slug": "ebusd", + "categories": [ + 16 + ], + "date_created": "2026-01-26", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": null, + "documentation": "https://github.com/john30/ebusd/wiki", + "website": "https://github.com/john30/ebusd", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/proxmox-helper-scripts.webp", + "config_path": "/etc/default/ebusd", + "description": "ebusd is a daemon for handling communication with eBUS devices connected to a 2-wire `energy bus` used by numerous heating systems.", + "install_methods": [ + { + "type": "default", + "script": "ct/ebusd.sh", + "resources": { + "cpu": 1, + "ram": 512, + "hdd": 2, + "os": "debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": "root", + "password": null + }, + "notes": [ + { + "text": "For required post installation actions, checkout: `https://github.com/community-scripts/ProxmoxVE/discussions/11352`", + "type": "info" + } + ] +} diff --git a/frontend/public/json/github-versions.json b/frontend/public/json/github-versions.json new file mode 100644 index 000000000..a2c97554c --- /dev/null +++ b/frontend/public/json/github-versions.json @@ -0,0 +1,145 @@ +{ + "generated": "2026-01-31T18:42:49Z", + "versions": [ + { + "slug": "affine", + "repo": "toeverything/AFFiNE", + "version": "v0.25.7", + "pinned": false, + "date": "2025-12-09T04:34:14Z" + }, + { + "slug": "anytype", + "repo": "grishy/any-sync-bundle", + "version": "v1.2.1-2025-12-10", + "pinned": false, + "date": "2025-12-24T20:40:15Z" + }, + { + "slug": "databasus", + "repo": "databasus/databasus", + "version": "v3.7.0", + "pinned": false, + "date": "2026-01-28T14:46:28Z" + }, + { + "slug": "ente", + "repo": "ente-io/ente", + "version": "photos-v1.3.7", + "pinned": false, + "date": "2026-01-29T17:20:41Z" + }, + { + "slug": "frigate", + "repo": "blakeblackshear/frigate", + "version": "v0.16.4", + "pinned": false, + "date": "2026-01-29T00:42:14Z" + }, + { + "slug": "hoodik", + "repo": "hudikhq/hoodik", + "version": "v1.8.1", + "pinned": false, + "date": "2025-12-22T20:32:27Z" + }, + { + "slug": "isponsorblocktv", + "repo": "dmunozv04/iSponsorBlockTV", + "version": "v2.6.1", + "pinned": false, + "date": "2025-10-19T17:43:10Z" + }, + { + "slug": "kitchenowl", + "repo": "TomBursch/kitchenowl", + "version": "v0.7.6", + "pinned": false, + "date": "2026-01-24T01:21:14Z" + }, + { + "slug": "minthcm", + "repo": "minthcm/minthcm", + "version": "4.2.2", + "pinned": false, + "date": "2025-10-10T09:37:21Z" + }, + { + "slug": "nextexplorer", + "repo": "vikramsoni2/nextExplorer", + "version": "v2.1.2a", + "pinned": false, + "date": "2026-01-31T00:09:18Z" + }, + { + "slug": "nightscout", + "repo": "nightscout/cgm-remote-monitor", + "version": "15.0.3", + "pinned": false, + "date": "2025-05-08T22:12:34Z" + }, + { + "slug": "opencloud", + "repo": "opencloud-eu/opencloud", + "version": "v5.0.1", + "pinned": true, + "date": "2026-01-28T15:08:23Z" + }, + { + "slug": "piler", + "repo": "jsuto/piler", + "version": "piler-1.4.8", + "pinned": false, + "date": "2025-09-24T06:51:38Z" + }, + { + "slug": "pixelfed", + "repo": "pixelfed/pixelfed", + "version": "v0.12.6", + "pinned": false, + "date": "2025-09-03T12:12:04Z" + }, + { + "slug": "romm", + "repo": "RetroAchievements/RALibretro", + "version": "1.8.2", + "pinned": false, + "date": "2026-01-23T17:03:31Z" + }, + { + "slug": "rustypaste", + "repo": "orhun/rustypaste", + "version": "v0.16.1", + "pinned": false, + "date": "2025-03-21T20:44:47Z" + }, + { + "slug": "seer", + "repo": "seerr-team/seerr", + "version": "v2.7.3", + "pinned": false, + "date": "2025-08-14T20:43:46Z" + }, + { + "slug": "shelfmark", + "repo": "FlareSolverr/FlareSolverr", + "version": "v3.4.6", + "pinned": false, + "date": "2025-11-29T02:43:00Z" + }, + { + "slug": "sonobarr", + "repo": "Dodelidoo-Labs/sonobarr", + "version": "0.11.0", + "pinned": false, + "date": "2026-01-21T19:07:21Z" + }, + { + "slug": "wishlist", + "repo": "cmintey/wishlist", + "version": "v0.59.0", + "pinned": false, + "date": "2026-01-19T16:42:14Z" + } + ] +} diff --git a/frontend/public/json/isponsorblocktv.json b/frontend/public/json/isponsorblocktv.json new file mode 100644 index 000000000..2b25f17e9 --- /dev/null +++ b/frontend/public/json/isponsorblocktv.json @@ -0,0 +1,44 @@ +{ + "name": "iSponsorBlockTV", + "slug": "isponsorblocktv", + "categories": [ + 13 + ], + "date_created": "2026-01-25", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": null, + "documentation": "https://github.com/dmunozv04/iSponsorBlockTV/wiki", + "website": "https://github.com/dmunozv04/iSponsorBlockTV", + "logo": "https://raw.githubusercontent.com/ajayyy/SponsorBlock/master/public/icons/IconSponsorBlocker512px.png", + "config_path": "/var/lib/isponsorblocktv/config.json", + "description": "iSponsorBlockTV connects to YouTube TV clients and automatically skips SponsorBlock segments, mutes ads, and presses the Skip Ad button when available.", + "install_methods": [ + { + "type": "default", + "script": "ct/isponsorblocktv.sh", + "resources": { + "cpu": 1, + "ram": 1024, + "hdd": 4, + "os": "debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "No web UI; run `iSponsorBlockTV setup` inside the container to configure.", + "type": "info" + }, + { + "text": "SSDP auto-discovery requires multicast on your bridge; manual pairing works without it.", + "type": "info" + } + ] +} diff --git a/frontend/public/json/kitchenowl.json b/frontend/public/json/kitchenowl.json deleted file mode 100644 index e6c150616..000000000 --- a/frontend/public/json/kitchenowl.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "KitchenOwl", - "slug": "kitchenowl", - "categories": [ - 13 - ], - "date_created": "2025-12-28", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 80, - "documentation": "https://docs.kitchenowl.org/", - "website": "https://kitchenowl.org/", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/kitchenowl.webp", - "config_path": "/opt/kitchenowl/kitchenowl.env", - "description": "KitchenOwl is a smart self-hosted grocery list and recipe manager with real-time synchronization, recipe management, meal planning, and expense tracking.", - "install_methods": [ - { - "type": "default", - "script": "ct/kitchenowl.sh", - "resources": { - "cpu": 1, - "ram": 2048, - "hdd": 6, - "os": "Debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": null, - "password": null - }, - "notes": [] -} diff --git a/frontend/public/json/languagetool.json b/frontend/public/json/languagetool.json deleted file mode 100644 index 03bb66f64..000000000 --- a/frontend/public/json/languagetool.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "LanguageTool", - "slug": "languagetool", - "categories": [ - 0 - ], - "date_created": "2025-12-10", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 8081, - "documentation": "https://dev.languagetool.org/", - "config_path": "/opt/LanguageTool/server.properties", - "website": "https://languagetool.org/", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/languagetool.webp", - "description": "LanguageTool is an Open Source proofreading software for English, Spanish, French, German, Portuguese, Polish, Dutch, and more than 20 other languages. It finds many errors that a simple spell checker cannot detect.", - "install_methods": [ - { - "type": "default", - "script": "ct/languagetool.sh", - "resources": { - "cpu": 2, - "ram": 4096, - "hdd": 8, - "os": "Debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": null, - "password": null - }, - "notes": [ - { - "text": "API is available at `http://:8081/v2`.", - "type": "info" - }, - { - "text": "Application doesn't come with n-gram data. If you wish to use that feature, please look at `https://dev.languagetool.org/finding-errors-using-n-gram-data.html`.", - "type": "info" - } - ] -} diff --git a/frontend/public/json/nodecast-tv.json b/frontend/public/json/nodecast-tv.json deleted file mode 100644 index 8d3f0e1fb..000000000 --- a/frontend/public/json/nodecast-tv.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "nodecast-tv", - "slug": "nodecast-tv", - "categories": [ - 13 - ], - "date_created": "2026-01-14", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 3000, - "documentation": "https://github.com/technomancer702/nodecast-tv/blob/main/README.md", - "website": "https://github.com/technomancer702/nodecast-tv", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/nodecast-tv.webp", - "config_path": "", - "description": "nodecast-tv is a modern, web-based IPTV player featuring Live TV, EPG, Movies (VOD), and Series support. Built with performance and user experience in mind.", - "install_methods": [ - { - "type": "default", - "script": "ct/nodecast-tv.sh", - "resources": { - "cpu": 2, - "ram": 2048, - "hdd": 4, - "os": "debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": null, - "password": null - }, - "notes": [] -} diff --git a/frontend/public/json/rustypaste.json b/frontend/public/json/rustypaste.json deleted file mode 100644 index 2ac17c188..000000000 --- a/frontend/public/json/rustypaste.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "RustyPaste", - "slug": "rustypaste", - "categories": [ - 12 - ], - "date_created": "2025-12-22", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 8000, - "documentation": "https://github.com/orhun/rustypaste", - "config_path": "/opt/rustypaste/config.toml", - "website": "https://github.com/orhun/rustypaste", - "logo": "https://github.com/orhun/rustypaste/raw/master/img/rustypaste_logo.png", - "description": "Rustypaste is a minimal file upload/pastebin service.", - "install_methods": [ - { - "type": "default", - "script": "ct/rustypaste.sh", - "resources": { - "cpu": 1, - "ram": 1024, - "hdd": 20, - "os": "Debian", - "version": "13" - } - }, - { - "type": "alpine", - "script": "ct/alpine-rustypaste.sh", - "resources": { - "cpu": 1, - "ram": 256, - "hdd": 4, - "os": "Alpine", - "version": "3.22" - } - } - ], - "default_credentials": { - "username": null, - "password": null - }, - "notes": [ - { - "text": "When updating the script it will backup the whole project including all the uploaded files, make sure to extract it to a safe location or remove", - "type": "info" - } - ] -} diff --git a/frontend/public/json/shelfmark.json b/frontend/public/json/shelfmark.json deleted file mode 100644 index 262dcd6f7..000000000 --- a/frontend/public/json/shelfmark.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "Shelfmark", - "slug": "shelfmark", - "categories": [ - 13 - ], - "date_created": "2026-01-24", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 8084, - "documentation": "https://github.com/calibrain/shelfmark/tree/main/docs", - "website": "https://github.com/calibrain/shelfmark", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/shelfmark.webp", - "config_path": "/etc/shelfmark", - "description": "Shelfmark is a unified web interface for searching and aggregating books and audiobook downloads from multiple sources - all in one place.", - "install_methods": [ - { - "type": "default", - "script": "ct/shelfmark.sh", - "resources": { - "cpu": 2, - "ram": 2048, - "hdd": 6, - "os": "Debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": null, - "password": null - }, - "notes": [ - { - "text": "The configuration at `/etc/shelfmark/.env` is for bootstrapping the initial install. Customize the configuration via the Shelfmark UI.", - "type": "info" - } - ] -} diff --git a/frontend/public/json/sonobarr.json b/frontend/public/json/sonobarr.json new file mode 100644 index 000000000..965d6aee0 --- /dev/null +++ b/frontend/public/json/sonobarr.json @@ -0,0 +1,40 @@ +{ + "name": "Sonobarr", + "slug": "sonobarr", + "categories": [ + 14 + ], + "date_created": "2026-01-21", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 5000, + "documentation": "https://github.com/Dodelidoo-Labs/sonobarr", + "config_path": "/etc/sonobarr/.env", + "website": "https://github.com/Dodelidoo-Labs/sonobarr", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/sonobarr.webp", + "description": "Sonobarr marries your existing Lidarr library with Last.fm’s discovery graph to surface artists you'll actually like. It runs as a Flask + Socket.IO application, ships with a polished Bootstrap UI, and includes admin tooling so folks can share a single instance safely.", + "install_methods": [ + { + "type": "default", + "script": "ct/sonobarr.sh", + "resources": { + "cpu": 1, + "ram": 1024, + "hdd": 20, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Default generated admin password is in the env file (sonobarr_superadmin_password)", + "type": "info" + } + ] +} diff --git a/frontend/public/json/truenas-vm.json b/frontend/public/json/truenas-vm.json new file mode 100644 index 000000000..7f11e7564 --- /dev/null +++ b/frontend/public/json/truenas-vm.json @@ -0,0 +1,40 @@ +{ + "name": "TrueNAS Community Edition", + "slug": "truenas-community-edition", + "categories": [ + 2 + ], + "date_created": "2026-01-16", + "type": "vm", + "updateable": true, + "privileged": false, + "interface_port": null, + "documentation": "https://www.truenas.com/docs/", + "website": "https://www.truenas.com/truenas-community-edition/", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/truenas-core.webp", + "config_path": "", + "description": "TrueNAS Community Edition is the world's most deployed storage software. Free, flexible and build on OpenZFS with Docker.", + "install_methods": [ + { + "type": "default", + "script": "vm/truenas-vm.sh", + "resources": { + "cpu": 2, + "ram": 8192, + "hdd": 16, + "os": null, + "version": null + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Once the script finishes, proceed with the OS installation via the console. For more details, please refer to this discussion: `https://github.com/community-scripts/ProxmoxVE/discussions/11344`", + "type": "info" + } + ] +} diff --git a/frontend/public/json/update-apps.json b/frontend/public/json/update-apps.json index 0cde58439..c94e9a327 100644 --- a/frontend/public/json/update-apps.json +++ b/frontend/public/json/update-apps.json @@ -51,6 +51,30 @@ { "text": "At the end of the update, containers requiring a reboot will be listed, and you may choose to reboot them directly.", "type": "info" + }, + { + "text": "Use `var_backup=yes|no` to enable/disable backup (skip prompt).", + "type": "info" + }, + { + "text": "Use `var_backup_storage=` to set backup storage location.", + "type": "info" + }, + { + "text": "Use `var_container=all|all_running|all_stopped|101,102,...` to select containers.", + "type": "info" + }, + { + "text": "Use `var_unattended=yes|no` to run updates without interaction.", + "type": "info" + }, + { + "text": "Use `var_skip_confirm=yes` to skip initial confirmation dialog.", + "type": "info" + }, + { + "text": "Use `var_auto_reboot=yes|no` to auto-reboot containers after update.", + "type": "info" } ] } diff --git a/frontend/public/json/writefreely.json b/frontend/public/json/writefreely.json new file mode 100644 index 000000000..984243807 --- /dev/null +++ b/frontend/public/json/writefreely.json @@ -0,0 +1,40 @@ +{ + "name": "WriteFreely", + "slug": "writefreely", + "categories": [ + 12 + ], + "date_created": "2026-01-25", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 80, + "documentation": "https://writefreely.org/docs", + "config_path": "/opt/writefreely/config.ini", + "website": "https://writefreely.org/", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/writefreely-light.webp", + "description": "WriteFreely is free and open source software for easily publishing writing on the web with support for the ActivityPub protocol. Use it to start a personal blog — or an entire community.", + "install_methods": [ + { + "type": "default", + "script": "ct/writefreely.sh", + "resources": { + "cpu": 2, + "ram": 1024, + "hdd": 4, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "After installation execute `./writefreely user create --admin username:password` in the `/opt/writefreely` directory to create your user.", + "type": "info" + } + ] +} diff --git a/frontend/src/__tests__/public/validate-json.test.ts b/frontend/src/__tests__/public/validate-json.test.ts index 1ab52db68..f0ab7dde1 100644 --- a/frontend/src/__tests__/public/validate-json.test.ts +++ b/frontend/src/__tests__/public/validate-json.test.ts @@ -7,16 +7,17 @@ console.log('Current directory: ' + process.cwd()); const jsonDir = "public/json"; const metadataFileName = "metadata.json"; const versionsFileName = "versions.json"; +const githubVersionsFileName = "github-versions.json"; const encoding = "utf-8"; const fileNames = (await fs.readdir(jsonDir)) - .filter((fileName) => fileName !== metadataFileName && fileName !== versionsFileName); + .filter((fileName) => fileName !== metadataFileName && fileName !== versionsFileName && fileName !== githubVersionsFileName); describe.each(fileNames)("%s", async (fileName) => { let script: Script; beforeAll(async () => { - const filePath = path.resolve(jsonDir, fileName); + const filePath = path.resolve(jsonDir, fileName); const fileContent = await fs.readFile(filePath, encoding) script = JSON.parse(fileContent); }) @@ -40,7 +41,7 @@ describe(`${metadataFileName}`, async () => { let metadata: Metadata; beforeAll(async () => { - const filePath = path.resolve(jsonDir, metadataFileName); + const filePath = path.resolve(jsonDir, metadataFileName); const fileContent = await fs.readFile(filePath, encoding) metadata = JSON.parse(fileContent); }) @@ -48,9 +49,9 @@ describe(`${metadataFileName}`, async () => { // TODO: create zod schema for metadata. Move zod schemas to /lib/types.ts assert(metadata.categories.length > 0); metadata.categories.forEach((category) => { - assert.isString(category.name) - assert.isNumber(category.id) - assert.isNumber(category.sort_order) + assert.isString(category.name) + assert.isNumber(category.id) + assert.isNumber(category.sort_order) }); }); }) diff --git a/install/affine-install.sh b/install/affine-install.sh index ce3cb9fac..5cce12462 100644 --- a/install/affine-install.sh +++ b/install/affine-install.sh @@ -29,7 +29,7 @@ PG_VERSION="16" PG_MODULES="pgvector" setup_postgresql PG_DB_NAME="affine" PG_DB_USER="affine" setup_postgresql_db NODE_VERSION="22" setup_nodejs setup_rust -import_local_ip + fetch_and_deploy_gh_release "affine_app" "toeverything/AFFiNE" "tarball" "latest" "/opt/affine" diff --git a/install/alpine-powerdns-install.sh b/install/alpine-powerdns-install.sh new file mode 100644 index 000000000..b3cbc6d65 --- /dev/null +++ b/install/alpine-powerdns-install.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Slaviša Arežina (tremor021) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://www.powerdns.com/ + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing PowerDNS" +$STD apk add --no-cache pdns pdns-backend-sqlite3 pdns-doc +msg_ok "Installed PowerDNS" + +msg_info "Configuring PowerDNS" +sed -i '/^# launch=$/c\launch=gsqlite3\ngsqlite3-database=/var/lib/powerdns/pdns.sqlite3' /etc/pdns/pdns.conf +mkdir /var/lib/powerdns +sqlite3 /var/lib/powerdns/pdns.sqlite3 < /usr/share/doc/pdns/schema.sqlite3.sql +chown -R pdns:pdns /var/lib/powerdns +msg_ok "Configured PowerDNS" + +msg_info "Creating Service" +$STD rc-update add pdns default +$STD rc-service pdns start +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/alpine-rustypaste-install.sh b/install/alpine-rustypaste-install.sh deleted file mode 100644 index 1f645c71e..000000000 --- a/install/alpine-rustypaste-install.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/orhun/rustypaste - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -msg_info "Installing RustyPaste" -$STD apk add --no-cache rustypaste --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community -msg_ok "Installed RustyPaste" - -msg_info "Configuring RustyPaste" -mkdir -p /var/lib/rustypaste -sed -i 's|^address = ".*"|address = "0.0.0.0:8000"|' /etc/rustypaste/config.toml -msg_ok "Configured RustyPaste" - -msg_info "Creating Service" -$STD rc-update add rustypaste default -$STD rc-service rustypaste start -msg_ok "Created Service" - -motd_ssh -customize -cleanup_lxc diff --git a/install/alpine-valkey-install.sh b/install/alpine-valkey-install.sh deleted file mode 100644 index 66501b3eb..000000000 --- a/install/alpine-valkey-install.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: pshankinclarke (lazarillo) -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Source: https://valkey.io/ - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -msg_info "Installing Valkey" -$STD apk add valkey valkey-openrc valkey-cli -$STD sed -i 's/^bind .*/bind 0.0.0.0/' /etc/valkey/valkey.conf -$STD rc-update add valkey default -$STD rc-service valkey start -msg_ok "Installed Valkey" - -motd_ssh -customize diff --git a/install/ampache-install.sh b/install/ampache-install.sh deleted file mode 100644 index 811548ed5..000000000 --- a/install/ampache-install.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/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/ampache/ampache - -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 \ - flac \ - vorbis-tools \ - lame \ - ffmpeg \ - inotify-tools \ - libavcodec-extra \ - libmp3lame-dev \ - libtheora-dev \ - libvorbis-dev \ - libvpx-dev -msg_ok "Installed Dependencies" - -PHP_VERSION=8.4 PHP_MODULE=bcmath,bz2,curl,gd,imagick,intl,mbstring,mysql,sqlite3,xml,xmlrpc,zip PHP_APACHE=YES setup_php -setup_mariadb -MARIADB_DB_USER=ampache MARIADB_DB_NAME=ampache setup_mariadb_db - -fetch_and_deploy_gh_release "ampache" "ampache/ampache" "prebuild" "latest" "/opt/ampache" "ampache-*_all_php8.4.zip" - -msg_info "Setup Ampache" -rm -rf /var/www/html -ln -s /opt/ampache/public /var/www/html -mv /opt/ampache/public/rest/.htaccess.dist /opt/ampache/public/rest/.htaccess -mv /opt/ampache/public/play/.htaccess.dist /opt/ampache/public/play/.htaccess -cp /opt/ampache/config/ampache.cfg.php.dist /opt/ampache/config/ampache.cfg.php -chmod 664 /opt/ampache/public/rest/.htaccess /opt/ampache/public/play/.htaccess -msg_ok "Set up Ampache" - -msg_info "Configuring Database Connection" -sed -i 's|^database_hostname = .*|database_hostname = "localhost"|' /opt/ampache/config/ampache.cfg.php -sed -i 's|^database_name = .*|database_name = "ampache"|' /opt/ampache/config/ampache.cfg.php -sed -i 's|^database_username = .*|database_username = "ampache"|' /opt/ampache/config/ampache.cfg.php -sed -i "s|^database_password = .*|database_password = \"${MARIADB_DB_PASS}\"|" /opt/ampache/config/ampache.cfg.php -chown -R www-data:www-data /opt/ampache -msg_ok "Configured Database Connection" - -msg_info "Importing Database Schema" -mariadb -u ampache -p"${MARIADB_DB_PASS}" ampache >/etc/mongod.conf + +replication: + replSetName: "rs0" +EOF +systemctl restart mongod +sleep 3 +$STD mongosh --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "127.0.0.1:27017"}]})' +msg_ok "Configured MongoDB Replica Set" + +msg_info "Installing Redis Stack" +setup_deb822_repo \ + "redis-stack" \ + "https://packages.redis.io/gpg" \ + "https://packages.redis.io/deb" \ + "jammy" \ + "main" +$STD apt-get install -y \ + redis-stack-server +systemctl enable -q --now redis-stack-server +msg_ok "Installed Redis Stack" + +fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz" +chmod +x /opt/anytype/any-sync-bundle + +msg_info "Configuring Anytype" +mkdir -p /opt/anytype/data/storage +cat </opt/anytype/.env +ANY_SYNC_BUNDLE_CONFIG=/opt/anytype/data/bundle-config.yml +ANY_SYNC_BUNDLE_CLIENT_CONFIG=/opt/anytype/data/client-config.yml +ANY_SYNC_BUNDLE_INIT_STORAGE=/opt/anytype/data/storage/ +ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS=${LOCAL_IP} +ANY_SYNC_BUNDLE_INIT_MONGO_URI=mongodb://127.0.0.1:27017/ +ANY_SYNC_BUNDLE_INIT_REDIS_URI=redis://127.0.0.1:6379/ +ANY_SYNC_BUNDLE_LOG_LEVEL=info +EOF +msg_ok "Configured Anytype" + +msg_info "Creating Service" +cat </etc/systemd/system/anytype.service +[Unit] +Description=Anytype Sync Server (any-sync-bundle) +After=network-online.target mongod.service redis-stack-server.service +Wants=network-online.target +Requires=mongod.service redis-stack-server.service + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/anytype +EnvironmentFile=/opt/anytype/.env +ExecStart=/opt/anytype/any-sync-bundle start-bundle +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now anytype +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/authelia-install.sh b/install/authelia-install.sh new file mode 100644 index 000000000..010be6cab --- /dev/null +++ b/install/authelia-install.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: thost96 (thost96) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://www.authelia.com/ + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +fetch_and_deploy_gh_release "authelia" "authelia/authelia" "binary" + +MAX_ATTEMPTS=3 +attempt=0 +while true; do + attempt=$((attempt + 1)) + read -rp "${TAB3}Enter your domain or IP (ex. example.com or 192.168.1.100): " DOMAIN + if [[ -z "$DOMAIN" ]]; then + if ((attempt >= MAX_ATTEMPTS)); then + DOMAIN="${LOCAL_IP:-localhost}" + msg_warn "Using fallback: $DOMAIN" + break + fi + msg_warn "Domain cannot be empty! (Attempt $attempt/$MAX_ATTEMPTS)" + elif [[ "$DOMAIN" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + valid_ip=true + IFS='.' read -ra octets <<< "$DOMAIN" + for octet in "${octets[@]}"; do + if ((octet > 255)); then + valid_ip=false + break + fi + done + if $valid_ip; then + break + else + msg_warn "Invalid IP address!" + fi + elif [[ "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$ ]]; then + break + else + msg_warn "Invalid domain format!" + fi +done +msg_info "Setting Authelia up" +touch /etc/authelia/emails.txt +JWT_SECRET=$(openssl rand -hex 64) +SESSION_SECRET=$(openssl rand -hex 64) +STORAGE_KEY=$(openssl rand -hex 64) + +if [[ "$DOMAIN" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + AUTHELIA_URL="https://${DOMAIN}:9091" +else + AUTHELIA_URL="https://auth.${DOMAIN}" +fi +echo "$AUTHELIA_URL" > /etc/authelia/.authelia_url + +cat </etc/authelia/users.yml +users: + authelia: + disabled: false + displayname: "Authelia Admin" + password: "\$argon2id\$v=19\$m=65536,t=3,p=4\$ZBopMzXrzhHXPEZxRDVT2w\$SxWm96DwhOsZyn34DLocwQEIb4kCDsk632PuiMdZnig" + groups: [] +EOF +cat </etc/authelia/configuration.yml +authentication_backend: + file: + path: /etc/authelia/users.yml +access_control: + default_policy: one_factor +session: + secret: "${SESSION_SECRET}" + name: 'authelia_session' + same_site: 'lax' + inactivity: '5m' + expiration: '1h' + remember_me: '1M' + cookies: + - domain: "${DOMAIN}" + authelia_url: "${AUTHELIA_URL}" +storage: + encryption_key: "${STORAGE_KEY}" + local: + path: /etc/authelia/db.sqlite +identity_validation: + reset_password: + jwt_secret: "${JWT_SECRET}" + jwt_lifespan: '5 minutes' + jwt_algorithm: 'HS256' +notifier: + filesystem: + filename: /etc/authelia/emails.txt +EOF +touch /etc/authelia/emails.txt +chown -R authelia:authelia /etc/authelia +systemctl enable -q --now authelia +msg_ok "Authelia Setup completed" + +motd_ssh +customize +cleanup_lxc diff --git a/install/checkmate-install.sh b/install/checkmate-install.sh new file mode 100644 index 000000000..614533829 --- /dev/null +++ b/install/checkmate-install.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/bluewave-labs/Checkmate + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt-get install -y \ + build-essential \ + openssl +msg_ok "Installed Dependencies" + +MONGO_VERSION="8.0" setup_mongodb +NODE_VERSION="22" setup_nodejs +fetch_and_deploy_gh_release "checkmate" "bluewave-labs/Checkmate" + +msg_info "Installing Checkmate Server" +cd /opt/checkmate/server +$STD npm install +msg_ok "Installed Checkmate Server" + +msg_info "Installing Checkmate Client" +cd /opt/checkmate/client +$STD npm install +$STD npm run build +msg_ok "Installed Checkmate Client" + +msg_info "Configuring Checkmate" +JWT_SECRET="$(openssl rand -hex 32)" +cat </opt/checkmate/server/.env +CLIENT_HOST="http://${LOCAL_IP}:5173" +JWT_SECRET="${JWT_SECRET}" +DB_CONNECTION_STRING="mongodb://localhost:27017/checkmate_db" +TOKEN_TTL="99d" +ORIGIN="${LOCAL_IP}" +LOG_LEVEL="info" +EOF + +cat </opt/checkmate/client/.env +VITE_APP_API_BASE_URL="http://${LOCAL_IP}:52345/api/v1" +VITE_APP_LOG_LEVEL="warn" +EOF +msg_ok "Configured Checkmate" + +msg_info "Creating Services" +cat </etc/systemd/system/checkmate-server.service +[Unit] +Description=Checkmate Server +After=network.target mongod.service + +[Service] +Type=simple +WorkingDirectory=/opt/checkmate/server +EnvironmentFile=/opt/checkmate/server/.env +ExecStart=/usr/bin/npm start +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +cat </etc/systemd/system/checkmate-client.service +[Unit] +Description=Checkmate Client +After=network.target checkmate-server.service + +[Service] +Type=simple +WorkingDirectory=/opt/checkmate/client +EnvironmentFile=/opt/checkmate/client/.env +ExecStart=/usr/bin/npm run preview -- --host 0.0.0.0 --port 5173 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now checkmate-server checkmate-client +msg_ok "Created Services" + +motd_ssh +customize +cleanup_lxc diff --git a/install/clawdbot-install.sh b/install/clawdbot-install.sh new file mode 100644 index 000000000..63556fb45 --- /dev/null +++ b/install/clawdbot-install.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: michelroegl-brunner +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/clawdbot/clawdbot + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + build-essential \ + git +msg_ok "Installed Dependencies" + + +NODE_VERSION="24" NODE_MODULE="pnpm@latest" setup_nodejs + +curl -fsSL https://clawd.bot/install.sh | bash + + +motd_ssh +customize +cleanup_lxc + diff --git a/install/databasus-install.sh b/install/databasus-install.sh index fbac56054..0b068d865 100644 --- a/install/databasus-install.sh +++ b/install/databasus-install.sh @@ -14,12 +14,12 @@ network_check update_os msg_info "Installing Dependencies" -$STD apt install -y nginx +$STD apt install -y \ + nginx \ + valkey msg_ok "Installed Dependencies" -import_local_ip PG_VERSION="17" setup_postgresql -PG_DB_NAME="databasus" PG_DB_USER="databasus" setup_postgresql_db setup_go NODE_VERSION="24" setup_nodejs @@ -36,19 +36,17 @@ $STD go install github.com/swaggo/swag/cmd/swag@latest $STD /root/go/bin/swag init -g cmd/main.go -o swagger $STD env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o databasus ./cmd/main.go mv /opt/databasus/backend/databasus /opt/databasus/databasus -mkdir -p /opt/databasus_data/{data,backups,logs} -mkdir -p /databasus-data/temp +mkdir -p /databasus-data/{pgdata,temp,backups,data,logs} mkdir -p /opt/databasus/ui/build +mkdir -p /opt/databasus/migrations cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/ -cp -r /opt/databasus/backend/migrations /opt/databasus/ -chown -R postgres:postgres /opt/databasus -chown -R postgres:postgres /opt/databasus_data +cp -r /opt/databasus/backend/migrations/* /opt/databasus/migrations/ chown -R postgres:postgres /databasus-data msg_ok "Built Databasus" msg_info "Configuring Databasus" -ADMIN_PASS=$(openssl rand -base64 12) JWT_SECRET=$(openssl rand -hex 32) +ENCRYPTION_KEY=$(openssl rand -hex 32) # Create PostgreSQL version symlinks for compatibility for v in 12 13 14 15 16 18; do @@ -67,50 +65,67 @@ ENV_MODE=production SERVER_PORT=4005 SERVER_HOST=0.0.0.0 -# Database (Internal PostgreSQL for app data) -DATABASE_DSN=host=localhost user=${PG_DB_USER} password=${PG_DB_PASS} dbname=${PG_DB_NAME} port=5432 sslmode=disable -DATABASE_URL=postgres://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}?sslmode=disable +# Database +DATABASE_DSN=host=localhost user=postgres password=postgres dbname=databasus port=5432 sslmode=disable +DATABASE_URL=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable # Migrations GOOSE_DRIVER=postgres -GOOSE_DBSTRING=postgres://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}?sslmode=disable +GOOSE_DBSTRING=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable GOOSE_MIGRATION_DIR=/opt/databasus/migrations +# Valkey (Redis-compatible cache) +VALKEY_HOST=localhost +VALKEY_PORT=6379 + # Security JWT_SECRET=${JWT_SECRET} -ENCRYPTION_KEY=$(openssl rand -hex 32) - -# Admin User -ADMIN_EMAIL=admin@localhost -ADMIN_PASSWORD=${ADMIN_PASS} +ENCRYPTION_KEY=${ENCRYPTION_KEY} # Paths -DATA_DIR=/opt/databasus_data/data -BACKUP_DIR=/opt/databasus_data/backups -LOG_DIR=/opt/databasus_data/logs - -# PostgreSQL Tools (for creating backups) -PG_DUMP_PATH=/usr/lib/postgresql/17/bin/pg_dump -PG_RESTORE_PATH=/usr/lib/postgresql/17/bin/pg_restore -PSQL_PATH=/usr/lib/postgresql/17/bin/psql +DATA_DIR=/databasus-data/data +BACKUP_DIR=/databasus-data/backups +LOG_DIR=/databasus-data/logs EOF chown postgres:postgres /opt/databasus/.env chmod 600 /opt/databasus/.env msg_ok "Configured Databasus" +msg_info "Configuring Valkey" +cat >/etc/valkey/valkey.conf </dev/null || true +$STD sudo -u postgres psql -c "ALTER USER postgres WITH SUPERUSER CREATEROLE CREATEDB;" 2>/dev/null || true +msg_ok "Created Database" + msg_info "Creating Databasus Service" cat </etc/systemd/system/databasus.service [Unit] -Description=Databasus - PostgreSQL Backup Management -After=network.target postgresql.service -Requires=postgresql.service +Description=Databasus - Database Backup Management +After=network.target postgresql.service valkey.service +Requires=postgresql.service valkey.service [Service] Type=simple -User=postgres -Group=postgres WorkingDirectory=/opt/databasus -Environment="PATH=/usr/local/bin:/usr/bin:/bin" EnvironmentFile=/opt/databasus/.env ExecStart=/opt/databasus/databasus Restart=always diff --git a/install/dawarich-install.sh b/install/dawarich-install.sh deleted file mode 100644 index 0adc17a14..000000000 --- a/install/dawarich-install.sh +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/Freika/dawarich - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -msg_info "Installing Dependencies" -$STD apt install -y \ - build-essential \ - git \ - libpq-dev \ - libgeos-dev \ - libyaml-dev \ - libffi-dev \ - libssl-dev \ - libjemalloc2 \ - imagemagick \ - libmagickwand-dev \ - libvips-dev \ - cmake \ - redis-server \ - nginx -msg_ok "Installed Dependencies" - -PG_VERSION="17" PG_MODULES="postgis-3" setup_postgresql -PG_DB_NAME="dawarich_db" PG_DB_USER="dawarich" PG_DB_EXTENSIONS="postgis" setup_postgresql_db - -fetch_and_deploy_gh_release "dawarich" "Freika/dawarich" "tarball" "latest" "/opt/dawarich/app" - -msg_info "Setting up Directories" -mkdir -p /opt/dawarich/app/{storage,log,tmp/pids,tmp/cache,tmp/sockets} -msg_ok "Set up Directories" - -msg_info "Configuring Environment" -SECRET_KEY_BASE=$(openssl rand -hex 64) -RELEASE=$(get_latest_github_release "Freika/dawarich") -cat </opt/dawarich/.env -RAILS_ENV=production -SECRET_KEY_BASE=${SECRET_KEY_BASE} -DATABASE_HOST=localhost -DATABASE_USERNAME=${PG_DB_USER} -DATABASE_PASSWORD=${PG_DB_PASS} -DATABASE_NAME=${PG_DB_NAME} -REDIS_URL=redis://127.0.0.1:6379/0 -BACKGROUND_PROCESSING_CONCURRENCY=10 -APPLICATION_HOST=${LOCAL_IP} -APPLICATION_HOSTS=${LOCAL_IP},localhost -TIME_ZONE=UTC -DISABLE_TELEMETRY=true -APP_VERSION=${RELEASE} -EOF -msg_ok "Configured Environment" - -NODE_VERSION="22" setup_nodejs -RUBY_VERSION=$(cat /opt/dawarich/app/.ruby-version 2>/dev/null || echo "3.4.6") -RUBY_VERSION=${RUBY_VERSION} RUBY_INSTALL_RAILS="false" setup_ruby - -msg_info "Installing Dawarich" -cd /opt/dawarich/app -source /root/.profile -export PATH="/root/.rbenv/shims:/root/.rbenv/bin:$PATH" -eval "$(/root/.rbenv/bin/rbenv init - bash)" -set -a && source /opt/dawarich/.env && set +a -$STD gem install bundler -$STD bundle config set --local deployment 'true' -$STD bundle config set --local without 'development test' -$STD bundle install -if [[ -f /opt/dawarich/package.json ]]; then - cd /opt/dawarich - $STD npm install - cd /opt/dawarich/app -elif [[ -f /opt/dawarich/app/package.json ]]; then - $STD npm install -fi -$STD bundle exec rake assets:precompile -$STD bundle exec rails db:prepare -$STD bundle exec rake data:migrate -msg_ok "Installed Dawarich" - -msg_info "Creating Services" -cat </etc/systemd/system/dawarich-web.service -[Unit] -Description=Dawarich Web Server -After=network.target postgresql.service redis-server.service -Requires=postgresql.service redis-server.service - -[Service] -Type=simple -WorkingDirectory=/opt/dawarich/app -EnvironmentFile=/opt/dawarich/.env -ExecStart=/root/.rbenv/shims/bundle exec puma -C config/puma.rb -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF - -cat </etc/systemd/system/dawarich-worker.service -[Unit] -Description=Dawarich Sidekiq Worker -After=network.target postgresql.service redis-server.service -Requires=postgresql.service redis-server.service - -[Service] -Type=simple -WorkingDirectory=/opt/dawarich/app -EnvironmentFile=/opt/dawarich/.env -ExecStart=/root/.rbenv/shims/bundle exec sidekiq -C config/sidekiq.yml -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF - -systemctl enable -q --now redis-server dawarich-web dawarich-worker -msg_ok "Created Services" - -msg_info "Configuring Nginx" -cat </etc/nginx/sites-available/dawarich.conf -upstream dawarich { - server 127.0.0.1:3000; -} - -server { - listen 80; - server_name _; - - root /opt/dawarich/app/public; - client_max_body_size 100M; - - location ~ ^/(assets|packs)/ { - expires max; - add_header Cache-Control "public, immutable"; - try_files \$uri =404; - } - - location / { - try_files \$uri @rails; - } - - location @rails { - proxy_pass http://dawarich; - proxy_http_version 1.1; - proxy_set_header Host \$host; - proxy_set_header X-Real-IP \$remote_addr; - proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto \$scheme; - proxy_set_header Upgrade \$http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_redirect off; - proxy_buffering off; - } -} -EOF -ln -sf /etc/nginx/sites-available/dawarich.conf /etc/nginx/sites-enabled/ -rm -f /etc/nginx/sites-enabled/default -systemctl enable -q --now nginx -msg_ok "Configured Nginx" - -motd_ssh -customize -cleanup_lxc diff --git a/install/deferred/docspell-install.sh b/install/deferred/docspell-install.sh index 794e4c9bb..3738c816b 100644 --- a/install/deferred/docspell-install.sh +++ b/install/deferred/docspell-install.sh @@ -14,7 +14,7 @@ update_os msg_info "Setup Functions" setup_local_ip_helper -import_local_ip + msg_ok "Setup Functions" msg_info "Installing Dependencies (Patience)" diff --git a/install/ebusd-install.sh b/install/ebusd-install.sh new file mode 100644 index 000000000..ab74a8312 --- /dev/null +++ b/install/ebusd-install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Joerg Heinemann (heinemannj) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/john30/ebusd + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +setup_deb822_repo \ + "ebusd" \ + "https://raw.githubusercontent.com/john30/ebusd-debian/master/ebusd.gpg" \ + "https://repo.ebusd.eu/apt/default/bookworm/" \ + "bookworm" \ + "main" + +msg_info "Installing ebusd" +$STD apt install -y ebusd +systemctl enable -q ebusd +msg_ok "Installed ebusd" + +motd_ssh +customize +cleanup_lxc diff --git a/install/ente-install.sh b/install/ente-install.sh index a6028e3cb..e1b9298e7 100644 --- a/install/ente-install.sh +++ b/install/ente-install.sh @@ -28,7 +28,7 @@ setup_go NODE_VERSION="24" NODE_MODULE="yarn" setup_nodejs RUST_CRATES="wasm-pack" setup_rust $STD rustup target add wasm32-unknown-unknown -import_local_ip + ENTE_CLI_VERSION=$(curl -s https://api.github.com/repos/ente-io/ente/releases | jq -r '[.[] | select(.tag_name | startswith("cli-v"))][0].tag_name') fetch_and_deploy_gh_release "ente-server" "ente-io/ente" "tarball" "latest" "/opt/ente" diff --git a/install/forgejo-runner-install.sh b/install/forgejo-runner-install.sh index 367b2b76e..78f0a6e5f 100644 --- a/install/forgejo-runner-install.sh +++ b/install/forgejo-runner-install.sh @@ -12,19 +12,19 @@ setting_up_container network_check update_os -if [[ -z "$var_forgejo_instance" ]]; then - read -rp "Forgejo Instance URL (e.g. https://code.forgejo.org): " var_forgejo_instance -fi +# Get required configuration with sensible fallbacks for unattended mode +# These will show a warning if defaults are used +var_forgejo_instance=$(prompt_input_required \ + "Forgejo Instance URL:" \ + "${var_forgejo_instance:-https://codeberg.org}" \ + 120 \ + "var_forgejo_instance") -if [[ -z "$var_forgejo_runner_token" ]]; then - read -rp "Forgejo Runner Registration Token: " var_forgejo_runner_token - echo -fi - -if [[ -z "$var_forgejo_instance" || -z "$var_forgejo_runner_token" ]]; then - echo "❌ Forgejo instance URL and runner token are required." - exit 1 -fi +var_forgejo_runner_token=$(prompt_input_required \ + "Forgejo Runner Registration Token:" \ + "${var_forgejo_runner_token:-REPLACE_WITH_YOUR_TOKEN}" \ + 120 \ + "var_forgejo_runner_token") export FORGEJO_INSTANCE="$var_forgejo_instance" export FORGEJO_RUNNER_TOKEN="$var_forgejo_runner_token" @@ -78,6 +78,9 @@ EOF systemctl enable -q --now forgejo-runner msg_ok "Created Services" +# Show warning if any required values used fallbacks +show_missing_values_warning + motd_ssh customize cleanup_lxc diff --git a/install/frigate-install.sh b/install/frigate-install.sh index 26822920c..27bb74894 100644 --- a/install/frigate-install.sh +++ b/install/frigate-install.sh @@ -7,7 +7,6 @@ # Source: https://frigate.video/ source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -set +e color verb_ip6 catch_errors @@ -15,48 +14,80 @@ setting_up_container network_check update_os -cat <<'EOF' >/etc/apt/sources.list.d/debian.sources -Types: deb deb-src -URIs: http://deb.debian.org/debian -Suites: bookworm -Components: main contrib non-free non-free-firmware -Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg - -Types: deb deb-src -URIs: http://deb.debian.org/debian -Suites: bookworm-updates -Components: main contrib non-free non-free-firmware -Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg - -Types: deb deb-src -URIs: http://security.debian.org -Suites: bookworm-security -Components: main contrib non-free non-free-firmware -Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg -EOF -rm -f /etc/apt/sources.list - -msg_info "Installing system dependencies" -$STD apt-get install -y jq wget xz-utils python3 python3-dev python3-pip gcc pkg-config libhdf5-dev unzip build-essential automake libtool ccache libusb-1.0-0-dev apt-transport-https cmake git libgtk-3-dev libavcodec-dev libavformat-dev libswscale-dev libv4l-dev libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev gfortran openexr libssl-dev libtbbmalloc2 libtbb-dev libdc1394-dev libopenexr-dev libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev tclsh libopenblas-dev liblapack-dev make moreutils -msg_ok "System dependencies installed" - -setup_hwaccel - -if [[ "$CTTYPE" == "0" ]]; then - msg_info "Configuring render group for privileged container" - sed -i -e 's/^kvm:x:104:$/render:x:104:root,frigate/' -e 's/^render:x:105:root$/kvm:x:105:/' /etc/group - msg_ok "Privileged container GPU access configured" -else - msg_info "Configuring render group for unprivileged container" - sed -i -e 's/^kvm:x:104:$/render:x:104:frigate/' -e 's/^render:x:105:$/kvm:x:105:/' /etc/group - msg_ok "Unprivileged container GPU access configured" +source /etc/os-release +if [[ "$VERSION_ID" != "12" ]]; then + msg_error "Frigate requires Debian 12 (Bookworm) due to Python 3.11 dependencies" + exit 1 fi +msg_info "Installing Dependencies" +$STD apt-get install -y \ + jq \ + wget \ + xz-utils \ + python3 \ + python3-dev \ + python3-pip \ + gcc \ + pkg-config \ + libhdf5-dev \ + unzip \ + build-essential \ + automake \ + libtool \ + ccache \ + libusb-1.0-0-dev \ + apt-transport-https \ + cmake \ + git \ + libgtk-3-dev \ + libavcodec-dev \ + libavformat-dev \ + libswscale-dev \ + libv4l-dev \ + libxvidcore-dev \ + libx264-dev \ + libjpeg-dev \ + libpng-dev \ + libtiff-dev \ + gfortran \ + openexr \ + libssl-dev \ + libtbbmalloc2 \ + libtbb-dev \ + libdc1394-dev \ + libopenexr-dev \ + libgstreamer-plugins-base1.0-dev \ + libgstreamer1.0-dev \ + tclsh \ + libopenblas-dev \ + liblapack-dev \ + make \ + moreutils +msg_ok "Installed Dependencies" + +msg_info "Setting Up Hardware Acceleration" +# Use Debian 12 native packages instead of setup_hwaccel (Intel Arc latest drivers require Debian 13) +$STD apt-get install -y \ + vainfo \ + intel-media-va-driver-non-free \ + intel-gpu-tools \ + mesa-va-drivers \ + mesa-vulkan-drivers || true +msg_ok "Set Up Hardware Acceleration" + +msg_info "Configuring GPU Access" +if [[ "$CTTYPE" == "0" ]]; then + sed -i -e 's/^kvm:x:104:$/render:x:104:root,frigate/' -e 's/^render:x:105:root$/kvm:x:105:/' /etc/group +else + sed -i -e 's/^kvm:x:104:$/render:x:104:frigate/' -e 's/^render:x:105:$/kvm:x:105:/' /etc/group +fi +msg_ok "Configured GPU Access" + export TARGETARCH="amd64" export CCACHE_DIR=/root/.ccache export CCACHE_MAXSIZE=2G export APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn -export DEBIAN_FRONTEND=noninteractive export PIP_BREAK_SYSTEM_PACKAGES=1 export NVIDIA_VISIBLE_DEVICES=all export NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" @@ -67,91 +98,73 @@ export HAILORT_LOGGER_PATH=NONE fetch_and_deploy_gh_release "frigate" "blakeblackshear/frigate" "tarball" "latest" "/opt/frigate" -msg_info "Building Nginx with custom modules" -#sed -i 's|if.*"$VERSION_ID" == "12".*|if [[ "$VERSION_ID" =~ ^(12|13)$ ]]; then|g' /opt/frigate/docker/main/build_nginx.sh +msg_info "Building Nginx" $STD bash /opt/frigate/docker/main/build_nginx.sh sed -e '/s6-notifyoncheck/ s/^#*/#/' -i /opt/frigate/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run ln -sf /usr/local/nginx/sbin/nginx /usr/local/bin/nginx -msg_ok "Nginx built successfully" +msg_ok "Built Nginx" -msg_info "Building SQLite with custom modules" -#sed -i 's|if.*"$VERSION_ID" == "12".*|if [[ "$VERSION_ID" =~ ^(12|13)$ ]]; then|g' /opt/frigate/docker/main/build_sqlite_vec.sh +msg_info "Building SQLite Extensions" $STD bash /opt/frigate/docker/main/build_sqlite_vec.sh -msg_ok "SQLite built successfully" +msg_ok "Built SQLite Extensions" fetch_and_deploy_gh_release "go2rtc" "AlexxIT/go2rtc" "singlefile" "latest" "/usr/local/go2rtc/bin" "go2rtc_linux_amd64" -msg_info "Installing tempio" -export TARGETARCH=amd64 +msg_info "Installing Tempio" sed -i 's|/rootfs/usr/local|/usr/local|g' /opt/frigate/docker/main/install_tempio.sh $STD bash /opt/frigate/docker/main/install_tempio.sh ln -sf /usr/local/tempio/bin/tempio /usr/local/bin/tempio -msg_ok "tempio installed" +msg_ok "Installed Tempio" -msg_info "Building libUSB without udev" +msg_info "Building libUSB" cd /opt -wget -q https://github.com/libusb/libusb/archive/v1.0.26.zip -O v1.0.26.zip -$STD unzip -q v1.0.26.zip +wget -q https://github.com/libusb/libusb/archive/v1.0.26.zip -O libusb.zip +$STD unzip -q libusb.zip cd libusb-1.0.26 $STD ./bootstrap.sh $STD ./configure CC='ccache gcc' CCX='ccache g++' --disable-udev --enable-shared -$STD make -j $(nproc --all) +$STD make -j "$(nproc)" cd /opt/libusb-1.0.26/libusb -mkdir -p '/usr/local/lib' -$STD bash ../libtool --mode=install /usr/bin/install -c libusb-1.0.la '/usr/local/lib' -mkdir -p '/usr/local/include/libusb-1.0' -$STD install -c -m 644 libusb.h '/usr/local/include/libusb-1.0' -mkdir -p '/usr/local/lib/pkgconfig' +mkdir -p /usr/local/lib /usr/local/include/libusb-1.0 /usr/local/lib/pkgconfig +$STD bash ../libtool --mode=install /usr/bin/install -c libusb-1.0.la /usr/local/lib +install -c -m 644 libusb.h /usr/local/include/libusb-1.0 cd /opt/libusb-1.0.26/ -$STD install -c -m 644 libusb-1.0.pc '/usr/local/lib/pkgconfig' +install -c -m 644 libusb-1.0.pc /usr/local/lib/pkgconfig ldconfig -msg_ok "libUSB built successfully" +msg_ok "Built libUSB" -#msg_info "Setting up Python" -#$STD update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3 1 -#msg_ok "Python configured" - -#msg_info "Initializing pip" -#wget -q https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py -#sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' /tmp/get-pip.py -#$STD python3 /tmp/get-pip.py "pip" -#msg_ok "Pip initialized" - -msg_info "Installing Python dependencies from requirements" +msg_info "Installing Python Dependencies" $STD pip3 install -r /opt/frigate/docker/main/requirements.txt -msg_ok "Python dependencies installed" +msg_ok "Installed Python Dependencies" -msg_info "Building pysqlite3" +msg_info "Building Python Wheels (Patience)" +mkdir -p /wheels sed -i 's|^SQLITE3_VERSION=.*|SQLITE3_VERSION="version-3.46.0"|g' /opt/frigate/docker/main/build_pysqlite3.sh $STD bash /opt/frigate/docker/main/build_pysqlite3.sh -mkdir -p /wheels for i in {1..3}; do - msg_info "Building wheels (attempt $i/3)..." - pip3 wheel --wheel-dir=/wheels -r /opt/frigate/docker/main/requirements-wheels.txt --default-timeout=300 --retries=3 && break - if [[ $i -lt 3 ]]; then sleep 10; fi + $STD pip3 wheel --wheel-dir=/wheels -r /opt/frigate/docker/main/requirements-wheels.txt --default-timeout=300 --retries=3 && break + [[ $i -lt 3 ]] && sleep 10 done -msg_ok "pysqlite3 built successfully" +msg_ok "Built Python Wheels" NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs -msg_info "Downloading inference models" +msg_info "Downloading Inference Models" mkdir -p /models /openvino-model wget -q -O edgetpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -cd /models -wget -q -O cpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite +wget -q -O /models/cpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite cp /opt/frigate/labelmap.txt /labelmap.txt -msg_ok "Inference models downloaded" +msg_ok "Downloaded Inference Models" -msg_info "Downloading audio classification model" -cd / -wget -q -O yamnet-tflite.tar.gz https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download -$STD tar xzf yamnet-tflite.tar.gz -mv 1.tflite cpu_audio_model.tflite +msg_info "Downloading Audio Model" +wget -q -O /tmp/yamnet.tar.gz https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download +$STD tar xzf /tmp/yamnet.tar.gz -C / +mv /1.tflite /cpu_audio_model.tflite cp /opt/frigate/audio-labelmap.txt /audio-labelmap.txt -rm -f yamnet-tflite.tar.gz -msg_ok "Audio model prepared" +rm -f /tmp/yamnet.tar.gz +msg_ok "Downloaded Audio Model" -msg_info "Building HailoRT runtime" +msg_info "Installing HailoRT Runtime" $STD bash /opt/frigate/docker/main/install_hailort.sh cp -a /opt/frigate/docker/main/rootfs/. / sed -i '/^.*unset DEBIAN_FRONTEND.*$/d' /opt/frigate/docker/main/install_deps.sh @@ -160,25 +173,24 @@ echo "libedgetpu1-max libedgetpu/install-confirm-max boolean true" | debconf-set $STD bash /opt/frigate/docker/main/install_deps.sh $STD pip3 install -U /wheels/*.whl ldconfig -$STD pip3 install -U /wheels/*.whl -msg_ok "HailoRT runtime built" +msg_ok "Installed HailoRT Runtime" -msg_info "Installing OpenVino runtime and libraries" +msg_info "Installing OpenVino" $STD pip3 install -r /opt/frigate/docker/main/requirements-ov.txt -msg_ok "OpenVino installed" +msg_ok "Installed OpenVino" -msg_info "Preparing OpenVino inference model" +msg_info "Building OpenVino Model" cd /models wget -q http://download.tensorflow.org/models/object_detection/ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz $STD tar -zxf ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz --no-same-owner $STD python3 /opt/frigate/docker/main/build_ov_model.py -cp -r /models/ssdlite_mobilenet_v2.xml /openvino-model/ -cp -r /models/ssdlite_mobilenet_v2.bin /openvino-model/ +cp /models/ssdlite_mobilenet_v2.xml /openvino-model/ +cp /models/ssdlite_mobilenet_v2.bin /openvino-model/ wget -q https://github.com/openvinotoolkit/open_model_zoo/raw/master/data/dataset_classes/coco_91cl_bkgr.txt -O /openvino-model/coco_91cl_bkgr.txt sed -i 's/truck/car/g' /openvino-model/coco_91cl_bkgr.txt -msg_ok "OpenVino model prepared" +msg_ok "Built OpenVino Model" -msg_info "Building Frigate application" +msg_info "Building Frigate Application (Patience)" cd /opt/frigate $STD pip3 install -r /opt/frigate/docker/main/requirements-dev.txt $STD bash /opt/frigate/.devcontainer/initialize.sh @@ -187,31 +199,22 @@ cd /opt/frigate/web $STD npm install $STD npm run build cp -r /opt/frigate/web/dist/* /opt/frigate/web/ -cd /opt/frigate sed -i '/^s6-svc -O \.$/s/^/#/' /opt/frigate/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run -msg_ok "Frigate application built" +msg_ok "Built Frigate Application" -msg_info "Preparing configuration directories" +msg_info "Configuring Frigate" mkdir -p /config /media/frigate cp -r /opt/frigate/config/. /config -msg_ok "Configuration directories prepared" -msg_info "Setting up sample video" curl -fsSL "https://github.com/intel-iot-devkit/sample-videos/raw/master/person-bicycle-car-detection.mp4" -o "/media/frigate/person-bicycle-car-detection.mp4" -msg_ok "Sample video downloaded" -msg_info "Configuring tmpfs cache" echo "tmpfs /tmp/cache tmpfs defaults 0 0" >>/etc/fstab -msg_ok "Cache tmpfs configured" -msg_info "Creating environment configuration" cat </etc/frigate.env DEFAULT_FFMPEG_VERSION="7.0" INCLUDED_FFMPEG_VERSIONS="7.0:5.0" EOF -msg_ok "Environment file created" -msg_info "Creating base Frigate configuration" cat </config/config.yml mqtt: enabled: false @@ -233,12 +236,8 @@ auth: detect: enabled: false EOF -msg_ok "Base Frigate configuration created" -msg_info "Configuring object detection model" -if grep -q -o -m1 -E 'avx[^ ]* | sse4_2' /proc/cpuinfo; then - msg_ok "AVX or SSE 4.2 support detected" - msg_info "Configuring hardware-accelerated OpenVino model" +if grep -q -o -m1 -E 'avx[^ ]*|sse4_2' /proc/cpuinfo; then cat <>/config/config.yml ffmpeg: hwaccel_args: auto @@ -253,19 +252,17 @@ model: path: /openvino-model/ssdlite_mobilenet_v2.xml labelmap_path: /openvino-model/coco_91cl_bkgr.txt EOF - msg_ok "OpenVino model configured" else - msg_info "Configuring CPU-only object detection model" cat <>/config/config.yml ffmpeg: hwaccel_args: auto model: path: /cpu_model.tflite EOF - msg_ok "CPU model configured" fi +msg_ok "Configured Frigate" -msg_info "Creating systemd services" +msg_info "Creating Services" cat </etc/systemd/system/create_directories.service [Unit] Description=Create necessary directories for Frigate logs @@ -291,7 +288,7 @@ Restart=always RestartSec=1 User=root EnvironmentFile=/etc/frigate.env -ExecStartPre=+rm /dev/shm/logs/go2rtc/current +ExecStartPre=+rm -f /dev/shm/logs/go2rtc/current ExecStart=/bin/bash -c "bash /opt/frigate/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run 2> >(/usr/bin/ts '%%Y-%%m-%%d %%H:%%M:%%.S ' >&2) | /usr/bin/ts '%%Y-%%m-%%d %%H:%%M:%%.S '" StandardOutput=file:/dev/shm/logs/go2rtc/current StandardError=file:/dev/shm/logs/go2rtc/current @@ -312,7 +309,7 @@ Restart=always RestartSec=1 User=root EnvironmentFile=/etc/frigate.env -ExecStartPre=+rm /dev/shm/logs/frigate/current +ExecStartPre=+rm -f /dev/shm/logs/frigate/current ExecStart=/bin/bash -c "bash /opt/frigate/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run 2> >(/usr/bin/ts '%%Y-%%m-%%d %%H:%%M:%%.S ' >&2) | /usr/bin/ts '%%Y-%%m-%%d %%H:%%M:%%.S '" StandardOutput=file:/dev/shm/logs/frigate/current StandardError=file:/dev/shm/logs/frigate/current @@ -332,7 +329,7 @@ Type=simple Restart=always RestartSec=1 User=root -ExecStartPre=+rm /dev/shm/logs/nginx/current +ExecStartPre=+rm -f /dev/shm/logs/nginx/current ExecStart=/bin/bash -c "bash /opt/frigate/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run 2> >(/usr/bin/ts '%%Y-%%m-%%d %%H:%%M:%%.S ' >&2) | /usr/bin/ts '%%Y-%%m-%%d %%H:%%M:%%.S '" StandardOutput=file:/dev/shm/logs/nginx/current StandardError=file:/dev/shm/logs/nginx/current @@ -341,7 +338,7 @@ StandardError=file:/dev/shm/logs/nginx/current WantedBy=multi-user.target EOF -$STD systemctl daemon-reload +systemctl daemon-reload systemctl enable -q --now create_directories sleep 2 systemctl enable -q --now go2rtc @@ -349,13 +346,11 @@ sleep 2 systemctl enable -q --now frigate sleep 2 systemctl enable -q --now nginx -msg_ok "Systemd services created and enabled" +msg_ok "Created Services" -msg_info "Cleaning up temporary files and caches" -rm -rf /opt/v*.zip /opt/libusb-1.0.26 /tmp/get-pip.py -$STD apt-get -y autoremove -$STD apt-get -y autoclean -msg_ok "Cleanup completed" +msg_info "Cleaning Up" +rm -rf /opt/libusb.zip /opt/libusb-1.0.26 /wheels /models/*.tar.gz +msg_ok "Cleaned Up" motd_ssh customize diff --git a/install/ghost-install.sh b/install/ghost-install.sh new file mode 100644 index 000000000..4cee724a6 --- /dev/null +++ b/install/ghost-install.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: fabrice1236 +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://ghost.org/ + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + nginx \ + ca-certificates \ + libjemalloc2 \ + git +msg_ok "Installed Dependencies" + +setup_mariadb +MARIADB_DB_NAME="ghost" MARIADB_DB_USER="ghostuser" setup_mariadb_db +NODE_VERSION="22" setup_nodejs + +msg_info "Installing Ghost CLI" +$STD npm install ghost-cli@latest -g +msg_ok "Installed Ghost CLI" + +msg_info "Creating Service" +$STD adduser --disabled-password --gecos "Ghost user" ghost-user +$STD usermod -aG sudo ghost-user +echo "ghost-user ALL=(ALL) NOPASSWD:ALL" | tee /etc/sudoers.d/ghost-user +mkdir -p /var/www/ghost +chown -R ghost-user:ghost-user /var/www/ghost +chmod 775 /var/www/ghost +$STD sudo -u ghost-user -H sh -c "cd /var/www/ghost && ghost install --db=mysql --dbhost=localhost --dbuser=$MARIADB_DB_USER --dbpass=$MARIADB_DB_PASS --dbname=$MARIADB_DB_NAME --url=http://localhost:2368 --no-prompt --no-setup-nginx --no-setup-ssl --no-setup-mysql --enable --start --ip 0.0.0.0" +rm /etc/sudoers.d/ghost-user +msg_ok "Creating Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/immich-install.sh b/install/immich-install.sh new file mode 100644 index 000000000..d0625e4cf --- /dev/null +++ b/install/immich-install.sh @@ -0,0 +1,488 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: vhsdream +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://immich.app + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +echo "" +echo "" +echo -e "🤖 ${BL}Immich Machine Learning Options${CL}" +echo "─────────────────────────────────────────" +echo "Please choose your machine-learning type:" +echo "" +echo " 1) CPU only (default)" +echo " 2) Intel OpenVINO (requires GPU passthrough)" +echo "" + +read -r -p "${TAB3}Select machine-learning type [1]: " ML_TYPE +ML_TYPE="${ML_TYPE:-1}" +if [[ "$ML_TYPE" == "2" ]]; then + msg_info "Installing OpenVINO dependencies" + touch ~/.openvino + $STD apt install -y --no-install-recommends patchelf + tmp_dir=$(mktemp -d) + $STD pushd "$tmp_dir" + curl -fsSLO https://raw.githubusercontent.com/immich-app/base-images/refs/heads/main/server/Dockerfile + readarray -t INTEL_URLS < <( + sed -n "/intel-[igc|opencl]/p" ./Dockerfile | awk '{print $2}' + sed -n "/libigdgmm12/p" ./Dockerfile | awk '{print $3}' + ) + for url in "${INTEL_URLS[@]}"; do + curl -fsSLO "$url" + done + $STD apt install -y ./libigdgmm12*.deb + rm ./libigdgmm12*.deb + $STD apt install -y ./*.deb + $STD apt-mark hold libigdgmm12 + $STD popd + rm -rf "$tmp_dir" + dpkg-query -W -f='${Version}\n' intel-opencl-icd >~/.intel_version + msg_ok "Installed OpenVINO dependencies" +fi + +setup_uv + +msg_info "Installing dependencies" +$STD apt install --no-install-recommends -y \ + git \ + redis \ + autoconf \ + build-essential \ + python3-dev \ + automake \ + cmake \ + jq \ + libtool \ + libltdl-dev \ + libgdk-pixbuf-2.0-dev \ + libbrotli-dev \ + libexif-dev \ + libexpat1-dev \ + libglib2.0-dev \ + libgsf-1-dev \ + libjpeg62-turbo-dev \ + libspng-dev \ + liblcms2-dev \ + libopenexr-dev \ + libgif-dev \ + librsvg2-dev \ + libexpat1 \ + libgcc-s1 \ + libgomp1 \ + liblqr-1-0 \ + libltdl7 \ + libopenjp2-7 \ + meson \ + ninja-build \ + pkg-config \ + mesa-utils \ + mesa-va-drivers \ + mesa-vulkan-drivers \ + ocl-icd-libopencl1 \ + tini \ + zlib1g \ + libio-compress-brotli-perl \ + libwebp7 \ + libwebpdemux2 \ + libwebpmux3 \ + libhwy1t64 \ + libdav1d-dev \ + libhwy-dev \ + libwebp-dev \ + libaom-dev \ + ccache + +setup_deb822_repo \ + "jellyfin" \ + "https://repo.jellyfin.org/jellyfin_team.gpg.key" \ + "https://repo.jellyfin.org/debian" \ + "$(get_os_info codename)" +$STD apt install -y jellyfin-ffmpeg7 +ln -sf /usr/lib/jellyfin-ffmpeg/ffmpeg /usr/bin/ffmpeg +ln -sf /usr/lib/jellyfin-ffmpeg/ffprobe /usr/bin/ffprobe + +# Set permissions for /dev/dri (only in privileged containers and if /dev/dri exists) +if [[ "$CTTYPE" == "0" && -d /dev/dri ]]; then + chgrp video /dev/dri 2>/dev/null || true + chmod 755 /dev/dri 2>/dev/null || true + chmod 660 /dev/dri/* 2>/dev/null || true + $STD adduser "$(id -u -n)" video 2>/dev/null || true + $STD adduser "$(id -u -n)" render 2>/dev/null || true +fi +msg_ok "Dependencies Installed" + +msg_info "Installing Mise" +curl -fSs https://mise.jdx.dev/gpg-key.pub | tee /etc/apt/keyrings/mise-archive-keyring.pub 1>/dev/null +echo "deb [signed-by=/etc/apt/keyrings/mise-archive-keyring.pub arch=amd64] https://mise.jdx.dev/deb stable main" >/etc/apt/sources.list.d/mise.list +$STD apt update +$STD apt install -y mise +msg_ok "Installed Mise" + +msg_info "Configuring Debian Testing Repo" +sed -i 's/ trixie-updates/ trixie-updates testing/g' /etc/apt/sources.list.d/debian.sources +cat </etc/apt/preferences.d/preferences +Package: * +Pin: release a=unstable +Pin-Priority: 450 + +Package: * +Pin:release a=testing +Pin-Priority: 450 +EOF +$STD apt update +msg_ok "Configured Debian Testing repo" +msg_info "Installing packages from Debian Testing repo" +$STD apt install -t testing --no-install-recommends -yqq libmimalloc3 libde265-dev +msg_ok "Installed packages from Debian Testing repo" + +PNPM_VERSION="$(curl -fsSL "https://raw.githubusercontent.com/immich-app/immich/refs/heads/main/package.json" | jq -r '.packageManager | split("@")[1]')" +NODE_VERSION="24" NODE_MODULE="pnpm@${PNPM_VERSION}" setup_nodejs +PG_VERSION="16" PG_MODULES="pgvector" setup_postgresql + +VCHORD_RELEASE="0.5.3" +msg_info "Installing Vectorchord v${VCHORD_RELEASE}" +curl -fsSL "https://github.com/tensorchord/VectorChord/releases/download/${VCHORD_RELEASE}/postgresql-16-vchord_${VCHORD_RELEASE}-1_amd64.deb" -o vchord.deb +$STD apt install -y ./vchord.deb +rm vchord.deb +echo "$VCHORD_RELEASE" >~/.vchord_version +msg_ok "Installed Vectorchord v${VCHORD_RELEASE}" + +sed -i -e "/^#shared_preload/s/^#//;/^shared_preload/s/''/'vchord.so'/" /etc/postgresql/16/main/postgresql.conf +systemctl restart postgresql.service +PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_GRANT_SUPERUSER="true" PG_DB_SKIP_ALTER_ROLE="true" setup_postgresql_db + +msg_info "Compiling Custom Photo-processing Library (extreme patience)" +LD_LIBRARY_PATH=/usr/local/lib +export LD_RUN_PATH=/usr/local/lib +STAGING_DIR=/opt/staging +BASE_REPO="https://github.com/immich-app/base-images" +BASE_DIR=${STAGING_DIR}/base-images +SOURCE_DIR=${STAGING_DIR}/image-source +$STD git clone -b main "$BASE_REPO" "$BASE_DIR" +mkdir -p "$SOURCE_DIR" + +msg_info "(1/5) Compiling libjxl" +cd "$STAGING_DIR" +SOURCE=${SOURCE_DIR}/libjxl +JPEGLI_LIBJPEG_LIBRARY_SOVERSION="62" +JPEGLI_LIBJPEG_LIBRARY_VERSION="62.3.0" +: "${LIBJXL_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libjxl.json)}" +$STD git clone https://github.com/libjxl/libjxl.git "$SOURCE" +cd "$SOURCE" +$STD git reset --hard "$LIBJXL_REVISION" +$STD git submodule update --init --recursive --depth 1 --recommend-shallow +$STD git apply "$BASE_DIR"/server/sources/libjxl-patches/jpegli-empty-dht-marker.patch +$STD git apply "$BASE_DIR"/server/sources/libjxl-patches/jpegli-icc-warning.patch +mkdir build +cd build +$STD cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DJPEGXL_ENABLE_DOXYGEN=OFF \ + -DJPEGXL_ENABLE_MANPAGES=OFF \ + -DJPEGXL_ENABLE_PLUGIN_GIMP210=OFF \ + -DJPEGXL_ENABLE_BENCHMARK=OFF \ + -DJPEGXL_ENABLE_EXAMPLES=OFF \ + -DJPEGXL_FORCE_SYSTEM_BROTLI=ON \ + -DJPEGXL_FORCE_SYSTEM_HWY=ON \ + -DJPEGXL_ENABLE_JPEGLI=ON \ + -DJPEGXL_ENABLE_JPEGLI_LIBJPEG=ON \ + -DJPEGXL_INSTALL_JPEGLI_LIBJPEG=ON \ + -DJPEGXL_ENABLE_PLUGINS=ON \ + -DJPEGLI_LIBJPEG_LIBRARY_SOVERSION="$JPEGLI_LIBJPEG_LIBRARY_SOVERSION" \ + -DJPEGLI_LIBJPEG_LIBRARY_VERSION="$JPEGLI_LIBJPEG_LIBRARY_VERSION" \ + -DLIBJPEG_TURBO_VERSION_NUMBER=2001005 \ + .. +$STD cmake --build . -- -j"$(nproc)" +$STD cmake --install . +ldconfig /usr/local/lib +$STD make clean +cd "$STAGING_DIR" +rm -rf "$SOURCE"/{build,third_party} +msg_ok "(1/5) Compiled libjxl" + +msg_info "(2/5) Compiling libheif" +SOURCE=${SOURCE_DIR}/libheif +: "${LIBHEIF_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libheif.json)}" +$STD git clone https://github.com/strukturag/libheif.git "$SOURCE" +cd "$SOURCE" +$STD git reset --hard "$LIBHEIF_REVISION" +mkdir build +cd build +$STD cmake --preset=release-noplugins \ + -DWITH_DAV1D=ON \ + -DENABLE_PARALLEL_TILE_DECODING=ON \ + -DWITH_LIBSHARPYUV=ON \ + -DWITH_LIBDE265=ON \ + -DWITH_AOM_DECODER=OFF \ + -DWITH_AOM_ENCODER=ON \ + -DWITH_X265=OFF \ + -DWITH_EXAMPLES=OFF \ + .. +$STD make install -j "$(nproc)" +ldconfig /usr/local/lib +$STD make clean +cd "$STAGING_DIR" +rm -rf "$SOURCE"/build +msg_ok "(2/5) Compiled libheif" + +msg_info "(3/5) Compiling libraw" +SOURCE=${SOURCE_DIR}/libraw +: "${LIBRAW_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libraw.json)}" +$STD git clone https://github.com/libraw/libraw.git "$SOURCE" +cd "$SOURCE" +$STD git reset --hard "$LIBRAW_REVISION" +$STD autoreconf --install +$STD ./configure --disable-examples +$STD make -j"$(nproc)" +$STD make install +ldconfig /usr/local/lib +$STD make clean +cd "$STAGING_DIR" +msg_ok "(3/5) Compiled libraw" + +msg_info "(4/5) Compiling imagemagick" +SOURCE=$SOURCE_DIR/imagemagick +: "${IMAGEMAGICK_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/imagemagick.json)}" +$STD git clone https://github.com/ImageMagick/ImageMagick.git "$SOURCE" +cd "$SOURCE" +$STD git reset --hard "$IMAGEMAGICK_REVISION" +$STD ./configure --with-modules CPPFLAGS="-DMAGICK_LIBRAW_VERSION_TAIL=202502" +$STD make -j"$(nproc)" +$STD make install +ldconfig /usr/local/lib +$STD make clean +cd "$STAGING_DIR" +msg_ok "(4/5) Compiled imagemagick" + +msg_info "(5/5) Compiling libvips" +SOURCE=$SOURCE_DIR/libvips +: "${LIBVIPS_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libvips.json)}" +$STD git clone https://github.com/libvips/libvips.git "$SOURCE" +cd "$SOURCE" +$STD git reset --hard "$LIBVIPS_REVISION" +$STD meson setup build --buildtype=release --libdir=lib -Dintrospection=disabled -Dtiff=disabled +cd build +$STD ninja install +ldconfig /usr/local/lib +cd "$STAGING_DIR" +rm -rf "$SOURCE"/build +msg_ok "(5/5) Compiled libvips" +{ + echo "imagemagick: $IMAGEMAGICK_REVISION" + echo "libheif: $LIBHEIF_REVISION" + echo "libjxl: $LIBJXL_REVISION" + echo "libraw: $LIBRAW_REVISION" + echo "libvips: $LIBVIPS_REVISION" +} >~/.immich_library_revisions +msg_ok "Custom Photo-processing Libraries Compiled Successfully" + +INSTALL_DIR="/opt/${APPLICATION}" +UPLOAD_DIR="${INSTALL_DIR}/upload" +SRC_DIR="${INSTALL_DIR}/source" +APP_DIR="${INSTALL_DIR}/app" +PLUGIN_DIR="${APP_DIR}/corePlugin" +ML_DIR="${APP_DIR}/machine-learning" +GEO_DIR="${INSTALL_DIR}/geodata" +mkdir -p "$INSTALL_DIR" +mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache} + +fetch_and_deploy_gh_release "immich" "immich-app/immich" "tag" "v2.5.2" "$SRC_DIR" + +msg_info "Installing Immich (patience)" + +cd "$SRC_DIR"/server +export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +export CI=1 +corepack enable + +# server build +export SHARP_IGNORE_GLOBAL_LIBVIPS=true +$STD pnpm --filter immich --frozen-lockfile build +unset SHARP_IGNORE_GLOBAL_LIBVIPS +export SHARP_FORCE_GLOBAL_LIBVIPS=true +$STD pnpm --filter immich --frozen-lockfile --prod --no-optional deploy "$APP_DIR" +cp "$APP_DIR"/package.json "$APP_DIR"/bin +sed -i 's|^start|./start|' "$APP_DIR"/bin/immich-admin + +# openapi & web build +cd "$SRC_DIR" +echo "packageImportMethod: hardlink" >>./pnpm-workspace.yaml +$STD pnpm --filter @immich/sdk --filter immich-web --frozen-lockfile --force install +unset SHARP_FORCE_GLOBAL_LIBVIPS +export SHARP_IGNORE_GLOBAL_LIBVIPS=true +$STD pnpm --filter @immich/sdk --filter immich-web build +cp -a web/build "$APP_DIR"/www +cp LICENSE "$APP_DIR" + +# cli build +$STD pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install +$STD pnpm --filter @immich/sdk --filter @immich/cli build +$STD pnpm --filter @immich/cli --prod --no-optional deploy "$APP_DIR"/cli + +# plugins +cd "$SRC_DIR" +$STD mise trust --ignore ./mise.toml +$STD mise trust ./plugins/mise.toml +cd plugins +$STD mise install +$STD mise run build +mkdir -p "$PLUGIN_DIR" +cp -r ./dist "$PLUGIN_DIR"/dist +cp ./manifest.json "$PLUGIN_DIR" +msg_ok "Installed Immich Server, Web and Plugin Components" + +cd "$SRC_DIR"/machine-learning +$STD useradd -U -s /usr/sbin/nologin -r -M -d "$INSTALL_DIR" immich +mkdir -p "$ML_DIR" && chown -R immich:immich "$INSTALL_DIR" +export VIRTUAL_ENV="${ML_DIR}/ml-venv" +if [[ -f ~/.openvino ]]; then + msg_info "Installing HW-accelerated machine-learning" + $STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p python3.13 --managed-python + patchelf --clear-execstack "${VIRTUAL_ENV}/lib/python3.13/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-313-x86_64-linux-gnu.so" + msg_ok "Installed HW-accelerated machine-learning" +else + msg_info "Installing machine-learning" + $STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p python3.11 --managed-python + msg_ok "Installed machine-learning" +fi +cd "$SRC_DIR" +cp -a machine-learning/{ann,immich_ml} "$ML_DIR" +if [[ -f ~/.openvino ]]; then + sed -i "/intra_op/s/int = 0/int = os.cpu_count() or 0/" "$ML_DIR"/immich_ml/config.py +fi +ln -sf "$APP_DIR"/resources "$INSTALL_DIR" + +cd "$APP_DIR" +grep -rl /usr/src | xargs -n1 sed -i "s|\/usr/src|$INSTALL_DIR|g" +grep -rlE "'/build'" | xargs -n1 sed -i "s|'/build'|'$APP_DIR'|g" +sed -i "s@\"/cache\"@\"$INSTALL_DIR/cache\"@g" "$ML_DIR"/immich_ml/config.py +ln -s "$UPLOAD_DIR" "$APP_DIR"/upload +ln -s "$UPLOAD_DIR" "$ML_DIR"/upload + +msg_info "Installing GeoNames data" +cd "$GEO_DIR" +curl -fsSLZ -O "https://download.geonames.org/export/dump/admin1CodesASCII.txt" \ + -O "https://download.geonames.org/export/dump/admin2Codes.txt" \ + -O "https://download.geonames.org/export/dump/cities500.zip" \ + -O "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson" +unzip -q cities500.zip +date --iso-8601=seconds | tr -d "\n" >geodata-date.txt +rm cities500.zip +cd "$INSTALL_DIR" +ln -s "$GEO_DIR" "$APP_DIR" +msg_ok "Installed GeoNames data" + +mkdir -p /var/log/immich +touch /var/log/immich/{web.log,ml.log} +msg_ok "Installed Immich" + +msg_info "Modifying user, creating env file, scripts & services" +usermod -aG video,render immich + +cat <"${INSTALL_DIR}"/.env +TZ=$(cat /etc/timezone) +IMMICH_VERSION=release +NODE_ENV=production +IMMICH_ALLOW_SETUP=true + +DB_HOSTNAME=127.0.0.1 +DB_USERNAME=${PG_DB_USER} +DB_PASSWORD=${PG_DB_PASS} +DB_DATABASE_NAME=${PG_DB_NAME} +DB_VECTOR_EXTENSION=vectorchord + +REDIS_HOSTNAME=127.0.0.1 +IMMICH_MACHINE_LEARNING_URL=http://127.0.0.1:3003 +MACHINE_LEARNING_CACHE_FOLDER=${INSTALL_DIR}/cache +## - For OpenVINO only - workaround for onnxruntime-openvino 1.23.x crash +## - See: https://github.com/immich-app/immich/pull/11240 +MACHINE_LEARNING_OPENVINO_NUM_THREADS=$(nproc) +## - Uncomment below to increase inference speed while reducing accuracy +# MACHINE_LEARNING_OPENVINO_PRECISION=FP16 + +IMMICH_MEDIA_LOCATION=${UPLOAD_DIR} +EOF +cat <"${ML_DIR}"/ml_start.sh +#!/usr/bin/env bash + +cd ${ML_DIR} +. ${VIRTUAL_ENV}/bin/activate + +set -a +. ${INSTALL_DIR}/.env +set +a + +python3 -m immich_ml +EOF +cat <"$APP_DIR"/bin/start.sh +#!/usr/bin/env bash + +set -a +. ${INSTALL_DIR}/.env +set +a + +/usr/bin/node ${APP_DIR}/dist/main.js "\$@" +EOF +chmod +x "$ML_DIR"/ml_start.sh "$APP_DIR"/bin/start.sh +cat </etc/systemd/system/"${APPLICATION}"-web.service +[Unit] +Description=${APPLICATION} Web Service +After=network.target +Requires=redis-server.service +Requires=postgresql.service +Requires=immich-ml.service + +[Service] +Type=simple +User=immich +Group=immich +UMask=0077 +WorkingDirectory=${APP_DIR} +EnvironmentFile=${INSTALL_DIR}/.env +ExecStart=/usr/bin/node ${APP_DIR}/dist/main +Restart=on-failure +SyslogIdentifier=immich-web +StandardOutput=append:/var/log/immich/web.log +StandardError=append:/var/log/immich/web.log + +[Install] +WantedBy=multi-user.target +EOF +cat </etc/systemd/system/"${APPLICATION}"-ml.service +[Unit] +Description=${APPLICATION} Machine-Learning +After=network.target + +[Service] +Type=simple +UMask=0077 +User=immich +Group=immich +WorkingDirectory=${APP_DIR} +EnvironmentFile=${INSTALL_DIR}/.env +ExecStart=${ML_DIR}/ml_start.sh +Restart=on-failure +SyslogIdentifier=immich-machine-learning +StandardOutput=append:/var/log/immich/ml.log +StandardError=append:/var/log/immich/ml.log + +[Install] +WantedBy=multi-user.target +EOF +chown -R immich:immich "$INSTALL_DIR" /var/log/immich +systemctl enable -q --now "$APPLICATION"-ml.service "$APPLICATION"-web.service +msg_ok "Modified user, created env file, scripts and services" + +motd_ssh +customize +cleanup_lxc diff --git a/install/isponsorblocktv-install.sh b/install/isponsorblocktv-install.sh new file mode 100644 index 000000000..8cce9c019 --- /dev/null +++ b/install/isponsorblocktv-install.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Matthew Stern (sternma) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/dmunozv04/iSponsorBlockTV + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +INSTALL_DIR="/opt/isponsorblocktv" +DATA_DIR="/var/lib/isponsorblocktv" + +msg_info "Installing Dependencies" +$STD apt install -y \ + python3 \ + python3-venv \ + python3-pip +msg_ok "Installed Dependencies" + +fetch_and_deploy_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV" + +msg_info "Setting up iSponsorBlockTV" +$STD python3 -m venv "$INSTALL_DIR/venv" +$STD "$INSTALL_DIR/venv/bin/pip" install --upgrade pip +$STD "$INSTALL_DIR/venv/bin/pip" install "$INSTALL_DIR" +msg_ok "Set up iSponsorBlockTV" + +msg_info "Creating data directory" +install -d "$DATA_DIR" +msg_ok "Created data directory" + +msg_info "Creating Service" +cat </etc/systemd/system/isponsorblocktv.service +[Unit] +Description=iSponsorBlockTV +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +Group=root +WorkingDirectory=$INSTALL_DIR +Environment=iSPBTV_data_dir=$DATA_DIR +ExecStart=$INSTALL_DIR/venv/bin/iSponsorBlockTV +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOT +systemctl enable -q --now isponsorblocktv +msg_ok "Created Service" + +msg_info "Creating CLI wrapper" +install -d /usr/local/bin +cat <<'EOT' >/usr/local/bin/iSponsorBlockTV +#!/usr/bin/env bash +export iSPBTV_data_dir="/var/lib/isponsorblocktv" + +set +e +/opt/isponsorblocktv/venv/bin/iSponsorBlockTV "$@" +status=$? +set -e + +case "${1:-}" in + setup|setup-cli) + systemctl restart isponsorblocktv >/dev/null 2>&1 || true + ;; +esac + +exit $status +EOT +chmod +x /usr/local/bin/iSponsorBlockTV +ln -sf /usr/local/bin/iSponsorBlockTV /usr/bin/iSponsorBlockTV +msg_ok "Created CLI wrapper" + +msg_info "Setting default data dir for shells" +cat <<'EOT' >/etc/profile.d/isponsorblocktv.sh +export iSPBTV_data_dir="/var/lib/isponsorblocktv" +EOT +if ! grep -q '^iSPBTV_data_dir=' /etc/environment 2>/dev/null; then + cat <<'EOT' >>/etc/environment +iSPBTV_data_dir=/var/lib/isponsorblocktv +EOT +fi +msg_ok "Set default data dir for shells" + +motd_ssh +customize +cleanup_lxc diff --git a/install/jotty-install.sh b/install/jotty-install.sh index a8ce9f634..e6b88fd59 100644 --- a/install/jotty-install.sh +++ b/install/jotty-install.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG -# Author: vhsdream +# Author: vhsdream | MickLesk # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # Source: https://github.com/fccview/jotty @@ -14,37 +14,16 @@ network_check update_os NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs -#fetch_and_deploy_gh_release "jotty" "fccview/jotty" "tarball" "latest" "/opt/jotty" +fetch_and_deploy_gh_release "jotty" "fccview/jotty" "prebuild" "latest" "/opt/jotty" "jotty_*_prebuild.tar.gz" + msg_info "Setup jotty" -mkdir -p /opt/jotty -wget -q https://github.com/fccview/jotty/releases/download/develop/jotty-prebuild-develop.tar.gz -O /opt/jotty.tar.gz -cd /opt -tar -xzf jotty.tar.gz - -cd /opt/jotty -# unset NODE_OPTIONS -# export NODE_OPTIONS="--max-old-space-size=3072" -# # $STD yarn --frozen-lockfiled -# # $STD yarn next telemetry disable -# # $STD yarn build - -# [ -d "public" ] && cp -r public .next/standalone/ -# [ -d "howto" ] && cp -r howto .next/standalone/ -# mkdir -p .next/standalone/.next -# cp -r .next/static .next/standalone/.next/ - -# mv .next/standalone /tmp/jotty_standalone -# rm -rf ./* .next .git .gitignore .yarn -# mv /tmp/jotty_standalone/* . -# mv /tmp/jotty_standalone/.[!.]* . 2>/dev/null || true -# rm -rf /tmp/jotty_standalone - mkdir -p data/{users,checklists,notes} cat </opt/jotty/.env NODE_ENV=production - # --- Uncomment to enable +# APP_URL=https://your-jotty-domain.com +# INTERNAL_API_URL=http://localhost:3000 # HTTPS=true # SERVE_PUBLIC_IMAGES=yes # SERVE_PUBLIC_FILES=yes @@ -57,12 +36,11 @@ NODE_ENV=production # SSO_MODE=oidc # OIDC_ISSUER= # OIDC_CLIENT_ID= -# APP_URL= # SSO_FALLBACK_LOCAL=yes # OIDC_CLIENT_SECRET=your_client_secret # OIDC_ADMIN_GROUPS=admins EOF -msg_ok "Installed ${APPLICATION}" +msg_ok "Setup jotty" msg_info "Creating Service" cat </etc/systemd/system/jotty.service diff --git a/install/kitchenowl-install.sh b/install/kitchenowl-install.sh deleted file mode 100644 index eb90ba9a8..000000000 --- a/install/kitchenowl-install.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2025 community-scripts ORG -# Author: snazzybean -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Source: https://github.com/TomBursch/kitchenowl - -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 \ - build-essential \ - libpq-dev \ - libffi-dev \ - libssl-dev -msg_ok "Installed Dependencies" - -PYTHON_VERSION="3.14" setup_uv -import_local_ip -fetch_and_deploy_gh_release "kitchenowl" "TomBursch/kitchenowl" "tarball" "latest" "/opt/kitchenowl" -rm -rf /opt/kitchenowl/web -fetch_and_deploy_gh_release "kitchenowl-web" "TomBursch/kitchenowl" "prebuild" "latest" "/opt/kitchenowl/web" "kitchenowl_Web.tar.gz" - -msg_info "Setting up KitchenOwl" -cd /opt/kitchenowl/backend -#rm -f uv.lock -$STD uv sync --frozen -sed -i 's/default=True/default=False/' /opt/kitchenowl/backend/wsgi.py -mkdir -p /nltk_data -$STD uv run python -m nltk.downloader -d /nltk_data averaged_perceptron_tagger_eng punkt_tab -JWT_SECRET=$(openssl rand -hex 32) -mkdir -p /opt/kitchenowl/data -cat </opt/kitchenowl/kitchenowl.env -STORAGE_PATH=/opt/kitchenowl/data -JWT_SECRET_KEY=${JWT_SECRET} -NLTK_DATA=/nltk_data -FRONT_URL=http://${LOCAL_IP} -FLASK_APP=wsgi.py -FLASK_ENV=production -EOF -set -a -source /opt/kitchenowl/kitchenowl.env -set +a -$STD uv run flask db upgrade -msg_ok "Set up KitchenOwl" - -msg_info "Creating Systemd Service" -cat </etc/systemd/system/kitchenowl.service -[Unit] -Description=KitchenOwl Backend -After=network.target - -[Service] -Type=simple -User=root -WorkingDirectory=/opt/kitchenowl/backend -EnvironmentFile=/opt/kitchenowl/kitchenowl.env -ExecStart=/usr/local/bin/uv run wsgi.py -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF -systemctl enable -q --now kitchenowl -msg_ok "Created and Started Service" - -msg_info "Configuring Nginx" -rm -f /etc/nginx/sites-enabled/default -cat <<'EOF' >/etc/nginx/sites-available/kitchenowl.conf -server { - listen 80; - server_name _; - - root /opt/kitchenowl/web; - index index.html; - - client_max_body_size 100M; - - # Security Headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - - location / { - try_files $uri $uri/ /index.html; - } - - location /api { - proxy_pass http://127.0.0.1:5000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location /socket.io { - proxy_pass http://127.0.0.1:5000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - # WebSocket Timeouts - allow long-lived connections - proxy_read_timeout 86400s; - proxy_send_timeout 86400s; - } -} -EOF -ln -sf /etc/nginx/sites-available/kitchenowl.conf /etc/nginx/sites-enabled/ -rm -f /etc/nginx/sites-enabled/default -$STD systemctl reload nginx -msg_ok "Configured Nginx" - -motd_ssh -customize -cleanup_lxc diff --git a/install/languagetool-install.sh b/install/languagetool-install.sh deleted file mode 100644 index 6a118e85f..000000000 --- a/install/languagetool-install.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: Slaviša Arežina (tremor021) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://languagetool.org/ - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -msg_info "Installing dependencies" -$STD apt install -y fasttext -msg_ok "Installed dependencies" - -JAVA_VERSION="21" setup_java - -msg_info "Setting up LanguageTool" -RELEASE=$(curl -fsSL https://languagetool.org/download/ | grep -oP 'LanguageTool-\K[0-9]+\.[0-9]+(\.[0-9]+)?(?=\.zip)' | sort -V | tail -n1) -download_file "https://languagetool.org/download/LanguageTool-stable.zip" /tmp/LanguageTool-stable.zip -unzip -q /tmp/LanguageTool-stable.zip -d /opt -mv /opt/LanguageTool-*/ /opt/LanguageTool/ -download_file "https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin" /opt/lid.176.bin - -read -r -p "${TAB3}Enter language code (en, de, es, fr, nl) to download ngrams or press ENTER to skip: " lang_code -ngram_dir="" -if [[ -n "$lang_code" ]]; then - if [[ "$lang_code" =~ ^(en|de|es|fr|nl)$ ]]; then - msg_info "Searching for $lang_code ngrams..." - filename=$(curl -fsSL https://languagetool.org/download/ngram-data/ | grep -oP "ngrams-${lang_code}-[0-9]+\.zip" | sort -uV | tail -n1) - - if [[ -n "$filename" ]]; then - msg_info "Downloading $filename" - download_file "https://languagetool.org/download/ngram-data/${filename}" "/tmp/${filename}" - - mkdir -p /opt/ngrams - msg_info "Extracting $lang_code ngrams to /opt/ngrams" - unzip -q "/tmp/${filename}" -d /opt/ngrams - rm "/tmp/${filename}" - - ngram_dir="/opt/ngrams" - msg_ok "Installed $lang_code ngrams" - else - msg_info "No ngram file found for ${lang_code}" - fi - else - msg_error "Invalid language code: $lang_code" - fi -fi - -cat </opt/LanguageTool/server.properties -fasttextModel=/opt/lid.176.bin -fasttextBinary=/usr/bin/fasttext -EOF -if [[ -n "$ngram_dir" ]]; then - echo "languageModel=/opt/ngrams" >> /opt/LanguageTool/server.properties -fi -echo "${RELEASE}" >~/.languagetool -msg_ok "Setup LanguageTool" - -msg_info "Creating Service" -cat <<'EOF' >/etc/systemd/system/language-tool.service -[Unit] -Description=LanguageTool Service -After=network.target - -[Service] -WorkingDirectory=/opt/LanguageTool -ExecStart=java -cp languagetool-server.jar org.languagetool.server.HTTPServer --config server.properties --public --allow-origin "*" -Restart=always - -[Install] -WantedBy=multi-user.target -EOF -systemctl enable -q --now language-tool -msg_ok "Created Service" - -motd_ssh -customize -cleanup_lxc diff --git a/install/linkwarden-install.sh b/install/linkwarden-install.sh index 8f4b569a5..3808c8426 100644 --- a/install/linkwarden-install.sh +++ b/install/linkwarden-install.sh @@ -22,7 +22,7 @@ PG_VERSION="16" setup_postgresql PG_DB_NAME="linkwardendb" PG_DB_USER="linkwarden" setup_postgresql_db RUST_CRATES="monolith" setup_rust fetch_and_deploy_gh_release "linkwarden" "linkwarden/linkwarden" -import_local_ip + read -r -p "${TAB3}Would you like to add Adminer? " prompt if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then diff --git a/install/minthcm-install.sh b/install/minthcm-install.sh index 090a31135..817c99e5b 100644 --- a/install/minthcm-install.sh +++ b/install/minthcm-install.sh @@ -12,14 +12,12 @@ catch_errors setting_up_container network_check update_os -PHP_VERSION="8.2" -PHP_APACHE="YES" PHP_MODULE="mysql,cli,redis" PHP_FPM="YES" setup_php -setup_composer -msg_info "Enabling Apache modules (rewrite, headers)" -$STD a2enmod rewrite -$STD a2enmod headers -msg_ok "Enabled Apache modules (rewrite, headers)" +PHP_VERSION="8.2" +PHP_APACHE="YES" PHP_MODULE="mysql,redis" PHP_FPM="YES" setup_php +setup_composer +setup_mariadb +$STD mariadb -u root -e "SET GLOBAL sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"; fetch_and_deploy_gh_release "MintHCM" "minthcm/minthcm" "tarball" "latest" "/var/www/MintHCM" @@ -37,19 +35,17 @@ mkdir -p /var/www/script cp /var/www/MintHCM/docker/script/generate_config.php /var/www/script/generate_config.php cp /var/www/MintHCM/docker/.env /var/www/script/.env chown -R www-data:www-data /var/www/script -msg_ok "Configured MintHCM" - -msg_info "Restarting Apache2" +$STD a2enmod rewrite +$STD a2enmod headers $STD systemctl restart apache2 -msg_ok "Restarted Apache2" +msg_ok "Configured MintHCM" msg_info "Setting up Elasticsearch" setup_deb822_repo \ "elasticsearch" \ "https://artifacts.elastic.co/GPG-KEY-elasticsearch" \ "https://artifacts.elastic.co/packages/7.x/apt" \ - "stable" \ - "main" + "stable" $STD apt install -y elasticsearch echo "-Xms2g" >>/etc/elasticsearch/jvm.options echo "-Xmx2g" >>/etc/elasticsearch/jvm.options @@ -57,35 +53,26 @@ $STD /usr/share/elasticsearch/bin/elasticsearch-plugin install ingest-attachment systemctl enable -q --now elasticsearch msg_ok "Set up Elasticsearch" -setup_mariadb -msg_info "Setting up MariaDB" -$STD mariadb -u root -e "SET GLOBAL sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"; -msg_ok "Set up MariaDB" - msg_info "Configuring Database" DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13) $STD mariadb -u root -e "CREATE USER 'minthcm'@'localhost' IDENTIFIED BY '${DB_PASS}';" $STD mariadb -u root -e "GRANT ALL ON *.* TO 'minthcm'@'localhost'; FLUSH PRIVILEGES;" -sed -i 's/^DB_HOST=.*/DB_HOST=localhost/' /var/www/script/.env -sed -i 's/^DB_USER=.*/DB_USER=minthcm/' /var/www/script/.env -sed -i "s/^DB_PASS=.*/DB_PASS=${DB_PASS}/" /var/www/script/.env -sed -i 's/^ELASTICSEARCH_HOST=.*/ELASTICSEARCH_HOST=localhost/' /var/www/script/.env -msg_ok "Configured MariaDB" -{ - echo "MintHCM DB Credentials" - echo "MariaDB User: minthcm" - echo "MariaDB Password: $DB_PASS" -} >>~/minthcm.creds +sed -i "s/^DB_HOST=.*/DB_HOST=localhost/" /var/www/script/.env +sed -i "s/^DB_USER=.*/DB_USER=minthcm/" /var/www/script/.env +sed -i "s/^DB_PASS=.*/DB_PASS=$DB_PASS/" /var/www/script/.env +sed -i "s/^ELASTICSEARCH_HOST=.*/ELASTICSEARCH_HOST=localhost/" /var/www/script/.env +msg_ok "Configured Database" msg_info "Generating configuration file" set -a source /var/www/script/.env set +a -php /var/www/script/generate_config.php +$STD php /var/www/script/generate_config.php msg_ok "Generated configuration file" msg_info "Installing MintHCM" -cd /var/www/MintHCM && su -s /bin/bash -c 'php /var/www/MintHCM/MintCLI install < /var/www/MintHCM/configMint4' www-data +cd /var/www/MintHCM +$STD sudo -u www-data php MintCLI install < /var/www/MintHCM/configMint4 printf "* * * * * cd /var/www/MintHCM/legacy; php -f cron.php > /dev/null 2>&1\n" > /var/spool/cron/crontabs/www-data service cron start rm -f /var/www/MintHCM/configMint4 diff --git a/install/nodecast-tv-install.sh b/install/nodecast-tv-install.sh deleted file mode 100644 index a9d2e3a7b..000000000 --- a/install/nodecast-tv-install.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: luismco -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/technomancer702/nodecast-tv - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -fetch_and_deploy_gh_release "nodecast-tv" "technomancer702/nodecast-tv" -setup_nodejs - -msg_info "Installing Modules" -cd /opt/nodecast-tv -$STD npm install -msg_ok "Installed Modules" - -msg_info "Creating Service" -cat </etc/systemd/system/nodecast-tv.service -[Unit] -Description=nodecast-tv -After=network.target -Wants=network.target - -[Service] -Type=simple -WorkingDirectory=/opt/nodecast-tv -ExecStart=/bin/npm run dev -Restart=on-failure -RestartSec=10 - -[Install] -WantedBy=multi-user.target -EOF -systemctl enable -q --now nodecast-tv -msg_ok "Created Service" - -motd_ssh -customize -cleanup_lxc diff --git a/install/opencloud-install.sh b/install/opencloud-install.sh index 9159b42c5..7d4c9a060 100644 --- a/install/opencloud-install.sh +++ b/install/opencloud-install.sh @@ -57,7 +57,7 @@ echo "$COOLPASS" >~/.coolpass msg_ok "Installed Collabora Online" # OpenCloud -fetch_and_deploy_gh_release "opencloud" "opencloud-eu/opencloud" "singlefile" "v4.1.0" "/usr/bin" "opencloud-*-linux-amd64" +fetch_and_deploy_gh_release "opencloud" "opencloud-eu/opencloud" "singlefile" "v5.0.1" "/usr/bin" "opencloud-*-linux-amd64" msg_info "Configuring OpenCloud" DATA_DIR="/var/lib/opencloud/" diff --git a/install/papra-install.sh b/install/papra-install.sh index 1f1be0a74..530dbc408 100644 --- a/install/papra-install.sh +++ b/install/papra-install.sh @@ -48,8 +48,10 @@ AUTH_SECRET=$(cat /opt/papra_data/.secret) BETTER_AUTH_SECRET=$(cat /opt/papra_data/.secret) BETTER_AUTH_TELEMETRY=0 CLIENT_BASE_URL=http://${LOCAL_IP}:1221 +SERVER_BASE_URL=http://${LOCAL_IP}:1221 EMAILS_DRY_RUN=true -INGESTION_FOLDER_ROOT=/opt/papra_data/ingestion +INGESTION_FOLDER_IS_ENABLED=true +INGESTION_FOLDER_ROOT_PATH=/opt/papra_data/ingestion EOF msg_ok "Configured Papra" diff --git a/install/piler-install.sh b/install/piler-install.sh index 1eeadff04..2ac56fd1c 100644 --- a/install/piler-install.sh +++ b/install/piler-install.sh @@ -30,7 +30,7 @@ $STD apt install -y \ gnupg msg_ok "Installed Dependencies" -import_local_ip + setup_mariadb MARIADB_DB_NAME="piler" MARIADB_DB_USER="piler" setup_mariadb_db PHP_VERSION="8.3" PHP_FPM="YES" PHP_MODULE="ldap,gd,memcached,pdo,mysql,curl,zip" setup_php @@ -179,8 +179,8 @@ msg_ok "Configured PHP-FPM Pool" msg_info "Configuring Piler Web GUI" # Check if config-site.php already exists (created by .deb package) -if [ ! -f /var/www/piler/config-site.php ]; then - cat </var/www/piler/config-site.php +if [ ! -f /var/piler/www/config-site.php ]; then + cat </var/piler/www/config-site.php /etc/nginx/sites-available/piler server { listen 80; server_name _; - root /var/www/piler; + root /var/piler/www; index index.php; access_log /var/log/nginx/piler-access.log; diff --git a/install/pixelfed-install.sh b/install/pixelfed-install.sh index f2b9ee78e..65a28cb38 100644 --- a/install/pixelfed-install.sh +++ b/install/pixelfed-install.sh @@ -14,14 +14,15 @@ network_check update_os msg_info "Installing Dependencies" -$STD apt-get install -y \ +$STD apt install -y \ nginx \ redis-server \ ffmpeg \ jpegoptim \ optipng \ pngquant \ - gifsicle + gifsicle \ + libvips42 msg_ok "Installed Dependencies" msg_info "Creating Pixelfed User" @@ -29,10 +30,10 @@ useradd -rU -s /bin/bash pixelfed usermod -aG redis pixelfed msg_ok "Created Pixelfed User" -import_local_ip + PG_VERSION="17" setup_postgresql PG_DB_NAME="pixelfed" PG_DB_USER="pixelfed" setup_postgresql_db -PHP_VERSION="8.4" PHP_FPM="YES" PHP_MODULE="bcmath,ctype,exif,imagick,pgsql,redis,tokenizer" PHP_UPLOAD_MAX_FILESIZE="500M" PHP_POST_MAX_SIZE="500M" PHP_MAX_EXECUTION_TIME="600" setup_php +PHP_VERSION="8.4" PHP_FPM="YES" PHP_MODULE="bcmath,ctype,curl,exif,gd,imagick,intl,mbstring,pgsql,redis,xml,zip" PHP_UPLOAD_MAX_FILESIZE="500M" PHP_POST_MAX_SIZE="500M" PHP_MAX_EXECUTION_TIME="600" setup_php setup_composer msg_info "Configuring Redis" @@ -40,7 +41,7 @@ REDIS_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13) sed -i "s/^# requirepass foobared/requirepass $REDIS_PASS/" /etc/redis/redis.conf sed -i "s/^requirepass .*/requirepass $REDIS_PASS/" /etc/redis/redis.conf systemctl restart redis-server -msg_ok "Redis configured" +msg_ok "Configured Redis" msg_info "Configuring PHP-FPM Pool" cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/pixelfed.conf @@ -51,50 +52,95 @@ sed -i 's|^listen = .*|listen = /run/php/php8.4-fpm-pixelfed.sock|' /etc/php/8.4 sed -i 's/^listen.owner = .*/listen.owner = www-data/' /etc/php/8.4/fpm/pool.d/pixelfed.conf sed -i 's/^listen.group = .*/listen.group = www-data/' /etc/php/8.4/fpm/pool.d/pixelfed.conf systemctl restart php8.4-fpm -msg_ok "PHP-FPM Pool configured" +msg_ok "Configured PHP-FPM Pool" fetch_and_deploy_gh_release "pixelfed" "pixelfed/pixelfed" "tarball" "latest" "/opt/pixelfed" msg_info "Installing Pixelfed (Patience)" cd /opt/pixelfed -cp .env.example .env -sed -i "s|APP_URL=.*|APP_URL=http://${LOCAL_IP}|" .env -sed -i "s|APP_DOMAIN=.*|APP_DOMAIN=${LOCAL_IP}|" .env -sed -i "s|ADMIN_DOMAIN=.*|ADMIN_DOMAIN=${LOCAL_IP}|" .env -sed -i "s|SESSION_DOMAIN=.*|SESSION_DOMAIN=${LOCAL_IP}|" .env -sed -i "s|DB_CONNECTION=.*|DB_CONNECTION=pgsql|" .env -sed -i "s|DB_HOST=.*|DB_HOST=127.0.0.1|" .env -sed -i "s|DB_PORT=.*|DB_PORT=5432|" .env -sed -i "s|DB_DATABASE=.*|DB_DATABASE=${PG_DB_NAME}|" .env -sed -i "s|DB_USERNAME=.*|DB_USERNAME=${PG_DB_USER}|" .env -sed -i "s|DB_PASSWORD=.*|DB_PASSWORD=${PG_DB_PASS}|" .env -sed -i "s|REDIS_HOST=.*|REDIS_HOST=127.0.0.1|" .env -sed -i "s|REDIS_PASSWORD=.*|REDIS_PASSWORD=${REDIS_PASS}|" .env -sed -i "s|REDIS_PORT=.*|REDIS_PORT=6379|" .env -sed -i "s|ACTIVITY_PUB=.*|ACTIVITY_PUB=true|" .env -sed -i "s|AP_REMOTE_FOLLOW=.*|AP_REMOTE_FOLLOW=true|" .env -sed -i "s|OAUTH_ENABLED=.*|OAUTH_ENABLED=true|" .env -echo "SESSION_SECURE_COOKIE=false" >>.env +cat </opt/pixelfed/.env +APP_NAME="Pixelfed" +APP_ENV="production" +APP_KEY= +APP_DEBUG="false" +APP_URL=http://${LOCAL_IP} +APP_DOMAIN=${LOCAL_IP} +ADMIN_DOMAIN=${LOCAL_IP} +SESSION_DOMAIN=${LOCAL_IP} +TRUST_PROXIES="*" +FORCE_HTTPS_URLS="false" -chown -R pixelfed:pixelfed /opt/pixelfed +OPEN_REGISTRATION="false" +ENFORCE_EMAIL_VERIFICATION="false" +PF_MAX_USERS="1000" +OAUTH_ENABLED="true" +ENABLE_CONFIG_CACHE="true" +INSTANCE_DISCOVER_PUBLIC="true" + +PF_OPTIMIZE_IMAGES="true" +IMAGE_QUALITY="80" +MAX_PHOTO_SIZE="15000" +MAX_CAPTION_LENGTH="500" +MAX_ALBUM_LENGTH="4" + +DB_CONNECTION="pgsql" +DB_HOST="127.0.0.1" +DB_PORT="5432" +DB_DATABASE="${PG_DB_NAME}" +DB_USERNAME="${PG_DB_USER}" +DB_PASSWORD="${PG_DB_PASS}" + +REDIS_CLIENT="predis" +REDIS_SCHEME="tcp" +REDIS_HOST="127.0.0.1" +REDIS_PASSWORD="${REDIS_PASS}" +REDIS_PORT="6379" + +SESSION_DRIVER="database" +CACHE_DRIVER="redis" +QUEUE_DRIVER="redis" +BROADCAST_DRIVER="log" +LOG_CHANNEL="stack" +HORIZON_PREFIX="horizon-" + +ACTIVITY_PUB="true" +AP_REMOTE_FOLLOW="true" +AP_INBOX="true" +AP_OUTBOX="true" +AP_SHAREDINBOX="true" + +EXP_EMC="true" + +MAIL_DRIVER="log" +MAIL_HOST="smtp.mailtrap.io" +MAIL_PORT="2525" +MAIL_USERNAME="null" +MAIL_PASSWORD="null" +MAIL_ENCRYPTION="null" +MAIL_FROM_ADDRESS="pixelfed@example.com" +MAIL_FROM_NAME="Pixelfed" + +PF_ENABLE_CLOUD="false" +FILESYSTEM_CLOUD="s3" +SESSION_SECURE_COOKIE="false" +HTTPS="false" +EOF chmod -R 755 /opt/pixelfed chmod -R 775 /opt/pixelfed/storage /opt/pixelfed/bootstrap/cache - export COMPOSER_ALLOW_SUPERUSER=1 -cd /opt/pixelfed $STD composer install --no-dev --no-ansi --no-interaction --optimize-autoloader - -sudo -u pixelfed php artisan key:generate -sudo -u pixelfed php artisan storage:link -$STD sudo -u pixelfed php artisan migrate --force -$STD sudo -u pixelfed php artisan import:cities -$STD sudo -u pixelfed php artisan passport:keys -$STD sudo -u pixelfed php artisan route:cache -$STD sudo -u pixelfed php artisan view:cache -$STD sudo -u pixelfed php artisan config:cache -$STD sudo -u pixelfed php artisan instance:actor -$STD sudo -u pixelfed php artisan horizon:install -msg_ok "Pixelfed installed" +$STD php artisan key:generate --force +$STD php artisan storage:link +$STD php artisan migrate --force +$STD php artisan import:cities +$STD php artisan passport:keys +$STD php artisan route:cache +$STD php artisan view:cache +$STD php artisan config:cache +$STD php artisan instance:actor +$STD php artisan horizon:install +chown -R pixelfed:pixelfed /opt/pixelfed +msg_ok "Installed Pixelfed" msg_info "Configuring Nginx" cat <<'EOF' >/etc/nginx/sites-available/pixelfed @@ -136,8 +182,8 @@ EOF ln -sf /etc/nginx/sites-available/pixelfed /etc/nginx/sites-enabled/pixelfed rm -f /etc/nginx/sites-enabled/default $STD nginx -t -systemctl enable -q --now nginx -msg_ok "Nginx configured" +systemctl reload nginx +msg_ok "Configured Nginx" msg_info "Creating Services" cat <<'EOF' >/etc/systemd/system/pixelfed-horizon.service @@ -181,30 +227,9 @@ Persistent=true [Install] WantedBy=timers.target EOF +systemctl enable -q --now pixelfed-horizon pixelfed-scheduler.timer +msg_ok "Created Services" -systemctl daemon-reload -systemctl enable -q --now pixelfed-horizon -systemctl enable -q --now pixelfed-scheduler.timer -msg_ok "Services created" - -msg_info "Saving Credentials" -CREDS_FILE="/root/pixelfed.creds" -{ - echo "Pixelfed Credentials" - echo "" - echo "PostgreSQL" - echo " Database: ${PG_DB_NAME}" - echo " User: ${PG_DB_USER}" - echo " Password: ${PG_DB_PASS}" - echo "" - echo "Redis" - echo " Host: 127.0.0.1:6379" - echo " Password: ${REDIS_PASS}" - echo "" - echo "Web Interface: http://${LOCAL_IP}" - echo "Config: /opt/pixelfed/.env" -} >"$CREDS_FILE" -msg_ok "Credentials saved to ${CREDS_FILE}" motd_ssh customize diff --git a/install/rustypaste-install.sh b/install/rustypaste-install.sh deleted file mode 100644 index 53e2caf57..000000000 --- a/install/rustypaste-install.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: GoldenSpringness | MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/orhun/rustypaste - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -fetch_and_deploy_gh_release "rustypaste" "orhun/rustypaste" "prebuild" "latest" "/opt/rustypaste" "*x86_64-unknown-linux-gnu.tar.gz" -fetch_and_deploy_gh_release "rustypaste-cli" "orhun/rustypaste-cli" "prebuild" "latest" "/usr/local/bin" "*x86_64-unknown-linux-gnu.tar.gz" - -msg_info "Setting up RustyPaste" -cd /opt/rustypaste -sed -i 's|^address = ".*"|address = "0.0.0.0:8000"|' config.toml -msg_ok "Set up RustyPaste" - -msg_info "Creating Service" -cat </etc/systemd/system/rustypaste.service -[Unit] -Description=rustypaste Service -After=network.target - -[Service] -WorkingDirectory=/opt/rustypaste -ExecStart=/opt/rustypaste/rustypaste -Restart=always - -[Install] -WantedBy=multi-user.target -EOF -systemctl enable -q --now rustypaste -msg_ok "Created Service" - -motd_ssh -customize -cleanup_lxc diff --git a/install/shelfmark-install.sh b/install/shelfmark-install.sh deleted file mode 100644 index d38645bae..000000000 --- a/install/shelfmark-install.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: vhsdream -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/calibrain/shelfmark - -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 \ - unrar-free -msg_ok "Installed Dependencies" - -NODE_VERSION="22" setup_nodejs -PYTHON_VERSION="3.12" setup_uv - -fetch_and_deploy_gh_release "shelfmark" "calibrain/shelfmark" "tarball" "latest" "/opt/shelfmark" - -msg_info "Building Shelfmark frontend" -cd /opt/shelfmark/src/frontend -$STD npm ci -$STD npm run build -mv /opt/shelfmark/src/frontend/dist /opt/shelfmark/frontend-dist -msg_ok "Built Shelfmark frontend" - -msg_info "Configuring Shelfmark" -cd /opt/shelfmark -$STD uv venv ./venv -$STD source ./venv/bin/activate -$STD uv pip install -r requirements-base.txt -mkdir -p {/var/log/shelfmark,/tmp/shelfmark,/etc/shelfmark} -cat </etc/shelfmark/.env -DOCKERMODE=false -CONFIG_DIR=/etc/shelfmark -TMP_DIR=/tmp/shelfmark -ENABLE_LOGGING=true -FLASK_HOST=0.0.0.0 -FLASK_PORT=8084 -# SESSION_COOKIES_SECURE=true -# CWA_DB_PATH= -# USE_CF_BYPASS=true -# USING_EXTERNAL_BYPASSER=true -# EXT_BYPASSER_URL= -# EXT_BYPASSER_PATH= -EOF -msg_ok "Configured Shelfmark" - -msg_info "Creating Service and start script" -cat </etc/systemd/system/shelfmark.service -[Unit] -Description=Shelfmark server -After=network.target - -[Service] -Type=simple -WorkingDirectory=/opt/shelfmark -EnvironmentFile=/etc/shelfmark/.env -ExecStart=/usr/bin/bash /opt/shelfmark/start.sh -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target -EOF - -cat </opt/shelfmark/start.sh -#!/usr/bin/env bash - -source /opt/shelfmark/venv/bin/activate -set -a -source /etc/shelfmark/.env -set +a - -gunicorn --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker --workers 1 -t 300 -b 0.0.0.0:8084 shelfmark.main:app -EOF -chmod +x /opt/shelfmark/start.sh - -systemctl enable -q --now shelfmark -msg_ok "Created Services and start script" - -motd_ssh -customize -cleanup_lxc diff --git a/install/sonobarr-install.sh b/install/sonobarr-install.sh new file mode 100644 index 000000000..ee0ab0b36 --- /dev/null +++ b/install/sonobarr-install.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: GoldenSpringness +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/Dodelidoo-Labs/sonobarr + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +fetch_and_deploy_gh_release "sonobarr" "Dodelidoo-Labs/sonobarr" "tarball" +PYTHON_VERSION="3.12" setup_uv + +msg_info "Setting up sonobarr" +source /opt/sonobarr/venv/bin/activate +$STD uv pip install --no-cache-dir -r /opt/sonobarr/requirements.txt +mkdir -p /etc/sonobarr +mv /opt/sonobarr/.sample-env /etc/sonobarr/.env +sed -i "s/^secret_key=.*/secret_key=$(openssl rand -hex 16)/" /etc/sonobarr/.env +sed -i "s/^sonobarr_superadmin_password=.*/sonobarr_superadmin_password=$(openssl rand -hex 16)/" /etc/sonobarr/.env +echo "release_version=$(cat ~/.sonobarr)" >>/etc/sonobarr/.env +echo "sonobarr_config_dir=/etc/sonobarr" >>/etc/sonobarr.env +msg_ok "Set up sonobarr" + +msg_info "Creating Service" +cat </etc/systemd/system/sonobarr.service +[Unit] +Description=sonobarr Service +After=network.target + +[Service] +WorkingDirectory=/opt/sonobarr/src +EnvironmentFile=/etc/sonobarr/.env +Environment="PATH=/opt/sonobarr/venv/bin" +ExecStart=/bin/bash -c 'gunicorn Sonobarr:app -c ../gunicorn_config.py' +Restart=always + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now sonobarr +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/vaultwarden-install.sh b/install/vaultwarden-install.sh new file mode 100644 index 000000000..9440b1163 --- /dev/null +++ b/install/vaultwarden-install.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 tteck +# Author: tteck (tteckster) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://github.com/dani-garcia/vaultwarden + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + build-essential \ + pkgconf \ + libssl-dev \ + libmariadb-dev-compat \ + libpq-dev \ + argon2 \ + ssl-cert +msg_ok "Installed Dependencies" + +setup_rust +fetch_and_deploy_gh_release "vaultwarden" "dani-garcia/vaultwarden" "tarball" "latest" "/tmp/vaultwarden-src" + +msg_info "Building Vaultwarden (Patience)" +cd /tmp/vaultwarden-src +$STD cargo build --features "sqlite,mysql,postgresql" --release +msg_ok "Built Vaultwarden" + +$STD addgroup --system vaultwarden +$STD adduser --system --home /opt/vaultwarden --shell /usr/sbin/nologin --no-create-home --gecos 'vaultwarden' --ingroup vaultwarden --disabled-login --disabled-password vaultwarden +mkdir -p /opt/vaultwarden/{bin,data,web-vault} +cp target/release/vaultwarden /opt/vaultwarden/bin/ +cd ~ && rm -rf /tmp/vaultwarden-src + +fetch_and_deploy_gh_release "vaultwarden_webvault" "dani-garcia/bw_web_builds" "prebuild" "latest" "/opt/vaultwarden/web-vault" "bw_web_*.tar.gz" + +cat </opt/vaultwarden/.env +ADMIN_TOKEN='' +ROCKET_ADDRESS=0.0.0.0 +ROCKET_TLS='{certs="/opt/vaultwarden/ssl-cert-snakeoil.pem",key="/opt/vaultwarden/ssl-cert-snakeoil.key"}' +DATA_FOLDER=/opt/vaultwarden/data +DATABASE_MAX_CONNS=10 +WEB_VAULT_FOLDER=/opt/vaultwarden/web-vault +WEB_VAULT_ENABLED=true +EOF + +mv /etc/ssl/certs/ssl-cert-snakeoil.pem /opt/vaultwarden/ +mv /etc/ssl/private/ssl-cert-snakeoil.key /opt/vaultwarden/ + +msg_info "Creating Service" +chown -R vaultwarden:vaultwarden /opt/vaultwarden/ +chown root:root /opt/vaultwarden/bin/vaultwarden +chmod +x /opt/vaultwarden/bin/vaultwarden +chown -R root:root /opt/vaultwarden/web-vault/ +chmod +r /opt/vaultwarden/.env + +cat <<'EOF' >/etc/systemd/system/vaultwarden.service +[Unit] +Description=Bitwarden Server (Powered by Vaultwarden) +Documentation=https://github.com/dani-garcia/vaultwarden +After=network.target + +[Service] +User=vaultwarden +Group=vaultwarden +EnvironmentFile=-/opt/vaultwarden/.env +ExecStart=/opt/vaultwarden/bin/vaultwarden +LimitNOFILE=65535 +LimitNPROC=4096 +PrivateTmp=true +PrivateDevices=true +ProtectHome=true +ProtectSystem=strict +DevicePolicy=closed +ProtectControlGroups=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +RestrictNamespaces=yes +RestrictRealtime=yes +MemoryDenyWriteExecute=yes +LockPersonality=yes +WorkingDirectory=/opt/vaultwarden +ReadWriteDirectories=/opt/vaultwarden/data +AmbientCapabilities=CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable --q -now vaultwarden +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/vikunja-install.sh b/install/vikunja-install.sh new file mode 100644 index 000000000..3307fd04c --- /dev/null +++ b/install/vikunja-install.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (Canbiz) | Co-Author: CrazyWolf13 +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://vikunja.io/ + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +fetch_and_deploy_gh_release "vikunja" "go-vikunja/vikunja" "binary" + +msg_info "Setting up Vikunja" +sed -i 's|^# \(service:\)|\1|' /etc/vikunja/config.yml +sed -i "s|^ # \(publicurl: \).*| \1\"http://$LOCAL_IP\"|" /etc/vikunja/config.yml +sed -i "0,/^ # \(timezone: \).*/s|| \1${tz}|" /etc/vikunja/config.yml +systemctl enable -q --now vikunja +msg_ok "Set up Vikunja" + +motd_ssh +customize +cleanup_lxc diff --git a/install/wishlist-install.sh b/install/wishlist-install.sh index 5203d2731..659439f74 100644 --- a/install/wishlist-install.sh +++ b/install/wishlist-install.sh @@ -24,7 +24,7 @@ msg_ok "Installed dependencies" NODE_VERSION="24" NODE_MODULE="pnpm" setup_nodejs fetch_and_deploy_gh_release "wishlist" "cmintey/wishlist" "tarball" LATEST_APP_VERSION=$(get_latest_github_release "cmintey/wishlist" false) -import_local_ip + msg_info "Installing Wishlist" cd /opt/wishlist diff --git a/install/writefreely-install.sh b/install/writefreely-install.sh new file mode 100644 index 000000000..c3a1e21a7 --- /dev/null +++ b/install/writefreely-install.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: StellaeAlis +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://github.com/writefreely/writefreely + +# Import Functions and Setup +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 crudini +msg_ok "Installed Dependencies" + +setup_mariadb +MARIADB_DB_NAME="writefreely" MARIADB_DB_USER="writefreely" setup_mariadb_db + +get_lxc_ip + +fetch_and_deploy_gh_release "writefreely" "writefreely/writefreely" "prebuild" "latest" "/opt/writefreely" "writefreely_*_linux_amd64.tar.gz" + +msg_info "Setting up WriteFreely" +cd /opt/writefreely +$STD ./writefreely config generate +$STD ./writefreely keys generate +msg_ok "Setup WriteFreely" + +msg_info "Configuring WriteFreely" +$STD crudini --set config.ini server port 80 +$STD crudini --set config.ini server bind $LOCAL_IP +$STD crudini --set config.ini database username $MARIADB_DB_USER +$STD crudini --set config.ini database password $MARIADB_DB_PASS +$STD crudini --set config.ini database database $MARIADB_DB_NAME +$STD crudini --set config.ini app host http://$LOCAL_IP:80 +$STD ./writefreely db init +msg_ok "Configured WriteFreely" + +msg_info "Creating Service" +cat </etc/systemd/system/writefreely.service +[Unit] +Description=WriteFreely Service +After=syslog.target network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/writefreely +ExecStart=/opt/writefreely/writefreely +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now writefreely +msg_ok "Created Service" + +msg_info "Cleaning up" +$STD rm ~/writefreely.creds +msg_ok "Cleaned up" + +motd_ssh +customize + +cleanup_lxc diff --git a/misc/alpine-tools.func b/misc/alpine-tools.func index 955554216..9386f737a 100644 --- a/misc/alpine-tools.func +++ b/misc/alpine-tools.func @@ -196,11 +196,17 @@ ensure_usr_local_bin_persist() { download_with_progress() { # $1 url, $2 dest - local url="$1" out="$2" cl + local url="$1" out="$2" content_length need_tool curl pv || return 1 - 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" || { + + content_length=$( + curl -fsSLI "$url" 2>/dev/null | + awk '(tolower($1) ~ /^content-length:/) && ($2 + 0 > 0) {print $2+0}' | + tail -1 | tr -cd '[:digit:]' || true + ) + + if [ -n "$content_length" ]; then + curl -fsSL "$url" | pv -s "$content_length" >"$out" || { msg_error "Download failed: $url" return 1 } @@ -277,7 +283,7 @@ fetch_and_deploy_gh_release() { # $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 ' ')" + app_lc=$(lower "$app" | tr -d ' ') local vfile="$HOME/.${app_lc}" local json url filename tmpd unpack @@ -288,7 +294,7 @@ fetch_and_deploy_gh_release() { need_tool curl jq tar || return 1 [ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true - tmpd="$(mktemp -d)" || return 1 + tmpd=$(mktemp -d) || return 1 mkdir -p "$target" # Release JSON (with token/rate-limit handling) @@ -305,10 +311,10 @@ fetch_and_deploy_gh_release() { return 1 } fi - json="$(cat "$tmpd/release.json")" + json=$(cat "$tmpd/release.json") # correct Version - version="$(printf '%s' "$json" | jq -r '.tag_name // empty')" + version=$(printf '%s' "$json" | jq -r '.tag_name // empty') version="${version#v}" [ -z "$version" ] && { @@ -317,9 +323,15 @@ fetch_and_deploy_gh_release() { return 1 } + get_url() { + printf '%s' "$json" | jq -r '.assets[].browser_download_url' | + awk -v p="$pattern" 'BEGIN{IGNORECASE=1} $0 ~ p {print; exit}' | + tr -d '[:cntrl:]' + } + case "$mode" in tarball | source) - url="$(printf '%s' "$json" | jq -r '.tarball_url // empty')" + 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" || { @@ -331,7 +343,7 @@ fetch_and_deploy_gh_release() { rm -rf "$tmpd" return 1 } - unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)" + unpack=$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1) [ "${CLEAN_INSTALL:-0}" = "1" ] && rm -rf "${target:?}/"* # copy content of unpack to target (cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || { @@ -342,7 +354,7 @@ fetch_and_deploy_gh_release() { ;; binary) [ -n "$pattern" ] || pattern="*.apk" - url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" 'BEGIN{IGNORECASE=1} $0 ~ p {print; exit}')" + url=$(get_url) [ -z "$url" ] && { msg_error "binary asset not found for pattern: $pattern" rm -rf "$tmpd" @@ -374,10 +386,7 @@ fetch_and_deploy_gh_release() { rm -rf "$tmpd" return 1 } - url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" ' - BEGIN{IGNORECASE=1} - $0 ~ p {print; exit} - ')" + url=$(get_url) [ -z "$url" ] && { msg_error "asset not found for pattern: $pattern" rm -rf "$tmpd" @@ -411,7 +420,7 @@ fetch_and_deploy_gh_release() { [ "${CLEAN_INSTALL:-0}" = "1" ] && rm -rf "${target:?}/"* # 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)" + 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" @@ -431,10 +440,7 @@ fetch_and_deploy_gh_release() { rm -rf "$tmpd" return 1 } - url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" ' - BEGIN{IGNORECASE=1} - $0 ~ p {print; exit} - ')" + url=$(get_url) [ -z "$url" ] && { msg_error "asset not found for pattern: $pattern" rm -rf "$tmpd" diff --git a/misc/api.func b/misc/api.func index f6f284bec..657d786ba 100644 --- a/misc/api.func +++ b/misc/api.func @@ -1,6 +1,6 @@ # Copyright (c) 2021-2026 community-scripts ORG # Author: michelroegl-brunner -# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/LICENSE +# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/LICENSE # ============================================================================== # API.FUNC - TELEMETRY & DIAGNOSTICS API @@ -36,14 +36,16 @@ # # - Maps numeric exit codes to human-readable error descriptions # - Supports: -# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143) -# * Package manager errors (APT, DPKG: 100, 101, 255) -# * Node.js/npm errors (243-249, 254) -# * Python/pip/uv errors (210-212) -# * PostgreSQL errors (231-234) -# * MySQL/MariaDB errors (241-244) -# * MongoDB errors (251-254) +# * Generic/Shell errors (1, 2, 124, 126-130, 134, 137, 139, 141, 143) +# * curl/wget errors (6, 7, 22, 28, 35) +# * Package manager errors (APT, DPKG: 100-102, 255) +# * Systemd/Service errors (150-154) +# * Python/pip/uv errors (160-162) +# * PostgreSQL errors (170-173) +# * MySQL/MariaDB errors (180-183) +# * MongoDB errors (190-193) # * Proxmox custom codes (200-231) +# * Node.js/npm errors (243, 245-249) # - Returns description string for given exit code # - Shared function with error_handler.func for consistency # ------------------------------------------------------------------------------ @@ -53,73 +55,98 @@ explain_exit_code() { # --- Generic / Shell --- 1) echo "General error / Operation not permitted" ;; 2) echo "Misuse of shell builtins (e.g. syntax error)" ;; - 126) echo "Command invoked cannot execute (permission problem?)" ;; - 127) echo "Command not found" ;; - 128) echo "Invalid argument to exit" ;; - 130) echo "Terminated by Ctrl+C (SIGINT)" ;; - 137) echo "Killed (SIGKILL / Out of memory?)" ;; - 139) echo "Segmentation fault (core dumped)" ;; - 143) echo "Terminated (SIGTERM)" ;; + + # --- curl / wget errors (commonly seen in downloads) --- + 6) echo "curl: DNS resolution failed (could not resolve host)" ;; + 7) echo "curl: Failed to connect (network unreachable / host down)" ;; + 22) echo "curl: HTTP error returned (404, 429, 500+)" ;; + 28) echo "curl: Operation timeout (network slow or server not responding)" ;; + 35) echo "curl: SSL/TLS handshake failed (certificate error)" ;; # --- Package manager / APT / DPKG --- 100) echo "APT: Package manager error (broken packages / dependency problems)" ;; 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;; - 255) echo "DPKG: Fatal internal error" ;; + 102) echo "APT: Lock held by another process (dpkg/apt still running)" ;; - # --- Node.js / npm / pnpm / yarn --- + # --- Common shell/system errors --- + 124) echo "Command timed out (timeout command)" ;; + 126) echo "Command invoked cannot execute (permission problem?)" ;; + 127) echo "Command not found" ;; + 128) echo "Invalid argument to exit" ;; + 130) echo "Terminated by Ctrl+C (SIGINT)" ;; + 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)" ;; + + # --- Systemd / Service errors (150-154) --- + 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)" ;; + + # --- Python / pip / uv (160-162) --- + 160) echo "Python: Virtualenv / uv environment missing or broken" ;; + 161) echo "Python: Dependency resolution failed" ;; + 162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;; + + # --- PostgreSQL (170-173) --- + 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" ;; + + # --- MySQL / MariaDB (180-183) --- + 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" ;; + + # --- MongoDB (190-193) --- + 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" ;; + + # --- Proxmox Custom Codes (200-231) --- + 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" ;; + 231) echo "Proxmox: LXC stack upgrade failed" ;; + + # --- Node.js / npm / pnpm / yarn (243-249) --- 243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;; 245) echo "Node.js: Invalid command-line option" ;; 246) echo "Node.js: Internal JavaScript Parse Error" ;; 247) echo "Node.js: Fatal internal error" ;; 248) echo "Node.js: Invalid C++ addon / N-API failure" ;; - 249) echo "Node.js: Inspector error" ;; - 254) echo "npm/pnpm/yarn: Unknown fatal error" ;; + 249) echo "npm/pnpm/yarn: Unknown fatal error" ;; - # --- Python / pip / uv --- - 210) echo "Python: Virtualenv / uv environment missing or broken" ;; - 211) echo "Python: Dependency resolution failed" ;; - 212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;; - - # --- PostgreSQL --- - 231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;; - 232) echo "PostgreSQL: Authentication failed (bad user/password)" ;; - 233) echo "PostgreSQL: Database does not exist" ;; - 234) echo "PostgreSQL: Fatal error in query / syntax" ;; - - # --- MySQL / MariaDB --- - 241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;; - 242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;; - 243) echo "MySQL/MariaDB: Database does not exist" ;; - 244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;; - - # --- MongoDB --- - 251) echo "MongoDB: Connection failed (server not running)" ;; - 252) echo "MongoDB: Authentication failed (bad user/password)" ;; - 253) echo "MongoDB: Database not found" ;; - 254) echo "MongoDB: Fatal query error" ;; - - # --- Proxmox Custom Codes --- - 200) echo "Custom: Failed to create lock file" ;; - 203) echo "Custom: Missing CTID variable" ;; - 204) echo "Custom: Missing PCT_OSTYPE variable" ;; - 205) echo "Custom: Invalid CTID (<100)" ;; - 206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;; - 207) echo "Custom: Password contains unescaped special characters (-, /, \\, *, etc.)" ;; - 208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;; - 209) echo "Custom: Container creation failed (check logs for pct create output)" ;; - 210) echo "Custom: Cluster not quorate" ;; - 211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;; - 214) echo "Custom: Not enough storage space" ;; - 215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;; - 216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;; - 217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;; - 218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;; - 220) echo "Custom: Unable to resolve template path" ;; - 221) echo "Custom: Template file exists but not readable (check file permissions)" ;; - 222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;; - 223) echo "Custom: Template not available after download (storage sync issue)" ;; - 225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;; - 231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;; + # --- DPKG --- + 255) echo "DPKG: Fatal internal error" ;; # --- Default --- *) echo "Unknown error" ;; diff --git a/misc/build.func b/misc/build.func index b584767de..20a20e985 100644 --- a/misc/build.func +++ b/misc/build.func @@ -1,8 +1,7 @@ #!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/branch/main/LICENSE -# Revision: 1 +# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/LICENSE # ============================================================================== # BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION @@ -81,109 +80,6 @@ variables() { fi } -# ----------------------------------------------------------------------------- -# Community-Scripts bootstrap loader -# - Always sources build.func from remote -# - Updates local core files only if build.func changed -# - Local cache: /usr/local/community-scripts/core -# ----------------------------------------------------------------------------- - -# FUNC_DIR="/usr/local/community-scripts/core" -# mkdir -p "$FUNC_DIR" - -# BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func" -# BUILD_REV="$FUNC_DIR/build.rev" -# DEVMODE="${DEVMODE:-no}" - -# # --- Step 1: fetch build.func content once, compute hash --- -# build_content="$(curl -fsSL "$BUILD_URL")" || { -# echo "❌ Failed to fetch build.func" -# exit 1 -# } - -# newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}') -# oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "") - -# # --- Step 2: if build.func changed, offer update for core files --- -# if [ "$newhash" != "$oldhash" ]; then -# echo "⚠️ build.func changed!" - -# while true; do -# read -rp "Refresh local core files? [y/N/diff]: " ans -# case "$ans" in -# [Yy]*) -# echo "$newhash" >"$BUILD_REV" - -# update_func_file() { -# local file="$1" -# local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file" -# local local_path="$FUNC_DIR/$file" - -# echo "⬇️ Downloading $file ..." -# curl -fsSL "$url" -o "$local_path" || { -# echo "❌ Failed to fetch $file" -# exit 1 -# } -# echo "✔️ Updated $file" -# } - -# update_func_file core.func -# update_func_file error_handler.func -# update_func_file tools.func -# break -# ;; -# [Dd]*) -# for file in core.func error_handler.func tools.func; do -# local_path="$FUNC_DIR/$file" -# url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file" -# remote_tmp="$(mktemp)" - -# curl -fsSL "$url" -o "$remote_tmp" || continue - -# if [ -f "$local_path" ]; then -# echo "🔍 Diff for $file:" -# diff -u "$local_path" "$remote_tmp" || echo "(no differences)" -# else -# echo "📦 New file $file will be installed" -# fi - -# rm -f "$remote_tmp" -# done -# ;; -# *) -# echo "❌ Skipped updating local core files" -# break -# ;; -# esac -# done -# else -# if [ "$DEVMODE" != "yes" ]; then -# echo "✔️ build.func unchanged → using existing local core files" -# fi -# fi - -# if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then -# return 0 2>/dev/null || exit 0 -# fi -# _COMMUNITY_SCRIPTS_LOADER=1 - -# # --- Step 3: always source local versions of the core files --- -# source "$FUNC_DIR/core.func" -# source "$FUNC_DIR/error_handler.func" -# source "$FUNC_DIR/tools.func" - -# # --- Step 4: finally, source build.func directly from memory --- -# # (no tmp file needed) -# source <(printf "%s" "$build_content") - -# ------------------------------------------------------------------------------ -# Load core + error handler functions from community-scripts repo -# -# - Prefer curl if available, fallback to wget -# - Load: core.func, error_handler.func, api.func -# - Initialize error traps after loading -# ------------------------------------------------------------------------------ - source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func) if command -v curl >/dev/null 2>&1; then @@ -191,13 +87,11 @@ if command -v curl >/dev/null 2>&1; then source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) load_functions catch_errors - #echo "(build.func) Loaded core.func via curl" elif command -v wget >/dev/null 2>&1; then source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func) source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) load_functions catch_errors - #echo "(build.func) Loaded core.func via wget" fi # ============================================================================== @@ -266,17 +160,29 @@ maxkeys_check() { # # - Returns current container IP depending on OS type # - Debian/Ubuntu: uses `hostname -I` -# - Alpine: parses eth0 via `ip -4 addr` +# - Alpine: parses eth0 via `ip -4 addr` or `ip -6 addr` +# - Supports IPv6-only environments as fallback # - Returns "Unknown" if OS type cannot be determined # ------------------------------------------------------------------------------ get_current_ip() { + CURRENT_IP="" if [ -f /etc/os-release ]; then # Check for Debian/Ubuntu (uses hostname -I) if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then - CURRENT_IP=$(hostname -I | awk '{print $1}') + # Try IPv4 first + CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1) + # Fallback to IPv6 if no IPv4 + if [[ -z "$CURRENT_IP" ]]; then + CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1) + fi # Check for Alpine (uses ip command) elif grep -q 'ID=alpine' /etc/os-release; then - CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) + # Try IPv4 first + CURRENT_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) + # Fallback to IPv6 if no IPv4 + if [[ -z "$CURRENT_IP" ]]; then + CURRENT_IP=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n 1) + fi else CURRENT_IP="Unknown" fi @@ -308,6 +214,7 @@ update_motd_ip() { # # - Installs SSH keys into container root account if SSH is enabled # - Uses pct push or direct input to authorized_keys +# - Supports both SSH_KEYS_FILE (from advanced settings) and SSH_AUTHORIZED_KEY (from user defaults) # - Falls back to warning if no keys provided # ------------------------------------------------------------------------------ install_ssh_keys_into_ct() { @@ -316,6 +223,13 @@ install_ssh_keys_into_ct() { # Ensure SSH_KEYS_FILE is defined (may not be set if advanced_settings was skipped) : "${SSH_KEYS_FILE:=}" + # If SSH_KEYS_FILE doesn't exist but SSH_AUTHORIZED_KEY is set (from user defaults), + # create a temporary SSH_KEYS_FILE with the key + if [[ -z "$SSH_KEYS_FILE" || ! -s "$SSH_KEYS_FILE" ]] && [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then + SSH_KEYS_FILE="$(mktemp)" + printf '%s\n' "$SSH_AUTHORIZED_KEY" >"$SSH_KEYS_FILE" + fi + if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then msg_info "Installing selected SSH keys into CT ${CTID}" pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || { @@ -332,7 +246,7 @@ install_ssh_keys_into_ct() { return 0 fi - # Fallback: nichts ausgewählt + # Fallback msg_warn "No SSH keys to install (skipping)." return 0 } @@ -384,6 +298,53 @@ get_valid_container_id() { echo "$suggested_id" } +# ------------------------------------------------------------------------------ +# validate_container_id() +# +# - Validates if a container ID is available for use +# - Checks if ID is already used by VM or LXC container +# - Checks if ID is used in LVM logical volumes +# - Returns 0 if ID is available, 1 if already in use +# ------------------------------------------------------------------------------ +validate_container_id() { + local ctid="$1" + + # Check if ID is numeric + if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then + return 1 + fi + + # Check if config file exists for VM or LXC + if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then + return 1 + fi + + # Check if ID is used in LVM logical volumes + if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# get_valid_container_id() +# +# - Returns a valid, unused container ID +# - If provided ID is valid, returns it +# - Otherwise increments from suggested ID until a free one is found +# - Calls validate_container_id() to check availability +# ------------------------------------------------------------------------------ +get_valid_container_id() { + local suggested_id="${1:-$(pvesh get /cluster/nextid)}" + + while ! validate_container_id "$suggested_id"; do + suggested_id=$((suggested_id + 1)) + done + + echo "$suggested_id" +} + # ------------------------------------------------------------------------------ # validate_hostname() # @@ -935,8 +896,6 @@ base_settings() { CORE_COUNT="${final_cpu}" RAM_SIZE="${final_ram}" VERBOSE=${var_verbose:-"${1:-no}"} - - # Password sanitization - clean up dashes and format properly PW="" if [[ -n "${var_pw:-}" ]]; then local _pw_raw="${var_pw}" @@ -1025,7 +984,6 @@ base_settings() { ENABLE_NESTING=${var_nesting:-"1"} ENABLE_KEYCTL=${var_keyctl:-"0"} ENABLE_MKNOD=${var_mknod:-"0"} - MOUNT_FS=${var_mount_fs:-""} PROTECT_CT=${var_protection:-"no"} CT_TIMEZONE=${var_timezone:-"$timezone"} [[ "${CT_TIMEZONE:-}" == Etc/* ]] && CT_TIMEZONE="host" # pct doesn't accept Etc/* zones @@ -1315,18 +1273,12 @@ var_fuse=no var_tun=no # Advanced Settings (Proxmox-official features) -# var_nesting: Allow nesting (required for Docker/LXC in CT) -var_nesting=1 -# var_keyctl: Allow keyctl() - needed for Docker (systemd-networkd workaround) -var_keyctl=0 -# var_mknod: Allow device node creation (requires kernel 5.3+, experimental) -var_mknod=0 -# var_mount_fs: Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults) -var_mount_fs= -# var_protection: Prevent accidental deletion of container -var_protection=no -# var_timezone: Container timezone (e.g. Europe/Berlin, leave empty for host timezone) -var_timezone= +var_nesting=1 # Allow nesting (required for Docker/LXC in CT) +var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround) +var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental) +var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults) +var_protection=no # Prevent accidental deletion of container +var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone) var_tags=community-script var_verbose=no @@ -1638,7 +1590,7 @@ maybe_offer_save_app_defaults() { # 1) if no file → offer to create if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then mkdir -p "$(dirname "$app_vars_path")" install -m 0644 "$new_tmp" "$app_vars_path" @@ -1663,7 +1615,7 @@ maybe_offer_save_app_defaults() { while true; do local sel - sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + sel="$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "APP DEFAULTS – ${APP}" \ --menu "Differences detected. What do you want to do?" 20 78 10 \ "Update Defaults" "Write new values to ${app_vars_file}" \ @@ -1684,7 +1636,7 @@ maybe_offer_save_app_defaults() { break ;; "View Diff") - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "Diff – ${APP}" \ --scrolltext --textbox "$diff_tmp" 25 100 ;; @@ -1709,6 +1661,13 @@ ensure_storage_selection_for_vars_file() { if [[ -n "$tpl" && -n "$ct" ]]; then TEMPLATE_STORAGE="$tpl" CONTAINER_STORAGE="$ct" + + # Validate storage space for loaded container storage + if [[ -n "${DISK_SIZE:-}" ]]; then + validate_storage_space "$ct" "$DISK_SIZE" "yes" + # Continue even if validation fails - user was warned + fi + return 0 fi @@ -1789,7 +1748,7 @@ advanced_settings() { elif [ -f /etc/timezone ]; then _host_timezone=$(cat /etc/timezone 2>/dev/null || echo "") fi - # pct doesn't accept Etc/* zones - map to 'host' instead + # Map Etc/* timezones to "host" (pct doesn't accept Etc/* zones) [[ "${_host_timezone:-}" == Etc/* ]] && _host_timezone="host" local _ct_timezone="${var_timezone:-$_host_timezone}" [[ "${_ct_timezone:-}" == Etc/* ]] && _ct_timezone="host" @@ -1886,32 +1845,36 @@ advanced_settings() { elif [[ "$PW1" == *" "* ]]; then whiptail --msgbox "Password cannot contain spaces." 8 58 else - # Clean up leading dashes from password local _pw1_clean="$PW1" while [[ "$_pw1_clean" == -* ]]; do _pw1_clean="${_pw1_clean#-}" done if [[ -z "$_pw1_clean" ]]; then whiptail --msgbox "Password cannot be only '-' characters." 8 58 + continue elif ((${#_pw1_clean} < 5)); then whiptail --msgbox "Password must be at least 5 characters (after removing leading '-')." 8 70 - else - # Verify password - if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ - --title "PASSWORD VERIFICATION" \ - --ok-button "Confirm" --cancel-button "Back" \ - --passwordbox "\nVerify Root Password" 10 58 \ - 3>&1 1>&2 2>&3); then - if [[ "$PW1" == "$PW2" ]]; then - _pw="--password $_pw1_clean" - _pw_display="********" - ((STEP++)) - else - whiptail --msgbox "Passwords do not match. Please try again." 8 58 - fi + continue + fi + # Verify password + if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "PASSWORD VERIFICATION" \ + --ok-button "Confirm" --cancel-button "Back" \ + --passwordbox "\nVerify Root Password" 10 58 \ + 3>&1 1>&2 2>&3); then + local _pw2_clean="$PW2" + while [[ "$_pw2_clean" == -* ]]; do + _pw2_clean="${_pw2_clean#-}" + done + if [[ "$_pw1_clean" == "$_pw2_clean" ]]; then + _pw="--password $_pw1_clean" + _pw_display="********" + ((STEP++)) else - ((STEP--)) + whiptail --msgbox "Passwords do not match. Please try again." 8 58 fi + else + ((STEP--)) fi fi else @@ -1929,22 +1892,25 @@ advanced_settings() { --inputbox "\nSet Container ID" 10 58 "$_ct_id" \ 3>&1 1>&2 2>&3); then local input_id="${result:-$NEXTID}" - # Validate container ID is numeric + + # Validate that ID is numeric if ! [[ "$input_id" =~ ^[0-9]+$ ]]; then - whiptail --msgbox "Container ID must be numeric." 8 58 + whiptail --backtitle "Proxmox VE Helper Scripts" --title "Invalid ID" --msgbox "Container ID must be numeric." 8 58 continue fi - # Validate container ID is available + + # Check if ID is already in use if ! validate_container_id "$input_id"; then - if whiptail --yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID: $(get_valid_container_id "$input_id")?" 10 58; then + if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" \ + --yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID ($(get_valid_container_id "$input_id"))?" 10 58; then _ct_id=$(get_valid_container_id "$input_id") - ((STEP++)) + else + continue fi - # else stay on this step else _ct_id="$input_id" - ((STEP++)) fi + ((STEP++)) else ((STEP--)) fi @@ -1957,15 +1923,16 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "HOSTNAME" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Hostname (lowercase, alphanumeric, hyphens only)" 10 58 "$_hostname" \ + --inputbox "\nSet Hostname (or FQDN, e.g. host.example.com)" 10 58 "$_hostname" \ 3>&1 1>&2 2>&3); then local hn_test="${result:-$NSAPP}" hn_test=$(echo "${hn_test,,}" | tr -d ' ') - if [[ "$hn_test" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then + + if validate_hostname "$hn_test"; then _hostname="$hn_test" ((STEP++)) else - whiptail --msgbox "Invalid hostname: '$hn_test'\n\nOnly lowercase letters, digits and hyphens are allowed." 10 58 + whiptail --msgbox "Invalid hostname: '$hn_test'\n\nRules:\n- Only lowercase letters, digits, dots and hyphens\n- Labels separated by dots (max 63 chars each)\n- No leading/trailing hyphens or dots\n- No consecutive dots\n- Total max 253 characters" 14 60 fi else ((STEP--)) @@ -2040,8 +2007,14 @@ advanced_settings() { # ═══════════════════════════════════════════════════════════════════════════ 8) if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then - _bridge="vmbr0" - ((STEP++)) + # Validate default bridge exists + if validate_bridge "vmbr0"; then + _bridge="vmbr0" + ((STEP++)) + else + whiptail --msgbox "Default bridge 'vmbr0' not found!\n\nPlease configure a network bridge in Proxmox first." 10 58 + exit 1 + fi else if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "NETWORK BRIDGE" \ @@ -2049,8 +2022,13 @@ advanced_settings() { --menu "\nSelect network bridge:" 16 58 6 \ "${BRIDGE_MENU_OPTIONS[@]}" \ 3>&1 1>&2 2>&3); then - _bridge="${result:-vmbr0}" - ((STEP++)) + local bridge_test="${result:-vmbr0}" + if validate_bridge "$bridge_test"; then + _bridge="$bridge_test" + ((STEP++)) + else + whiptail --msgbox "Bridge '$bridge_test' is not available or not active." 8 58 + fi else ((STEP--)) fi @@ -2064,9 +2042,10 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "IPv4 CONFIGURATION" \ --ok-button "Next" --cancel-button "Back" \ - --menu "\nSelect IPv4 Address Assignment:" 14 60 2 \ + --menu "\nSelect IPv4 Address Assignment:" 16 65 3 \ "dhcp" "Automatic (DHCP, recommended)" \ "static" "Static (manual entry)" \ + "range" "IP Range Scan (find first free IP)" \ 3>&1 1>&2 2>&3); then if [[ "$result" == "static" ]]; then @@ -2077,7 +2056,7 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then + if validate_ip_address "$static_ip"; then # Get gateway local gateway_ip if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ @@ -2085,16 +2064,62 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nEnter Gateway IP address" 10 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - _net="$static_ip" - _gate=",gw=$gateway_ip" - ((STEP++)) + if validate_gateway_ip "$gateway_ip"; then + # Validate gateway is in same subnet + if validate_gateway_in_subnet "$static_ip" "$gateway_ip"; then + _net="$static_ip" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Gateway is not in the same subnet as the static IP.\n\nStatic IP: $static_ip\nGateway: $gateway_ip" 10 58 + fi else - whiptail --msgbox "Invalid Gateway IP format." 8 58 + whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58 fi fi else - whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58 + whiptail --msgbox "Invalid IPv4 CIDR format.\n\nEach octet must be 0-255.\nCIDR must be 1-32.\nExample: 192.168.1.100/24" 12 58 + fi + fi + elif [[ "$result" == "range" ]]; then + # IP Range Scan + local ip_range + if ip_range=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "IP RANGE SCAN" \ + --ok-button "Scan" --cancel-button "Back" \ + --inputbox "\nEnter IP range to scan for free address\n(e.g. 192.168.1.100/24-192.168.1.200/24)" 12 65 "" \ + 3>&1 1>&2 2>&3); then + if is_ip_range "$ip_range"; then + # Exit whiptail screen temporarily to show scan progress + clear + header_info + echo -e "${INFO}${BOLD}${DGN}Scanning IP range for free address...${CL}\n" + if resolve_ip_from_range "$ip_range"; then + # Get gateway + local gateway_ip + if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ + --title "GATEWAY IP" \ + --ok-button "Next" --cancel-button "Back" \ + --inputbox "\nFound free IP: $NET_RESOLVED\n\nEnter Gateway IP address" 12 58 "" \ + 3>&1 1>&2 2>&3); then + if validate_gateway_ip "$gateway_ip"; then + # Validate gateway is in same subnet + if validate_gateway_in_subnet "$NET_RESOLVED" "$gateway_ip"; then + _net="$NET_RESOLVED" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Gateway is not in the same subnet as the IP.\n\nIP: $NET_RESOLVED\nGateway: $gateway_ip" 10 58 + fi + else + whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58 + fi + fi + else + whiptail --msgbox "No free IP found in the specified range.\nAll IPs responded to ping." 10 58 + fi + else + whiptail --msgbox "Invalid IP range format.\n\nExample: 192.168.1.100/24-192.168.1.200/24" 10 58 fi fi else @@ -2130,16 +2155,33 @@ advanced_settings() { --title "STATIC IPv6 ADDRESS" \ --inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$ipv6_addr" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then + if validate_ipv6_address "$ipv6_addr"; then _ipv6_addr="$ipv6_addr" - # Optional gateway - _ipv6_gate=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "IPv6 GATEWAY" \ - --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ - 3>&1 1>&2 2>&3) || true - ((STEP++)) + # Optional gateway - loop until valid or empty + local ipv6_gw_valid=false + while [[ "$ipv6_gw_valid" == "false" ]]; do + local ipv6_gw + ipv6_gw=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "IPv6 GATEWAY" \ + --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ + 3>&1 1>&2 2>&3) || true + # Validate gateway if provided + if [[ -n "$ipv6_gw" ]]; then + if validate_ipv6_address "$ipv6_gw"; then + _ipv6_gate="$ipv6_gw" + ipv6_gw_valid=true + ((STEP++)) + else + whiptail --msgbox "Invalid IPv6 gateway format.\n\nExample: 2001:db8::1" 8 58 + fi + else + _ipv6_gate="" + ipv6_gw_valid=true + ((STEP++)) + fi + done else - whiptail --msgbox "Invalid IPv6 CIDR format." 8 58 + whiptail --msgbox "Invalid IPv6 CIDR format.\n\nExample: 2001:db8::1/64\nCIDR must be 1-128." 10 58 fi fi ;; @@ -2148,11 +2190,7 @@ advanced_settings() { _ipv6_gate="" ((STEP++)) ;; - disable) - _ipv6_addr="" - _ipv6_gate="" - ((STEP++)) - ;; + none) _ipv6_addr="none" _ipv6_gate="" @@ -2176,10 +2214,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "MTU SIZE" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500)" 12 58 "" \ + --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500, common values: 1500, 9000)" 12 62 "" \ 3>&1 1>&2 2>&3); then - _mtu="$result" - ((STEP++)) + if validate_mtu "$result"; then + _mtu="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid MTU size.\n\nMTU must be between 576 and 65535.\nCommon values: 1500 (default), 9000 (jumbo frames)" 10 58 + fi else ((STEP--)) fi @@ -2224,10 +2266,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "MAC ADDRESS" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet MAC Address\n(leave blank for auto-generated)" 12 58 "" \ + --inputbox "\nSet MAC Address\n(leave blank for auto-generated, format: XX:XX:XX:XX:XX:XX)" 12 62 "" \ 3>&1 1>&2 2>&3); then - _mac="$result" - ((STEP++)) + if validate_mac_address "$result"; then + _mac="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid MAC address format.\n\nRequired format: XX:XX:XX:XX:XX:XX\nExample: 02:00:00:00:00:01" 10 58 + fi else ((STEP--)) fi @@ -2240,10 +2286,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "VLAN TAG" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet VLAN Tag\n(leave blank for no VLAN)" 12 58 "" \ + --inputbox "\nSet VLAN Tag (1-4094)\n(leave blank for no VLAN)" 12 58 "" \ 3>&1 1>&2 2>&3); then - _vlan="$result" - ((STEP++)) + if validate_vlan_tag "$result"; then + _vlan="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid VLAN tag.\n\nVLAN must be a number between 1 and 4094." 8 58 + fi else ((STEP--)) fi @@ -2256,11 +2306,16 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "CONTAINER TAGS" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Custom Tags (semicolon-separated)\n(remove all for no tags)" 12 58 "$_tags" \ + --inputbox "\nSet Custom Tags (semicolon-separated)\n(alphanumeric, hyphens, underscores only)" 12 58 "$_tags" \ 3>&1 1>&2 2>&3); then - _tags="${result:-;}" - _tags=$(echo "$_tags" | tr -d '[:space:]') - ((STEP++)) + local tags_test="${result:-}" + tags_test=$(echo "$tags_test" | tr -d '[:space:]') + if validate_tags "$tags_test"; then + _tags="$tags_test" + ((STEP++)) + else + whiptail --msgbox "Invalid tag format.\n\nTags can only contain:\n- Letters (a-z, A-Z)\n- Numbers (0-9)\n- Hyphens (-)\n- Underscores (_)\n- Semicolons (;) as separator" 14 58 + fi else ((STEP--)) fi @@ -2439,8 +2494,14 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${_host_timezone:-unknown}\n\nLeave empty to inherit from host." 16 62 "$_ct_timezone" \ 3>&1 1>&2 2>&3); then - _ct_timezone="$result" - ((STEP++)) + local tz_test="$result" + [[ "${tz_test:-}" == Etc/* ]] && tz_test="host" # pct doesn't accept Etc/* zones + if validate_timezone "$tz_test"; then + _ct_timezone="$tz_test" + ((STEP++)) + else + whiptail --msgbox "Invalid timezone: '$result'\n\nTimezone must exist in /usr/share/zoneinfo/\n\nExamples:\n- Europe/Berlin\n- America/New_York\n- Asia/Tokyo\n- UTC" 14 58 + fi else ((STEP--)) fi @@ -2640,10 +2701,9 @@ Advanced: export UDHCPC_FIX export SSH_KEYS_FILE - # Exit alternate screen buffer BEFORE displaying summary - # so the summary is visible in the main terminal + # Exit alternate screen buffer before showing summary (so output remains visible) tput rmcup 2>/dev/null || true - trap - RETURN # Remove the trap since we already called rmcup + trap - RETURN # Display final summary echo -e "\n${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" @@ -2658,14 +2718,14 @@ Advanced: echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" echo -e "${NETWORK}${BOLD}${DGN}IPv4: ${BGN}$NET${CL}" echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}$IPV6_METHOD${CL}" - echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}$ENABLE_FUSE${CL}" - [[ "$ENABLE_TUN" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "$ENABLE_NESTING" == "1" ] && echo "Enabled" || echo "Disabled")${CL}" - [[ "$ENABLE_KEYCTL" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}" - echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}$ENABLE_GPU${CL}" - [[ "$PROTECT_CT" == "yes" || "$PROTECT_CT" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}" - [[ -n "$CT_TIMEZONE" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}" - [[ "$APT_CACHER" == "yes" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}" + echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}${ENABLE_FUSE:-no}${CL}" + [[ "${ENABLE_TUN:-no}" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")${CL}" + [[ "${ENABLE_KEYCTL:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}" + echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}${ENABLE_GPU:-no}${CL}" + [[ "${PROTECT_CT:-no}" == "yes" || "${PROTECT_CT:-no}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}" + [[ -n "${CT_TIMEZONE:-}" ]] && echo -e "${INFO}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}" + [[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}" echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" } @@ -2690,12 +2750,12 @@ diagnostics_check() { fi if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then - if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then cat </usr/local/community-scripts/diagnostics DIAGNOSTICS=yes #This file is used to store the diagnostics settings for the Community-Scripts API. -#https://github.com/community-scripts/ProxmoxVED/discussions/1836 +#https://git.community-scripts.org/community-scripts/ProxmoxVED/discussions/1836 #Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. #You can review the data at https://community-scripts.github.io/ProxmoxVE/data #If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. @@ -2720,7 +2780,7 @@ EOF DIAGNOSTICS=no #This file is used to store the diagnostics settings for the Community-Scripts API. -#https://github.com/community-scripts/ProxmoxVED/discussions/1836 +#https://git.community-scripts.org/community-scripts/ProxmoxVED/discussions/1836 #Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. #You can review the data at https://community-scripts.github.io/ProxmoxVE/data #If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. @@ -2749,7 +2809,7 @@ EOF diagnostics_menu() { if [ "${DIAGNOSTICS:-no}" = "yes" ]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "DIAGNOSTIC SETTINGS" \ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ --yes-button "No" --no-button "Back"; then @@ -2758,7 +2818,7 @@ diagnostics_menu() { whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 fi else - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + if whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "DIAGNOSTIC SETTINGS" \ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ --yes-button "Yes" --no-button "Back"; then @@ -2788,8 +2848,8 @@ echo_default() { echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "${var_gpu:-no}" == "yes" ]; then - echo -e "🎮${BOLD}${DGN} GPU Passthrough: ${BGN}Enabled${CL}" + if [[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]]; then + echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}Enabled${CL}" fi if [ "$VERBOSE" == "yes" ]; then echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" @@ -2830,8 +2890,7 @@ install_script() { else timezone="UTC" fi - # pct doesn't accept Etc/* zones - map to 'host' instead - [[ "${timezone:-}" == Etc/* ]] && timezone="host" + [[ "${timezone:-}" == Etc/* ]] && timezone="host" # pct doesn't accept Etc/* zones # Show APP Header header_info @@ -2895,7 +2954,6 @@ install_script() { 2 | advanced | ADVANCED) header_info echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" METHOD="advanced" base_settings advanced_settings @@ -2916,8 +2974,8 @@ install_script() { header_info echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" METHOD="appdefaults" + load_vars_file "$(get_app_defaults_path)" "yes" # Force override script defaults base_settings - load_vars_file "$(get_app_defaults_path)" echo_default defaults_target="$(get_app_defaults_path)" break @@ -3028,21 +3086,29 @@ check_container_resources() { # ------------------------------------------------------------------------------ # check_container_storage() # -# - Checks /boot partition usage +# - Checks root (/) partition usage # - Warns if usage >80% and asks user confirmation before proceeding # ------------------------------------------------------------------------------ check_container_storage() { - total_size=$(df /boot --output=size | tail -n 1) - local used_size=$(df /boot --output=used | tail -n 1) - usage=$((100 * used_size / total_size)) - if ((usage > 80)); then - echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}" - echo -ne "Continue anyway? " + usage=$(df / -P | awk 'NR==2 {print $5}' | tr -d '%') + + if [ -z "$usage" ] || [ "$usage" -lt 0 ]; then + echo -e "${CROSS}${HOLD}${RD}Error: Failed to check disk usage.${CL}" + exit 1 + fi + + if [ "$usage" -gt 80 ]; then + echo -e "${INFO}${HOLD}${YWB}Warning: Storage is dangerously low (${usage}%).${CL}" + printf "Continue anyway? " read -r prompt - if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then + + case "$prompt" in + [yY][eE][sS] | [yY]) ;; + *) echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}" exit 1 - fi + ;; + esac fi } @@ -3058,9 +3124,9 @@ ssh_extract_keys_from_file() { tr -d '\r' <"$f" | awk ' /^[[:space:]]*#/ {next} /^[[:space:]]*$/ {next} - # nackt: typ base64 [comment] + # bare format: type base64 [comment] /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next} - # mit Optionen: finde ab erstem Key-Typ + # with options: find from first key-type onward { match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/) if (RSTART>0) { print substr($0, RSTART) } @@ -3134,8 +3200,8 @@ ssh_discover_default_files() { configure_ssh_settings() { local step_info="${1:-}" - local backtitle="[dev] Proxmox VE Helper Scripts" - [[ -n "$step_info" ]] && backtitle="[dev] Proxmox VE Helper Scripts [${step_info}]" + local backtitle="Proxmox VE Helper Scripts" + [[ -n "$step_info" ]] && backtitle="Proxmox VE Helper Scripts [${step_info}]" SSH_KEYS_FILE="$(mktemp)" : >"$SSH_KEYS_FILE" @@ -3244,7 +3310,7 @@ start() { update_script cleanup_lxc else - CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ + CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ "Support/Update functions for ${APP} LXC. Choose an option:" \ 12 60 3 \ "1" "YES (Silent Mode)" \ @@ -3302,7 +3368,7 @@ build_container() { esac fi - # IP (immer zwingend, Standard dhcp) + # IP (always required, default dhcp) NET_STRING+=",ip=${NET:-dhcp}" # Gateway @@ -3359,14 +3425,14 @@ build_container() { FEATURES="${FEATURES}fuse=1" fi - # NEW IMPLEMENTATION (Fixed): Build PCT_OPTIONS properly - # Key insight: Bash cannot export arrays, so we build the options as a string - + # Build PCT_OPTIONS as string for export TEMP_DIR=$(mktemp -d) pushd "$TEMP_DIR" >/dev/null - - # Unified install.func automatically detects OS type (debian, alpine, fedora, etc.) - export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" + if [ "$var_os" == "alpine" ]; then + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)" + else + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" + fi # Core exports for install.func export DIAGNOSTICS="$DIAGNOSTICS" @@ -3385,11 +3451,11 @@ build_container() { export CTTYPE="$CT_TYPE" export ENABLE_FUSE="$ENABLE_FUSE" export ENABLE_TUN="$ENABLE_TUN" - export ENABLE_GPU="$ENABLE_GPU" - export IPV6_METHOD="$IPV6_METHOD" export PCT_OSTYPE="$var_os" export PCT_OSVERSION="$var_version" export PCT_DISK_SIZE="$DISK_SIZE" + export IPV6_METHOD="$IPV6_METHOD" + export ENABLE_GPU="$ENABLE_GPU" # DEV_MODE exports (optional, for debugging) export BUILD_LOG="$BUILD_LOG" @@ -3403,38 +3469,25 @@ build_container() { export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}" export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" - # Validate storage space before container creation - if [[ -n "$CONTAINER_STORAGE" ]]; then - msg_info "Validating storage space" - if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then - local free_space - free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') - local free_fmt - free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB") - msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}" - exit 214 - fi - msg_ok "Storage space validated" - fi + # MODE export for unattended detection in install scripts + # This tells install scripts whether to prompt for input or use defaults + export MODE="${METHOD:-default}" # Build PCT_OPTIONS as multi-line string - PCT_OPTIONS_STRING="" + PCT_OPTIONS_STRING=" -hostname $HN" - # Add features if set - if [ -n "$FEATURES" ]; then - PCT_OPTIONS_STRING=" -features $FEATURES" - fi - - # Add hostname - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - -hostname $HN" - - # Add tags if set + # Only add -tags if TAGS is not empty if [ -n "$TAGS" ]; then PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING -tags $TAGS" fi + # Only add -features if FEATURES is not empty + if [ -n "$FEATURES" ]; then + PCT_OPTIONS_STRING=" -features $FEATURES +$PCT_OPTIONS_STRING" + fi + # Add storage if specified if [ -n "$SD" ]; then PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING @@ -3461,10 +3514,12 @@ build_container() { -protection 1" fi - # Timezone flag (if var_timezone was set) + # Timezone (map Etc/* to "host" as pct doesn't accept them) if [ -n "${CT_TIMEZONE:-}" ]; then + local _pct_timezone="$CT_TIMEZONE" + [[ "$_pct_timezone" == Etc/* ]] && _pct_timezone="host" PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - -timezone $CT_TIMEZONE" + -timezone $_pct_timezone" fi # Password (already formatted) @@ -3478,10 +3533,20 @@ build_container() { export TEMPLATE_STORAGE="${var_template_storage:-}" export CONTAINER_STORAGE="${var_container_storage:-}" - # # DEBUG: Show final PCT_OPTIONS being exported - # echo "[DEBUG] PCT_OPTIONS to be exported:" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Calling create_lxc_container..." + # Validate storage space only if CONTAINER_STORAGE is already set + # (Storage selection happens in create_lxc_container for some modes) + if [[ -n "$CONTAINER_STORAGE" ]]; then + msg_info "Validating storage space" + if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then + local free_space + free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') + local free_fmt + free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB") + msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}" + exit 214 + fi + msg_ok "Storage space validated" + fi create_lxc_container || exit $? @@ -3540,12 +3605,16 @@ build_container() { msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" # Simple passthrough - just bind /dev/nvidia* devices if they exist - # Skip directories like /dev/nvidia-caps (they need special handling) + # Only include character devices (-c), skip directories like /dev/nvidia-caps for d in /dev/nvidia*; do - [[ -e "$d" ]] || continue - [[ -d "$d" ]] && continue # Skip directories - NVIDIA_DEVICES+=("$d") + [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d") done + # Also check for devices inside /dev/nvidia-caps/ directory + if [[ -d /dev/nvidia-caps ]]; then + for d in /dev/nvidia-caps/*; do + [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d") + done + fi if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough" @@ -3628,12 +3697,8 @@ EOF selected_gpu="${available_gpus[0]}" msg_ok "Automatically configuring ${selected_gpu} GPU passthrough" else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu + # Multiple GPUs - ask user (use first as default in unattended mode) + selected_gpu=$(prompt_select "Which GPU type to passthrough?" 1 60 "${available_gpus[@]}") selected_gpu="${selected_gpu^^}" # Validate selection @@ -3778,7 +3843,6 @@ EOF msg_ok "Network in LXC is reachable (ping)" fi fi - # Function to get correct GID inside container get_container_gid() { local group="$1" @@ -3788,188 +3852,69 @@ EOF fix_gpu_gids - # Continue with standard container setup + # Fix Debian 13 LXC template bug where / is owned by nobody:nogroup + # This causes systemd-tmpfiles to fail with "unsafe path transition" errors + # We need to fix this from the host before any package installation + if [[ "$var_os" == "debian" && "$var_version" == "13" ]]; then + # Stop container, fix ownership, restart + pct stop "$CTID" >/dev/null 2>&1 || true + sleep 1 + # Get the actual rootfs path from pct mount + local rootfs_path + rootfs_path=$(pct mount "$CTID" 2>/dev/null | grep -oP 'mounted at \K.*' || echo "") + if [[ -n "$rootfs_path" && -d "$rootfs_path" ]]; then + chown root:root "$rootfs_path" 2>/dev/null || true + fi + pct unmount "$CTID" >/dev/null 2>&1 || true + pct start "$CTID" >/dev/null 2>&1 + sleep 3 + fi + msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - install core dependencies based on OS - sleep 3 - - case "$var_os" in - alpine) + # Continue with standard container setup + if [ "$var_os" == "alpine" ]; then + sleep 3 pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories http://dl-cdn.alpinelinux.org/alpine/latest-stable/main http://dl-cdn.alpinelinux.org/alpine/latest-stable/community EOF' pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null" - ;; + else + sleep 3 + LANG=${LANG:-en_US.UTF-8} - debian | ubuntu | devuan) - # First install locales package (required for locale-gen on minimal templates) - pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y locales >/dev/null 2>&1 || true" - - # Locale setup for Debian-based - pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen 2>/dev/null || true" - pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen 2>/dev/null | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ - [[ -n \"\$locale_line\" ]] && echo LANG=\$locale_line >/etc/default/locale && \ - locale-gen >/dev/null 2>&1 && \ - export LANG=\$locale_line || true" - - # Timezone setup - if [[ -z "${tz:-}" ]]; then - tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC") + # Devuan templates don't include locales package by default - install it first + if [ "$var_os" == "devuan" ]; then + pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y locales >/dev/null" || true fi + + # Only configure locale if locale.gen exists (some minimal templates don't have it) + if pct exec "$CTID" -- test -f /etc/locale.gen 2>/dev/null; then + pct exec "$CTID" -- bash -c "sed -i \"/$LANG/ s/^# //\" /etc/locale.gen" + pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ + echo LANG=\$locale_line >/etc/default/locale && \ + locale-gen >/dev/null && \ + export LANG=\$locale_line" + fi + + if [[ -z "${tz:-}" ]]; then + tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC") + fi + [[ "${tz:-}" == Etc/* ]] && tz="UTC" # Normalize Etc/* to UTC for container setup + if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then + # Set timezone using symlink (Debian 13+ compatible) + # Create /etc/timezone for backwards compatibility with older scripts pct exec "$CTID" -- bash -c "tz='$tz'; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true" else msg_warn "Skipping timezone setup – zone '$tz' not found in container" fi - # Core dependencies pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || { msg_error "apt-get base packages installation failed" exit 1 } - ;; - - fedora | rockylinux | almalinux | centos) - # RHEL-based: Fedora, Rocky, AlmaLinux, CentOS - # Detect OS major version for EL10+ compatibility (DNF 5, different packages) - local rhel_version - rhel_version=$(pct exec "$CTID" -- bash -c "grep -oP '(?<=VERSION_ID=\")[0-9]+' /etc/os-release 2>/dev/null || echo 9") - - # First run makecache to ensure repos are ready (critical for fresh templates) - msg_info "Initializing package manager (this may take a moment)..." - if ! pct exec "$CTID" -- bash -c "dnf makecache --refresh 2>&1 || yum makecache 2>&1" >/dev/null 2>&1; then - msg_warn "Package cache update had issues, continuing anyway..." - fi - - # Build package list - EL10+ may not have glibc-langpack-en in same form - local rhel_packages="curl sudo mc jq which tar procps-ng ncurses" - if [[ "$rhel_version" -lt 10 ]]; then - rhel_packages="$rhel_packages glibc-langpack-en" - else - # EL10 uses glibc-all-langpacks or langpacks-en - rhel_packages="$rhel_packages langpacks-en glibc-all-langpacks" - fi - - # Install base packages with better error handling - local install_log="/tmp/dnf_install_${CTID}.log" - if ! pct exec "$CTID" -- bash -c "dnf install -y $rhel_packages 2>&1 | tee $install_log; exit \${PIPESTATUS[0]}" >/dev/null 2>&1; then - # Check if it's just missing optional packages - if pct exec "$CTID" -- bash -c "rpm -q curl sudo mc jq which tar procps-ng" >/dev/null 2>&1; then - msg_warn "Some optional packages may have failed, but core packages installed" - else - # Real failure - try minimal install - msg_warn "Full package install failed, trying minimal set..." - if ! pct exec "$CTID" -- bash -c "dnf install -y curl sudo jq which tar 2>&1" >/dev/null 2>&1; then - msg_error "dnf/yum base packages installation failed" - pct exec "$CTID" -- bash -c "cat $install_log 2>/dev/null" || true - exit 1 - fi - fi - fi - - # Set locale for RHEL-based systems - pct exec "$CTID" -- bash -c "localectl set-locale LANG=en_US.UTF-8 2>/dev/null || echo 'LANG=en_US.UTF-8' > /etc/locale.conf" || true - - # Timezone setup for RHEL - if [[ -z "${tz:-}" ]]; then - tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC") - fi - [[ "${tz:-}" == Etc/* ]] && tz="UTC" - if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then - pct exec "$CTID" -- bash -c "timedatectl set-timezone '$tz' 2>/dev/null || ln -sf '/usr/share/zoneinfo/$tz' /etc/localtime" || true - fi - ;; - - opensuse) - # openSUSE - special handling for terminal/locale issues - # Use --gpg-auto-import-keys to avoid interactive prompts that cause hangs - msg_info "Initializing package manager for openSUSE..." - pct exec "$CTID" -- bash -c "zypper --gpg-auto-import-keys --non-interactive refresh 2>&1" >/dev/null 2>&1 || true - - # Install packages - ncurses and terminfo are CRITICAL for terminal to work - if ! pct exec "$CTID" -- bash -c "zypper --gpg-auto-import-keys --non-interactive install -y curl sudo mc jq glibc-locale ncurses terminfo-base 2>&1" >/dev/null 2>&1; then - # Try without glibc-locale - if ! pct exec "$CTID" -- bash -c "zypper --gpg-auto-import-keys --non-interactive install -y curl sudo mc jq ncurses terminfo-base 2>&1" >/dev/null 2>&1; then - msg_error "zypper base packages installation failed" - exit 1 - fi - fi - - # Fix 'unknown terminal type' error - set TERM in multiple places - pct exec "$CTID" -- bash -c "localectl set-locale LANG=en_US.UTF-8 2>/dev/null || echo 'LANG=en_US.UTF-8' > /etc/locale.conf" || true - - # Set TERM globally for all users - pct exec "$CTID" -- bash -c "cat > /etc/profile.d/term.sh << 'EOFTERM' -# Fix terminal type for LXC containers -if [ -z \"\$TERM\" ] || [ \"\$TERM\" = \"dumb\" ] || [ \"\$TERM\" = \"-\" ]; then - export TERM=xterm-256color -fi -EOFTERM -chmod +x /etc/profile.d/term.sh" || true - - # Also set in /etc/environment for non-login shells - pct exec "$CTID" -- bash -c "grep -q '^TERM=' /etc/environment 2>/dev/null || echo 'TERM=xterm-256color' >> /etc/environment" || true - ;; - - gentoo) - # Gentoo - OpenRC based, emerge is slow - # Use emerge-webrsync (faster, uses http instead of rsync) - msg_info "Syncing Gentoo portage via webrsync (faster than rsync)..." - pct exec "$CTID" -- bash -c "emerge-webrsync 2>&1" >/dev/null 2>&1 || { - msg_warn "emerge-webrsync failed, trying emerge --sync..." - pct exec "$CTID" -- bash -c "emerge --sync 2>&1" >/dev/null 2>&1 || true - } - - # Install curl FIRST - it's required for install.func to work - msg_info "Installing essential packages for Gentoo..." - if ! pct exec "$CTID" -- bash -c "emerge --quiet --noreplace net-misc/curl 2>&1" >/dev/null 2>&1; then - msg_error "Failed to install curl on Gentoo - this is required" - exit 1 - fi - - # Install remaining packages - pct exec "$CTID" -- bash -c "emerge --quiet --noreplace app-misc/jq app-misc/mc sys-libs/ncurses 2>&1" >/dev/null 2>&1 || { - msg_warn "Some Gentoo packages may need manual setup" - } - - # Set TERM for Gentoo - pct exec "$CTID" -- bash -c "echo 'export TERM=xterm-256color' >> /etc/profile.d/term.sh && chmod +x /etc/profile.d/term.sh" || true - ;; - - openeuler) - # openEuler (RHEL-compatible, uses DNF) - # Note: Template was patched with /etc/redhat-release in create_container - msg_info "Initializing package manager for openEuler..." - pct exec "$CTID" -- bash -c "dnf makecache --refresh 2>&1" >/dev/null 2>&1 || true - - # openEuler package names may differ from RHEL - local euler_packages="curl sudo mc jq procps-ng ncurses" - if ! pct exec "$CTID" -- bash -c "dnf install -y $euler_packages 2>&1" >/dev/null 2>&1; then - # Try without procps-ng (might be just 'procps' in openEuler) - if ! pct exec "$CTID" -- bash -c "dnf install -y curl sudo mc jq ncurses 2>&1" >/dev/null 2>&1; then - msg_error "dnf base packages installation failed" - exit 1 - fi - fi - # Set locale - pct exec "$CTID" -- bash -c "echo 'LANG=en_US.UTF-8' > /etc/locale.conf" || true - ;; - - *) - msg_warn "Unknown OS '$var_os' - skipping core dependency installation" - ;; - esac + fi msg_ok "Customized LXC Container" @@ -3977,12 +3922,7 @@ chmod +x /etc/profile.d/term.sh" || true install_ssh_keys_into_ct # Run application installer - # NOTE: We disable error handling here because: - # 1. Container errors are caught by error_handler INSIDE container - # 2. Container creates flag file with exit code - # 3. We read flag file and handle cleanup manually below - # 4. We DON'T want host error_handler to fire for lxc-attach command itself - + # Disable error trap - container errors are handled internally via flag file set +Eeuo pipefail # Disable ALL error handling temporarily trap - ERR # Remove ERR trap completely @@ -4052,35 +3992,129 @@ chmod +x /etc/profile.d/term.sh" || true exit $install_exit_code fi - # Report failure to API before container cleanup - post_update_to_api "failed" "$install_exit_code" - # Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner) echo "" - echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" + + # Detect error type for smart recovery options + local is_oom=false + local error_explanation="" + if declare -f explain_exit_code >/dev/null 2>&1; then + error_explanation="$(explain_exit_code "$install_exit_code")" + fi + + # OOM detection: exit codes 134 (SIGABRT/heap), 137 (SIGKILL/OOM), 243 (Node.js heap) + if [[ $install_exit_code -eq 134 || $install_exit_code -eq 137 || $install_exit_code -eq 243 ]]; then + is_oom=true + fi + + # Show error explanation if available + if [[ -n "$error_explanation" ]]; then + echo -e "${TAB}${RD}Error: ${error_explanation}${CL}" + echo "" + fi + + # Build recovery menu based on error type + echo -e "${YW}What would you like to do?${CL}" + echo "" + echo -e " ${GN}1)${CL} Remove container and exit" + echo -e " ${GN}2)${CL} Keep container for debugging" + echo -e " ${GN}3)${CL} Retry with verbose mode" + if [[ "$is_oom" == true ]]; then + local new_ram=$((RAM_SIZE * 3 / 2)) + local new_cpu=$((CORE_COUNT + 1)) + echo -e " ${GN}4)${CL} Retry with more resources (RAM: ${RAM_SIZE}→${new_ram} MiB, CPU: ${CORE_COUNT}→${new_cpu} cores)" + fi + echo "" + echo -en "${YW}Select option [1-$([[ "$is_oom" == true ]] && echo "4" || echo "3")] (default: 1, auto-remove in 60s): ${CL}" if read -t 60 -r response; then - if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then + case "${response:-1}" in + 1) # Remove container echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" pct stop "$CTID" &>/dev/null || true pct destroy "$CTID" &>/dev/null || true echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" - elif [[ "$response" =~ ^[Nn]$ ]]; then + ;; + 2) echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}" - # Dev mode: Setup MOTD/SSH for debugging access to broken container if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}" if pct exec "$CTID" -- bash -c " - source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func) - declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true - " >/dev/null 2>&1; then + source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func) + declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true + " >/dev/null 2>&1; then local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}" fi fi - fi + exit $install_exit_code + ;; + 3) + # Retry with verbose mode + echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild...${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" + echo "" + # Get new container ID + local old_ctid="$CTID" + export CTID=$(get_valid_container_id "$CTID") + export VERBOSE="yes" + export var_verbose="yes" + + # Show rebuild summary + echo -e "${YW}Rebuilding with preserved settings:${CL}" + echo -e " Container ID: ${old_ctid} → ${CTID}" + echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB" + echo -e " Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}" + echo -e " Verbose: ${GN}enabled${CL}" + echo "" + msg_info "Restarting installation..." + # Re-run build_container + build_container + return $? + ;; + 4) + if [[ "$is_oom" == true ]]; then + # Retry with more resources + echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more resources...${CL}" + pct stop "$CTID" &>/dev/null || true + pct destroy "$CTID" &>/dev/null || true + echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" + echo "" + # Get new container ID and increase resources + local old_ctid="$CTID" + local old_ram="$RAM_SIZE" + local old_cpu="$CORE_COUNT" + export CTID=$(get_valid_container_id "$CTID") + export RAM_SIZE=$((RAM_SIZE * 3 / 2)) + export CORE_COUNT=$((CORE_COUNT + 1)) + export var_ram="$RAM_SIZE" + export var_cpu="$CORE_COUNT" + + # Show rebuild summary + echo -e "${YW}Rebuilding with increased resources:${CL}" + echo -e " Container ID: ${old_ctid} → ${CTID}" + echo -e " RAM: ${old_ram} → ${GN}${RAM_SIZE}${CL} MiB (+50%)" + echo -e " CPU: ${old_cpu} → ${GN}${CORE_COUNT}${CL} cores (+1)" + echo -e " Disk: ${DISK_SIZE} GB | Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}" + echo "" + msg_info "Restarting installation..." + # Re-run build_container + build_container + return $? + else + echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}" + exit $install_exit_code + fi + ;; + *) + echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}" + exit $install_exit_code + ;; + esac else # Timeout - auto-remove echo -e "\n${YW}No response - auto-removing container${CL}" @@ -4100,38 +4134,24 @@ destroy_lxc() { return 1 fi - # Abbruch bei Ctrl-C / Ctrl-D / ESC + # Abort on Ctrl-C / Ctrl-D / ESC trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT - local prompt - if ! read -rp "Remove this Container? " prompt; then - # read gibt != 0 zurück bei Ctrl-D/ESC - msg_error "Aborted input (Ctrl-D/ESC)" - return 130 - fi - - case "${prompt,,}" in - y | yes) + if prompt_confirm "Remove this Container?" "n" 60; then if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then msg_ok "Removed Container $CT_ID" else msg_error "Failed to remove Container $CT_ID" return 1 fi - ;; - "" | n | no) + else msg_custom "ℹ️" "${BL}" "Container was not removed." - ;; - *) - msg_warn "Invalid response. Container was not removed." - ;; - esac + fi } # ------------------------------------------------------------------------------ # Storage discovery / selection helpers # ------------------------------------------------------------------------------ -# ===== Storage discovery / selection helpers (ported from create_lxc.sh) ===== resolve_storage_preselect() { local class="$1" preselect="$2" required_content="" case "$class" in @@ -4207,15 +4227,14 @@ fix_gpu_gids() { # For privileged containers: also fix permissions inside container if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " + pct exec "$CTID" -- sh -c " if [ -d /dev/dri ]; then for dev in /dev/dri/*; do if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi + case \"\$dev\" in + *renderD*) chgrp ${render_gid} \"\$dev\" 2>/dev/null || true ;; + *) chgrp ${video_gid} \"\$dev\" 2>/dev/null || true ;; + esac chmod 660 \"\$dev\" 2>/dev/null || true fi done @@ -4290,18 +4309,13 @@ select_storage() { if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" STORAGE_INFO="${MENU[1]}" - - # Validate storage space for auto-picked container storage - if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then - validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes" - fi return 0 fi local WIDTH=$((COL_WIDTH + 42)) while true; do local DISPLAY_SELECTED - DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ + DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "Storage Pools" \ --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \ 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; } @@ -4322,7 +4336,9 @@ select_storage() { # Validate storage space for container storage if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes" + # Continue even if validation fails - user was warned fi + return 0 done } @@ -4385,6 +4401,18 @@ validate_storage_space() { return 0 } +# ============================================================================== +# SECTION 8: CONTAINER CREATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# create_lxc_container() +# +# - Main function for creating LXC containers +# - Handles all phases: validation, template discovery, container creation, +# network config, storage, etc. +# - Extensive error checking with detailed exit codes +# ------------------------------------------------------------------------------ create_lxc_container() { # ------------------------------------------------------------------------------ # Optional verbose mode (debug tracing) @@ -4435,9 +4463,7 @@ create_lxc_container() { echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}" echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}" echo - read -rp "Do you want to upgrade now? [y/N] " _ans - case "${_ans,,}" in - y | yes) + if prompt_confirm "Do you want to upgrade now?" "n" 60; then msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)" if $STD apt-get update && $STD apt-get install -y --only-upgrade pve-container lxc-pve; then msg_ok "LXC stack upgraded." @@ -4456,9 +4482,9 @@ create_lxc_container() { msg_error "Upgrade failed. Please check APT output." return 3 fi - ;; - *) return 2 ;; - esac + else + return 2 + fi } # ------------------------------------------------------------------------------ @@ -4556,14 +4582,6 @@ create_lxc_container() { fi msg_ok "Template storage '$TEMPLATE_STORAGE' validated" - # Free space check - STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') - REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024)) - [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || { - msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G." - exit 214 - } - # Cluster quorum (if cluster) if [[ -f /etc/pve/corosync.conf ]]; then msg_info "Checking cluster quorum" @@ -4576,81 +4594,52 @@ create_lxc_container() { # ------------------------------------------------------------------------------ # Template discovery & validation - # Supported OS types (pveam available): alpine, almalinux, centos, debian, - # devuan, fedora, gentoo, openeuler, opensuse, rockylinux, ubuntu - # Template naming conventions: - # - Debian/Ubuntu/Devuan: --standard__.tar.zst - # - Alpine/Fedora/Rocky/CentOS/AlmaLinux/openEuler: --default__.tar.xz - # - Gentoo: gentoo-current-openrc__.tar.xz (note: underscore before date!) - # - openSUSE: opensuse--default__.tar.xz - # - CentOS: centos--stream-default__.tar.xz (note: stream in name) # ------------------------------------------------------------------------------ TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" case "$PCT_OSTYPE" in - debian | ubuntu | devuan) TEMPLATE_PATTERN="-standard_" ;; - alpine | fedora | rockylinux | almalinux | openeuler) TEMPLATE_PATTERN="-default_" ;; - centos) TEMPLATE_PATTERN="-stream-default_" ;; - gentoo) TEMPLATE_PATTERN="-openrc_" ;; # Pattern: gentoo-current-openrc_ (underscore!) - opensuse) TEMPLATE_PATTERN="-default_" ;; + debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;; + alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;; *) TEMPLATE_PATTERN="" ;; esac msg_info "Searching for template '$TEMPLATE_SEARCH'" - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" + # Initialize variables + ONLINE_TEMPLATE="" + ONLINE_TEMPLATES=() + # Step 1: Check local templates first (instant) mapfile -t LOCAL_TEMPLATES < <( pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + awk -v search="${TEMPLATE_SEARCH}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | sed 's|.*/||' | sort -t - -k 2 -V ) - # Update template catalog with timeout to prevent hangs on slow networks - if command -v timeout &>/dev/null; then - if ! timeout 30 pveam update >/dev/null 2>&1; then - msg_warn "Template catalog update timed out or failed (continuing with cached data)" - fi - else - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - fi - - msg_ok "Template search completed" - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - + # Step 2: If local template found, use it immediately (skip pveam update) if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then TEMPLATE="${LOCAL_TEMPLATES[-1]}" TEMPLATE_SOURCE="local" + msg_ok "Template search completed" else + # Step 3: No local template - need to check online (this may be slow) + msg_info "No local template found, checking online catalog..." + + # Update catalog with timeout to prevent long hangs + if command -v timeout &>/dev/null; then + if ! timeout 30 pveam update >/dev/null 2>&1; then + msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose." + fi + else + pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)" + fi + + ONLINE_TEMPLATES=() + mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^${TEMPLATE_SEARCH}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + TEMPLATE="$ONLINE_TEMPLATE" TEMPLATE_SOURCE="online" + msg_ok "Template search completed" fi # If still no template, try to find alternatives @@ -4659,63 +4648,40 @@ create_lxc_container() { echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." # Get all available versions for this OS type - # Special handling for Gentoo which uses 'current' instead of numeric version - if [[ "$PCT_OSTYPE" == "gentoo" ]]; then - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk '{print $2}' | - grep "^gentoo-" | - sed -E 's/gentoo-([^-]+)-.*/\1/' | - sort -u 2>/dev/null || sort -u - ) - else - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk '{print $2}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - grep -E '^[0-9]' | - sort -u -V 2>/dev/null || sort -u - ) - fi + AVAILABLE_VERSIONS=() + mapfile -t AVAILABLE_VERSIONS < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk -F'\t' '{print $1}' | + grep "^${PCT_OSTYPE}-" | + sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | + sort -u -V 2>/dev/null + ) if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice + # Use prompt_select for version selection (supports unattended mode) + local selected_version + selected_version=$(prompt_select "Select ${PCT_OSTYPE} version:" 1 60 "${AVAILABLE_VERSIONS[@]}") - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" + # prompt_select always returns a value (uses default in unattended mode) + PCT_OSVERSION="$selected_version" + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" + ONLINE_TEMPLATES=() + mapfile -t ONLINE_TEMPLATES < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk '{print $2}' | + grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" | + sort -t - -k 2 -V 2>/dev/null || true + ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi + if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${ONLINE_TEMPLATES[-1]}" + TEMPLATE_SOURCE="online" else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 0 + msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" + exit 225 fi else msg_error "No ${PCT_OSTYPE} templates available at all" @@ -4723,9 +4689,6 @@ create_lxc_container() { fi fi - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" if [[ -z "$TEMPLATE_PATH" ]]; then TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) @@ -4751,65 +4714,56 @@ create_lxc_container() { ) if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done + # Use prompt_select for version selection (supports unattended mode) + local selected_version + selected_version=$(prompt_select "Select ${PCT_OSTYPE} version:" 1 60 "${AVAILABLE_VERSIONS[@]}") - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice + # prompt_select always returns a value (uses default in unattended mode) + export var_version="$selected_version" + export PCT_OSVERSION="$var_version" + msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" + # Retry template search with new version + TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" + mapfile -t LOCAL_TEMPLATES < <( + pveam list "$TEMPLATE_STORAGE" 2>/dev/null | + awk -v search="${TEMPLATE_SEARCH}-" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | + sed 's|.*/||' | sort -t - -k 2 -V + ) + mapfile -t ONLINE_TEMPLATES < <( + pveam available -section system 2>/dev/null | + grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | + awk '{print $2}' | + grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" | + sort -t - -k 2 -V 2>/dev/null || true + ) + ONLINE_TEMPLATE="" + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } + if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then + TEMPLATE="${LOCAL_TEMPLATES[-1]}" + TEMPLATE_SOURCE="local" else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 1 + TEMPLATE="$ONLINE_TEMPLATE" + TEMPLATE_SOURCE="online" fi + + TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" + if [[ -z "$TEMPLATE_PATH" ]]; then + TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) + [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" + fi + + # If we still don't have a path but have a valid template name, construct it + if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then + TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" + fi + + [[ -n "$TEMPLATE_PATH" ]] || { + msg_error "Template still not found after version change" + exit 220 + } else msg_error "No ${PCT_OSTYPE} templates available" exit 220 @@ -4891,7 +4845,6 @@ create_lxc_container() { if [[ "$PCT_OSTYPE" == "debian" ]]; then OSVER="$(parse_template_osver "$TEMPLATE")" if [[ -n "$OSVER" ]]; then - # Proactive, aber ohne Abbruch – nur Angebot offer_lxc_stack_upgrade_and_maybe_retry "no" || true fi fi @@ -4912,7 +4865,7 @@ create_lxc_container() { -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}" fi - # Lock by template file (avoid concurrent downloads/creates) + # Lock by template file (avoid concurrent template downloads/validation) lockfile="/tmp/template.${TEMPLATE}.lock" # Cleanup stale lock files (older than 1 hour - likely from crashed processes) @@ -4946,84 +4899,54 @@ create_lxc_container() { LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" - # ------------------------------------------------------------------------------ - # openEuler Template Patch: Create /etc/redhat-release inside template - # PVE's post_create_hook expects this file for RHEL-family OS detection - # Without it, container creation fails with "error in setup task" - # ------------------------------------------------------------------------------ - if [[ "${var_os:-}" == "openeuler" ]]; then - msg_info "Patching openEuler template for PVE compatibility..." - local TEMP_EXTRACT_DIR="/tmp/openeuler_template_patch_$$" - local PATCHED_TEMPLATE="${TEMPLATE_PATH%.tar.xz}_patched.tar.xz" - - # Only patch if not already patched - if [[ ! -f "$PATCHED_TEMPLATE" ]]; then - mkdir -p "$TEMP_EXTRACT_DIR" - - # Extract template - if tar -xf "$TEMPLATE_PATH" -C "$TEMP_EXTRACT_DIR" 2>/dev/null; then - # Create /etc/redhat-release if it doesn't exist - if [[ ! -f "$TEMP_EXTRACT_DIR/etc/redhat-release" ]]; then - echo "openEuler release ${var_version:-25.03}" >"$TEMP_EXTRACT_DIR/etc/redhat-release" - fi - - # Repack template - if tar -cJf "$PATCHED_TEMPLATE" -C "$TEMP_EXTRACT_DIR" . 2>/dev/null; then - # Replace original with patched version - mv "$PATCHED_TEMPLATE" "$TEMPLATE_PATH" - msg_ok "openEuler template patched successfully" - else - msg_warn "Failed to repack template, trying without patch..." - fi - else - msg_warn "Failed to extract template for patching, trying without patch..." - fi - - rm -rf "$TEMP_EXTRACT_DIR" + # Validate template before pct create (while holding lock) + if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH" 2>/dev/null || echo 0)" -lt 1000000 ]]; then + msg_info "Template file missing or too small – downloading" + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template downloaded" + elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_info "Template appears corrupted – re-downloading" + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template re-downloaded" + else + msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." fi fi - # # DEBUG: Show the actual command that will be executed - # echo "[DEBUG] ===== PCT CREATE COMMAND DETAILS =====" - # echo "[DEBUG] CTID: $CTID" - # echo "[DEBUG] Template: ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" - # echo "[DEBUG] PCT_OPTIONS (will be word-split):" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Full command line:" - # echo " pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - # echo "[DEBUG] ========================================" + # Release lock after template validation - pct create has its own internal locking + exec 9>&- msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" msg_debug "Logfile: $LOGFILE" # First attempt (PCT_OPTIONS is a multi-line string, use it directly) if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then - msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Validating template..." + msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..." - # Validate template file - if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - msg_warn "Template file too small or missing – re-downloading." + # Check if template issue - retry with fresh download + if grep -qiE 'unable to open|corrupt|invalid' "$LOGFILE"; then + msg_info "Template may be corrupted – re-downloading" rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - else - msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." - fi + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template re-downloaded" fi # Retry after repair if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then # Fallback to local storage if not already on local if [[ "$TEMPLATE_STORAGE" != "local" ]]; then - msg_info "Retrying container creation with fallback to local storage..." + msg_info "Retrying container creation with fallback to local storage" LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then - msg_info "Downloading template to local..." + msg_ok "Trying local storage fallback" + msg_info "Downloading template to local" pveam download local "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template downloaded to local" + else + msg_ok "Trying local storage fallback" fi if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then # Local fallback also failed - check for LXC stack version issue @@ -5147,15 +5070,15 @@ description() { - GitHub + Git - Discussions + Discussions - Issues + Issues EOF diff --git a/misc/core.func b/misc/core.func index 4506b101b..e14ba3c22 100644 --- a/misc/core.func +++ b/misc/core.func @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG -# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/LICENSE +# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/LICENSE # ============================================================================== # CORE FUNCTIONS - LXC CONTAINER UTILITIES @@ -123,6 +123,7 @@ icons() { CREATING="${TAB}🚀${TAB}${CL}" ADVANCED="${TAB}🧩${TAB}${CL}" FUSE="${TAB}🗂️${TAB}${CL}" + GPU="${TAB}🎮${TAB}${CL}" HOURGLASS="${TAB}⏳${TAB}" } @@ -551,11 +552,8 @@ msg_info() { if ! declare -p MSG_INFO_SHOWN &>/dev/null || ! declare -A MSG_INFO_SHOWN &>/dev/null; then declare -gA MSG_INFO_SHOWN=() fi - # Sanitize message for use as associative array key (remove ANSI codes and special chars) - local sanitized_msg - sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g') - [[ -n "${MSG_INFO_SHOWN["$sanitized_msg"]+x}" ]] && return - MSG_INFO_SHOWN["$sanitized_msg"]=1 + [[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return + MSG_INFO_SHOWN["$msg"]=1 stop_spinner SPINNER_MSG="$msg" @@ -600,7 +598,6 @@ msg_ok() { stop_spinner clear_line echo -e "$CM ${GN}${msg}${CL}" - # Sanitize message for use as associative array key (remove ANSI codes and special chars) local sanitized_msg sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g') unset 'MSG_INFO_SHOWN['"$sanitized_msg"']' 2>/dev/null || true @@ -717,7 +714,7 @@ exit_script() { # ------------------------------------------------------------------------------ get_header() { local app_name=$(echo "${APP,,}" | tr -d ' ') - local app_type=${APP_TYPE:-ct} # Default zu 'ct' falls nicht gesetzt + local app_type=${APP_TYPE:-ct} # Default to 'ct' if not set local header_url="https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/${app_type}/headers/${app_name}" local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}" @@ -813,6 +810,562 @@ is_verbose_mode() { [[ "$verbose" != "no" || ! -t 2 ]] } +# ------------------------------------------------------------------------------ +# is_unattended() +# +# - Detects if script is running in unattended/non-interactive mode +# - Checks MODE variable first (primary method) +# - Falls back to legacy flags (PHS_SILENT, var_unattended) +# - Returns 0 (true) if unattended, 1 (false) otherwise +# - Used by prompt functions to auto-apply defaults +# +# Modes that are unattended: +# - default (1) : Use script defaults, no prompts +# - mydefaults (3) : Use user's default.vars, no prompts +# - appdefaults (4) : Use app-specific defaults, no prompts +# +# Modes that are interactive: +# - advanced (2) : Full wizard with all options +# +# Note: Even in advanced mode, install scripts run unattended because +# all values are already collected during the wizard phase. +# ------------------------------------------------------------------------------ +is_unattended() { + # Primary: Check MODE variable (case-insensitive) + local mode="${MODE:-${mode:-}}" + mode="${mode,,}" # lowercase + + case "$mode" in + default|1) + return 0 + ;; + mydefaults|userdefaults|3) + return 0 + ;; + appdefaults|4) + return 0 + ;; + advanced|2) + # Advanced mode is interactive ONLY during wizard + # Inside container (install scripts), it should be unattended + # Check if we're inside a container (no pveversion command) + if ! command -v pveversion &>/dev/null; then + # We're inside the container - all values already collected + return 0 + fi + # On host during wizard - interactive + return 1 + ;; + esac + + # Legacy fallbacks for compatibility + [[ "${PHS_SILENT:-0}" == "1" ]] && return 0 + [[ "${var_unattended:-}" =~ ^(yes|true|1)$ ]] && return 0 + [[ "${UNATTENDED:-}" =~ ^(yes|true|1)$ ]] && return 0 + + # No TTY available = unattended + [[ ! -t 0 ]] && return 0 + + # Default: interactive + return 1 +} + +# ------------------------------------------------------------------------------ +# show_missing_values_warning() +# +# - Displays a summary of required values that used fallback defaults +# - Should be called at the end of install scripts +# - Only shows warning if MISSING_REQUIRED_VALUES array has entries +# - Provides clear guidance on what needs manual configuration +# +# Global: +# MISSING_REQUIRED_VALUES - Array of variable names that need configuration +# +# Example: +# # At end of install script: +# show_missing_values_warning +# ------------------------------------------------------------------------------ +show_missing_values_warning() { + if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then + echo "" + echo -e "${YW}╔════════════════════════════════════════════════════════════╗${CL}" + echo -e "${YW}║ ⚠️ MANUAL CONFIGURATION REQUIRED ║${CL}" + echo -e "${YW}╠════════════════════════════════════════════════════════════╣${CL}" + echo -e "${YW}║ The following values were not provided and need to be ║${CL}" + echo -e "${YW}║ configured manually for the service to work properly: ║${CL}" + echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}" + for val in "${MISSING_REQUIRED_VALUES[@]}"; do + printf "${YW}║${CL} • %-56s ${YW}║${CL}\n" "$val" + done + echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}" + echo -e "${YW}║ Check the service configuration files or environment ║${CL}" + echo -e "${YW}║ variables and update the placeholder values. ║${CL}" + echo -e "${YW}╚════════════════════════════════════════════════════════════╝${CL}" + echo "" + return 1 + fi + return 0 +} + +# ------------------------------------------------------------------------------ +# prompt_confirm() +# +# - Prompts user for yes/no confirmation with timeout and unattended support +# - In unattended mode: immediately returns default value +# - In interactive mode: waits for user input with configurable timeout +# - After timeout: auto-applies default value +# +# Arguments: +# $1 - Prompt message (required) +# $2 - Default value: "y" or "n" (optional, default: "n") +# $3 - Timeout in seconds (optional, default: 60) +# +# Returns: +# 0 - User confirmed (yes) +# 1 - User declined (no) or timeout with default "n" +# +# Example: +# if prompt_confirm "Proceed with installation?" "y" 30; then +# echo "Installing..." +# fi +# +# # Unattended: prompt_confirm will use default without waiting +# var_unattended=yes +# prompt_confirm "Delete files?" "n" && echo "Deleting" || echo "Skipped" +# ------------------------------------------------------------------------------ +prompt_confirm() { + local message="${1:-Confirm?}" + local default="${2:-n}" + local timeout="${3:-60}" + local response + + # Normalize default to lowercase + default="${default,,}" + [[ "$default" != "y" ]] && default="n" + + # Build prompt hint + local hint + if [[ "$default" == "y" ]]; then + hint="[Y/n]" + else + hint="[y/N]" + fi + + # Unattended mode: apply default immediately + if is_unattended; then + if [[ "$default" == "y" ]]; then + return 0 + else + return 1 + fi + fi + + # Check if running in a TTY + if [[ ! -t 0 ]]; then + # Not a TTY, use default + if [[ "$default" == "y" ]]; then + return 0 + else + return 1 + fi + fi + + # Interactive prompt with timeout + echo -en "${YW}${message} ${hint} (auto-${default} in ${timeout}s): ${CL}" + + if read -t "$timeout" -r response; then + # User provided input + response="${response,,}" # lowercase + case "$response" in + y|yes) + return 0 + ;; + n|no) + return 1 + ;; + "") + # Empty response, use default + if [[ "$default" == "y" ]]; then + return 0 + else + return 1 + fi + ;; + *) + # Invalid input, use default + echo -e "${YW}Invalid response, using default: ${default}${CL}" + if [[ "$default" == "y" ]]; then + return 0 + else + return 1 + fi + ;; + esac + else + # Timeout occurred + echo "" # Newline after timeout + echo -e "${YW}Timeout - auto-selecting: ${default}${CL}" + if [[ "$default" == "y" ]]; then + return 0 + else + return 1 + fi + fi +} + +# ------------------------------------------------------------------------------ +# prompt_input() +# +# - Prompts user for text input with timeout and unattended support +# - In unattended mode: immediately returns default value +# - In interactive mode: waits for user input with configurable timeout +# - After timeout: auto-applies default value +# +# Arguments: +# $1 - Prompt message (required) +# $2 - Default value (optional, default: "") +# $3 - Timeout in seconds (optional, default: 60) +# +# Output: +# Prints the user input or default value to stdout +# +# Example: +# username=$(prompt_input "Enter username:" "admin" 30) +# echo "Using username: $username" +# +# # With validation +# while true; do +# port=$(prompt_input "Enter port:" "8080" 30) +# [[ "$port" =~ ^[0-9]+$ ]] && break +# echo "Invalid port number" +# done +# ------------------------------------------------------------------------------ +prompt_input() { + local message="${1:-Enter value:}" + local default="${2:-}" + local timeout="${3:-60}" + local response + + # Build display default hint + local hint="" + [[ -n "$default" ]] && hint=" (default: ${default})" + + # Unattended mode: return default immediately + if is_unattended; then + echo "$default" + return 0 + fi + + # Check if running in a TTY + if [[ ! -t 0 ]]; then + # Not a TTY, use default + echo "$default" + return 0 + fi + + # Interactive prompt with timeout + echo -en "${YW}${message}${hint} (auto-default in ${timeout}s): ${CL}" >&2 + + if read -t "$timeout" -r response; then + # User provided input (or pressed Enter for empty) + if [[ -n "$response" ]]; then + echo "$response" + else + echo "$default" + fi + else + # Timeout occurred + echo "" >&2 # Newline after timeout + echo -e "${YW}Timeout - using default: ${default}${CL}" >&2 + echo "$default" + fi +} + +# ------------------------------------------------------------------------------ +# prompt_input_required() +# +# - Prompts user for REQUIRED text input with fallback support +# - In unattended mode: Uses fallback value if no env var set (with warning) +# - In interactive mode: loops until user provides non-empty input +# - Tracks missing required values for end-of-script summary +# +# Arguments: +# $1 - Prompt message (required) +# $2 - Fallback/example value for unattended mode (optional) +# $3 - Timeout in seconds (optional, default: 120) +# $4 - Environment variable name hint for error messages (optional) +# +# Output: +# Prints the user input or fallback value to stdout +# +# Returns: +# 0 - Success (value provided or fallback used) +# 1 - Failed (interactive timeout without input) +# +# Global: +# MISSING_REQUIRED_VALUES - Array tracking fields that used fallbacks +# +# Example: +# # With fallback - script continues even in unattended mode +# token=$(prompt_input_required "Enter API Token:" "YOUR_TOKEN_HERE" 60 "var_api_token") +# +# # Check at end of script if any values need manual configuration +# if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then +# msg_warn "Please configure: ${MISSING_REQUIRED_VALUES[*]}" +# fi +# ------------------------------------------------------------------------------ +# Global array to track missing required values +declare -g -a MISSING_REQUIRED_VALUES=() + +prompt_input_required() { + local message="${1:-Enter required value:}" + local fallback="${2:-CHANGE_ME}" + local timeout="${3:-120}" + local env_var_hint="${4:-}" + local response="" + + # Check if value is already set via environment variable (if hint provided) + if [[ -n "$env_var_hint" ]]; then + local env_value="${!env_var_hint:-}" + if [[ -n "$env_value" ]]; then + echo "$env_value" + return 0 + fi + fi + + # Unattended mode: use fallback with warning + if is_unattended; then + if [[ -n "$env_var_hint" ]]; then + echo -e "${YW}⚠ Required value '${env_var_hint}' not set - using fallback: ${fallback}${CL}" >&2 + MISSING_REQUIRED_VALUES+=("$env_var_hint") + else + echo -e "${YW}⚠ Required value not provided - using fallback: ${fallback}${CL}" >&2 + MISSING_REQUIRED_VALUES+=("(unnamed)") + fi + echo "$fallback" + return 0 + fi + + # Check if running in a TTY + if [[ ! -t 0 ]]; then + echo -e "${YW}⚠ Not interactive - using fallback: ${fallback}${CL}" >&2 + MISSING_REQUIRED_VALUES+=("${env_var_hint:-unnamed}") + echo "$fallback" + return 0 + fi + + # Interactive prompt - loop until non-empty input or use fallback on timeout + local attempts=0 + while [[ -z "$response" ]]; do + attempts=$((attempts + 1)) + + if [[ $attempts -gt 3 ]]; then + echo -e "${YW}Too many empty inputs - using fallback: ${fallback}${CL}" >&2 + MISSING_REQUIRED_VALUES+=("${env_var_hint:-manual_input}") + echo "$fallback" + return 0 + fi + + echo -en "${YW}${message} (required, timeout ${timeout}s): ${CL}" >&2 + + if read -t "$timeout" -r response; then + if [[ -z "$response" ]]; then + echo -e "${YW}This field is required. Please enter a value. (attempt ${attempts}/3)${CL}" >&2 + fi + else + # Timeout occurred - use fallback + echo "" >&2 + echo -e "${YW}Timeout - using fallback value: ${fallback}${CL}" >&2 + MISSING_REQUIRED_VALUES+=("${env_var_hint:-timeout}") + echo "$fallback" + return 0 + fi + done + + echo "$response" +} + +# ------------------------------------------------------------------------------ +# prompt_select() +# +# - Prompts user to select from a list of options with timeout support +# - In unattended mode: immediately returns default selection +# - In interactive mode: displays numbered menu and waits for choice +# - After timeout: auto-applies default selection +# +# Arguments: +# $1 - Prompt message (required) +# $2 - Default option number, 1-based (optional, default: 1) +# $3 - Timeout in seconds (optional, default: 60) +# $4+ - Options to display (required, at least 2) +# +# Output: +# Prints the selected option value to stdout +# +# Returns: +# 0 - Success +# 1 - No options provided or invalid state +# +# Example: +# choice=$(prompt_select "Select database:" 1 30 "PostgreSQL" "MySQL" "SQLite") +# echo "Selected: $choice" +# +# # With array +# options=("Option A" "Option B" "Option C") +# selected=$(prompt_select "Choose:" 2 60 "${options[@]}") +# ------------------------------------------------------------------------------ +prompt_select() { + local message="${1:-Select option:}" + local default="${2:-1}" + local timeout="${3:-60}" + shift 3 + + local options=("$@") + local num_options=${#options[@]} + + # Validate options + if [[ $num_options -eq 0 ]]; then + echo "" >&2 + return 1 + fi + + # Validate default + if [[ ! "$default" =~ ^[0-9]+$ ]] || [[ "$default" -lt 1 ]] || [[ "$default" -gt "$num_options" ]]; then + default=1 + fi + + # Unattended mode: return default immediately + if is_unattended; then + echo "${options[$((default - 1))]}" + return 0 + fi + + # Check if running in a TTY + if [[ ! -t 0 ]]; then + echo "${options[$((default - 1))]}" + return 0 + fi + + # Display menu + echo -e "${YW}${message}${CL}" >&2 + local i + for i in "${!options[@]}"; do + local num=$((i + 1)) + if [[ $num -eq $default ]]; then + echo -e " ${GN}${num})${CL} ${options[$i]} ${YW}(default)${CL}" >&2 + else + echo -e " ${GN}${num})${CL} ${options[$i]}" >&2 + fi + done + + # Interactive prompt with timeout + echo -en "${YW}Select [1-${num_options}] (auto-select ${default} in ${timeout}s): ${CL}" >&2 + + local response + if read -t "$timeout" -r response; then + if [[ -z "$response" ]]; then + # Empty response, use default + echo "${options[$((default - 1))]}" + elif [[ "$response" =~ ^[0-9]+$ ]] && [[ "$response" -ge 1 ]] && [[ "$response" -le "$num_options" ]]; then + # Valid selection + echo "${options[$((response - 1))]}" + else + # Invalid input, use default + echo -e "${YW}Invalid selection, using default: ${options[$((default - 1))]}${CL}" >&2 + echo "${options[$((default - 1))]}" + fi + else + # Timeout occurred + echo "" >&2 # Newline after timeout + echo -e "${YW}Timeout - auto-selecting: ${options[$((default - 1))]}${CL}" >&2 + echo "${options[$((default - 1))]}" + fi +} + +# ------------------------------------------------------------------------------ +# prompt_password() +# +# - Prompts user for password input with hidden characters +# - In unattended mode: returns default or generates random password +# - Supports auto-generation of secure passwords +# - After timeout: generates random password if allowed +# +# Arguments: +# $1 - Prompt message (required) +# $2 - Default value or "generate" for auto-generation (optional) +# $3 - Timeout in seconds (optional, default: 60) +# $4 - Minimum length for validation (optional, default: 0 = no minimum) +# +# Output: +# Prints the password to stdout +# +# Example: +# password=$(prompt_password "Enter password:" "generate" 30 8) +# echo "Password set" +# +# # Require user input (no default) +# db_pass=$(prompt_password "Database password:" "" 60 12) +# ------------------------------------------------------------------------------ +prompt_password() { + local message="${1:-Enter password:}" + local default="${2:-}" + local timeout="${3:-60}" + local min_length="${4:-0}" + local response + + # Generate random password if requested + local generated="" + if [[ "$default" == "generate" ]]; then + generated=$(openssl rand -base64 16 2>/dev/null | tr -dc 'a-zA-Z0-9' | head -c 16) + [[ -z "$generated" ]] && generated=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16) + default="$generated" + fi + + # Unattended mode: return default immediately + if is_unattended; then + echo "$default" + return 0 + fi + + # Check if running in a TTY + if [[ ! -t 0 ]]; then + echo "$default" + return 0 + fi + + # Build hint + local hint="" + if [[ -n "$generated" ]]; then + hint=" (Enter for auto-generated)" + elif [[ -n "$default" ]]; then + hint=" (Enter for default)" + fi + [[ "$min_length" -gt 0 ]] && hint="${hint} [min ${min_length} chars]" + + # Interactive prompt with timeout (silent input) + echo -en "${YW}${message}${hint} (timeout ${timeout}s): ${CL}" >&2 + + if read -t "$timeout" -rs response; then + echo "" >&2 # Newline after hidden input + if [[ -n "$response" ]]; then + # Validate minimum length + if [[ "$min_length" -gt 0 ]] && [[ ${#response} -lt "$min_length" ]]; then + echo -e "${YW}Password too short (min ${min_length}), using default${CL}" >&2 + echo "$default" + else + echo "$response" + fi + else + echo "$default" + fi + else + # Timeout occurred + echo "" >&2 # Newline after timeout + echo -e "${YW}Timeout - using generated password${CL}" >&2 + echo "$default" + fi +} + # ============================================================================== # SECTION 6: CLEANUP & MAINTENANCE # ============================================================================== @@ -820,71 +1373,64 @@ is_verbose_mode() { # ------------------------------------------------------------------------------ # cleanup_lxc() # -# - Comprehensive cleanup of package managers, caches, and logs -# - Supports Alpine (apk), Debian/Ubuntu (apt), Fedora/Rocky/CentOS (dnf/yum), -# openSUSE (zypper), Gentoo (emerge), and language package managers -# - Cleans: Python (pip/uv), Node.js (npm/yarn/pnpm), Go, Rust, Ruby, PHP -# - Truncates log files and vacuums systemd journal -# - Run at end of container creation to minimize disk usage +# - Cleans package manager and language caches (safe for installs AND updates) +# - Supports Alpine (apk), Debian/Ubuntu (apt), Python, Node.js, Go, Rust, Ruby, PHP +# - Uses fallback error handling to prevent cleanup failures from breaking installs # ------------------------------------------------------------------------------ cleanup_lxc() { msg_info "Cleaning up" - # OS-specific package manager cleanup + if is_alpine; then - $STD apk cache clean 2>/dev/null || true + $STD apk cache clean || true rm -rf /var/cache/apk/* - elif command -v apt &>/dev/null; then - # Debian/Ubuntu/Devuan - $STD apt -y autoremove 2>/dev/null || true - $STD apt -y autoclean 2>/dev/null || true - $STD apt -y clean 2>/dev/null || true - elif command -v dnf &>/dev/null; then - # Fedora/Rocky/AlmaLinux/CentOS 8+ - $STD dnf clean all 2>/dev/null || true - $STD dnf autoremove -y 2>/dev/null || true - elif command -v yum &>/dev/null; then - # CentOS 7/older RHEL - $STD yum clean all 2>/dev/null || true - elif command -v zypper &>/dev/null; then - # openSUSE - $STD zypper clean --all 2>/dev/null || true - elif command -v emerge &>/dev/null; then - # Gentoo - $STD emerge --quiet --depclean 2>/dev/null || true - $STD eclean-dist -d 2>/dev/null || true - $STD eclean-pkg -d 2>/dev/null || true + else + $STD apt -y autoremove 2>/dev/null || msg_warn "apt autoremove failed (non-critical)" + $STD apt -y autoclean 2>/dev/null || msg_warn "apt autoclean failed (non-critical)" + $STD apt -y clean 2>/dev/null || msg_warn "apt clean failed (non-critical)" fi - # Clear temp artifacts (keep sockets/FIFOs; ignore errors) find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true - # Truncate writable log files silently (permission errors ignored) - if command -v truncate >/dev/null 2>&1; then - find /var/log -type f -writable -print0 2>/dev/null | - xargs -0 -n1 truncate -s 0 2>/dev/null || true + # Python + if command -v pip &>/dev/null; then + rm -rf /root/.cache/pip 2>/dev/null || true + fi + if command -v uv &>/dev/null; then + rm -rf /root/.cache/uv 2>/dev/null || true fi - # Node.js npm + # Node.js if command -v npm &>/dev/null; then rm -rf /root/.npm/_cacache /root/.npm/_logs 2>/dev/null || true fi - # Node.js yarn - #if command -v yarn &>/dev/null; then $STD yarn cache clean 2>/dev/null || true; fi - # Node.js pnpm - if command -v pnpm &>/dev/null; then $STD pnpm store prune 2>/dev/null || true; fi - # Go - if command -v go &>/dev/null; then $STD go clean -cache -modcache 2>/dev/null || true; fi - # Rust cargo - if command -v cargo &>/dev/null; then $STD cargo clean 2>/dev/null || true; fi - # Ruby gem - if command -v gem &>/dev/null; then $STD gem cleanup 2>/dev/null || true; fi - # Composer (PHP) - if command -v composer &>/dev/null; then $STD composer clear-cache 2>/dev/null || true; fi - - if command -v journalctl &>/dev/null; then - $STD journalctl --vacuum-time=10m 2>/dev/null || true + if command -v yarn &>/dev/null; then + rm -rf /root/.cache/yarn /root/.yarn/cache 2>/dev/null || true fi + if command -v pnpm &>/dev/null; then + pnpm store prune &>/dev/null || true + fi + + # Go (only build cache, not modules) + if command -v go &>/dev/null; then + $STD go clean -cache 2>/dev/null || true + fi + + # Rust (only registry cache, not build artifacts) + if command -v cargo &>/dev/null; then + rm -rf /root/.cargo/registry/cache /root/.cargo/.package-cache 2>/dev/null || true + fi + + # Ruby + if command -v gem &>/dev/null; then + rm -rf /root/.gem/cache 2>/dev/null || true + fi + + # PHP + if command -v composer &>/dev/null; then + rm -rf /root/.composer/cache 2>/dev/null || true + fi + msg_ok "Cleaned" } @@ -908,15 +1454,13 @@ check_or_create_swap() { msg_error "No active swap detected" - read -p "Do you want to create a swap file? [y/N]: " create_swap - create_swap="${create_swap,,}" # to lowercase - - if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then + if ! prompt_confirm "Do you want to create a swap file?" "n" 60; then msg_info "Skipping swap file creation" return 1 fi - read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb + local swap_size_mb + swap_size_mb=$(prompt_input "Enter swap size in MB (e.g., 2048 for 2GB):" "2048" 60) if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then msg_error "Invalid size input. Aborting." return 1 @@ -954,14 +1498,14 @@ function get_lxc_ip() { get_current_ip() { local ip - # Try direct interface lookup for eth0 FIRST (most reliable for LXC) + # Try direct interface lookup for eth0 FIRST (most reliable for LXC) - IPv4 ip=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1) if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "$ip" return 0 fi - # Fallback: Try hostname -I + # Fallback: Try hostname -I (returns IPv4 first if available) if command -v hostname >/dev/null 2>&1; then ip=$(hostname -I 2>/dev/null | awk '{print $1}') if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then @@ -970,9 +1514,9 @@ function get_lxc_ip() { fi fi - # Last resort: Use routing table - local targets=("8.8.8.8" "1.1.1.1" "default") - for target in "${targets[@]}"; do + # Try routing table with IPv4 targets + local ipv4_targets=("8.8.8.8" "1.1.1.1" "default") + for target in "${ipv4_targets[@]}"; do if [[ "$target" == "default" ]]; then ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') else @@ -984,6 +1528,32 @@ function get_lxc_ip() { fi done + # IPv6 fallback: Try direct interface lookup for eth0 + ip=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1) + if [[ -n "$ip" && "$ip" =~ : ]]; then + echo "$ip" + return 0 + fi + + # IPv6 fallback: Try hostname -I for IPv6 + if command -v hostname >/dev/null 2>&1; then + ip=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1) + if [[ -n "$ip" && "$ip" =~ : ]]; then + echo "$ip" + return 0 + fi + fi + + # IPv6 fallback: Use routing table with IPv6 targets + local ipv6_targets=("2001:4860:4860::8888" "2606:4700:4700::1111") + for target in "${ipv6_targets[@]}"; do + ip=$(ip -6 route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + if [[ -n "$ip" && "$ip" =~ : ]]; then + echo "$ip" + return 0 + fi + done + return 1 } diff --git a/misc/install.func b/misc/install.func index 0a26d7d70..9aa13be45 100644 --- a/misc/install.func +++ b/misc/install.func @@ -902,28 +902,34 @@ EOF sysvinit) # Devuan/older systems - modify inittab for auto-login # Devuan 5 (daedalus) uses SysVinit with various inittab formats - # CRITICAL: LXC uses /dev/console, NOT tty1! pct console connects to console device + # LXC can use /dev/console OR /dev/tty1 depending on how pct console connects if [[ -f /etc/inittab ]]; then # Backup original inittab cp /etc/inittab /etc/inittab.bak 2>/dev/null || true - # First, enable autologin on tty1 (for direct access) - sed -i 's|^1:[0-9]*:respawn:.*/\(a\?getty\).*|1:2345:respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab + # Enable autologin on tty1 (for direct access) - handle various formats + # Devuan uses format: 1:2345:respawn:/sbin/getty 38400 tty1 + sed -i 's|^\(1:[0-9]*:respawn:\).*getty.*tty1.*|1:2345:respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab - # CRITICAL: Add console entry for LXC - this is what pct console uses! - # Check if there's already a console getty entry - if ! grep -qE '^[^#].*respawn.*console' /etc/inittab; then - # Add new console entry for LXC - echo "" >>/etc/inittab - echo "# LXC console autologin (added by community-scripts)" >>/etc/inittab - echo "co:2345:respawn:/sbin/agetty --autologin root --noclear console 115200,38400,9600 linux" >>/etc/inittab - else - # Enable autologin on existing console entry - sed -i 's|^[^#]*:[0-9]*:respawn:.*/\(a\?getty\).*console.*|co:2345:respawn:/sbin/agetty --autologin root --noclear console 115200,38400,9600 linux|' /etc/inittab - fi + # CRITICAL: Add/replace console entry for LXC - this is what pct console uses! + # Remove any existing console entries first (commented or not) + sed -i '/^[^#]*:.*:respawn:.*getty.*console/d' /etc/inittab + sed -i '/^# LXC console autologin/d' /etc/inittab - # Force a reload of inittab - try multiple methods - telinit q &>/dev/null || init q &>/dev/null || kill -1 1 &>/dev/null || true + # Add new console entry for LXC at the end + echo "" >>/etc/inittab + echo "# LXC console autologin (added by community-scripts)" >>/etc/inittab + echo "co:2345:respawn:/sbin/agetty --autologin root --noclear console 115200 linux" >>/etc/inittab + + # Force a reload of inittab and respawn ALL getty processes + # Kill ALL getty processes to force respawn with new autologin settings + pkill -9 -f '[ag]etty' &>/dev/null || true + + # Small delay to let init notice the dead processes + sleep 1 + + # Reload inittab - try multiple methods + telinit q &>/dev/null || init q &>/dev/null || kill -HUP 1 &>/dev/null || true fi touch /root/.hushlogin ;; diff --git a/misc/tools.func b/misc/tools.func index 1e1b86b5c..40acc1abb 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -1563,6 +1563,77 @@ check_for_gh_release() { return 1 } +# ------------------------------------------------------------------------------ +# Checks if a pinned GitHub tag exists and compares to local version. +# +# Description: +# - For tags that are NOT releases (e.g., hotfix tags) +# - Checks if the specified tag exists via Git refs API +# - Compares to local cached version (~/.) +# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0 +# +# Usage: +# if check_for_gh_tag "immich" "immich-app/immich" "v2.5.2"; then +# # trigger update... +# fi +# +# Notes: +# - Requires explicit tag (no 'latest' support - use check_for_gh_release for that) +# - Sets CHECK_UPDATE_RELEASE to the tag name if update is needed +# ------------------------------------------------------------------------------ +check_for_gh_tag() { + local app="$1" + local source="$2" + local pinned_tag="$3" + local app_lc="${app,,}" + local current_file="$HOME/.${app_lc}" + + if [[ -z "$pinned_tag" ]]; then + msg_error "check_for_gh_tag requires a pinned tag version" + return 1 + fi + + msg_info "Checking for update: ${app} (tag: ${pinned_tag})" + + # DNS check + if ! getent hosts api.github.com >/dev/null 2>&1; then + msg_error "Network error: cannot resolve api.github.com" + return 1 + fi + + ensure_dependencies jq + + # Check if tag exists via Git refs API + local tag_check + tag_check=$(curl -fsSL --max-time 20 \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "https://api.github.com/repos/${source}/git/refs/tags/${pinned_tag}" 2>/dev/null) + + if [[ $? -ne 0 ]] || [[ -z "$tag_check" ]] || echo "$tag_check" | jq -e '.message' &>/dev/null; then + msg_error "Tag ${pinned_tag} not found in ${source}" + return 1 + fi + + local pin_clean="${pinned_tag#v}" + + # Current installed version + local current="" + if [[ -f "$current_file" ]]; then + current="$(<"$current_file")" + fi + current="${current#v}" + + if [[ "$current" != "$pin_clean" ]]; then + CHECK_UPDATE_RELEASE="$pinned_tag" + msg_ok "Update available: ${app} ${current:-not installed} → ${pin_clean}" + return 0 + fi + + msg_ok "No update available: ${app} is already on version (${current})" + return 1 +} + # ------------------------------------------------------------------------------ # Creates and installs self-signed certificates. # @@ -1621,7 +1692,11 @@ function download_with_progress() { # Content-Length aus HTTP-Header holen local content_length - content_length=$(curl -fsSLI "$url" | awk '/Content-Length/ {print $2}' | tr -d '\r' || true) + content_length=$( + curl -fsSLI "$url" 2>/dev/null | + awk '(tolower($1) ~ /^content-length:/) && ($2 + 0 > 0) {print $2+0}' | + tail -1 | tr -cd '[:digit:]' || true + ) if [[ -z "$content_length" ]]; then if ! curl -fL# -o "$output" "$url"; then @@ -1694,12 +1769,15 @@ function ensure_usr_local_bin_persist() { # # # 4. Single binary (chmod +x) like Argus, Promtail etc. # fetch_and_deploy_gh_release "argus" "release-argus/Argus" "singlefile" "0.26.3" "/opt/argus" "Argus-.*linux-amd64" +# +# # 5. Git tag (not a release) - bypasses Release API, fetches tarball directly from tag +# fetch_and_deploy_gh_release "immich" "immich-app/immich" "tag" "v2.5.2" "/opt/immich/source" # ------------------------------------------------------------------------------ function fetch_and_deploy_gh_release() { local app="$1" local repo="$2" - local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile + local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile | tag local version="${4:-latest}" local target="${5:-/opt/$app}" local asset_pattern="${6:-}" @@ -1715,6 +1793,81 @@ function fetch_and_deploy_gh_release() { ensure_dependencies jq + ### Tag Mode (bypass Release API) ### + if [[ "$mode" == "tag" ]]; then + if [[ "$version" == "latest" ]]; then + msg_error "Mode 'tag' requires explicit version (not 'latest')" + return 1 + fi + + local tag_name="$version" + [[ "$tag_name" =~ ^v ]] && version="${tag_name:1}" || version="$tag_name" + + if [[ "$current_version" == "$version" ]]; then + $STD msg_ok "$app is already up-to-date (v$version)" + return 0 + fi + + # DNS check + if ! getent hosts "github.com" &>/dev/null; then + msg_error "DNS resolution failed for github.com – check /etc/resolv.conf or networking" + return 1 + fi + + local tmpdir + tmpdir=$(mktemp -d) || return 1 + + msg_info "Fetching GitHub tag: $app ($tag_name)" + + local safe_version="${version//@/_}" + safe_version="${safe_version//\//_}" + local filename="${app_lc}-${safe_version}.tar.gz" + local download_success=false + + # For tags with special characters (@, /), use codeload.github.com + if [[ "$tag_name" =~ [@/] ]]; then + local codeload_encoded="${tag_name//@/%40}" + local codeload_url="https://codeload.github.com/$repo/tar.gz/refs/tags/$codeload_encoded" + if curl $download_timeout -fsSL -o "$tmpdir/$filename" "$codeload_url"; then + download_success=true + fi + else + local direct_tarball_url="https://github.com/$repo/archive/refs/tags/${tag_name}.tar.gz" + if curl $download_timeout -fsSL -o "$tmpdir/$filename" "$direct_tarball_url"; then + download_success=true + fi + fi + + if [[ "$download_success" != "true" ]]; then + msg_error "Download failed for $app ($tag_name)" + rm -rf "$tmpdir" + return 1 + fi + + mkdir -p "$target" + if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then + rm -rf "${target:?}/"* + fi + + tar --no-same-owner -xzf "$tmpdir/$filename" -C "$tmpdir" || { + msg_error "Failed to extract tarball" + rm -rf "$tmpdir" + return 1 + } + + local unpack_dir + unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1) + + shopt -s dotglob nullglob + cp -r "$unpack_dir"/* "$target/" + shopt -u dotglob nullglob + + echo "$version" >"$version_file" + msg_ok "Deployed: $app ($version)" + rm -rf "$tmpdir" + return 0 + fi + local api_url="https://api.github.com/repos/$repo/releases" [[ "$version" != "latest" ]] && api_url="$api_url/tags/$version" || api_url="$api_url/latest" local header=() @@ -2027,6 +2180,440 @@ function fetch_and_deploy_gh_release() { rm -rf "$tmpdir" } +# ------------------------------------------------------------------------------ +# Downloads and deploys latest Codeberg release (source, binary, tarball, asset). +# +# Description: +# - Fetches latest release metadata from Codeberg API +# - Supports the following modes: +# - tarball: Source code tarball (default if omitted) +# - source: Alias for tarball (same behavior) +# - binary: .deb package install (arch-dependent) +# - prebuild: Prebuilt .tar.gz archive (e.g. Go binaries) +# - singlefile: Standalone binary (no archive, direct chmod +x install) +# - tag: Direct tag download (bypasses Release API) +# - Handles download, extraction/installation and version tracking in ~/. +# +# Parameters: +# $1 APP - Application name (used for install path and version file) +# $2 REPO - Codeberg repository in form user/repo +# $3 MODE - Release type: +# tarball → source tarball (.tar.gz) +# binary → .deb file (auto-arch matched) +# prebuild → prebuilt archive (e.g. tar.gz) +# singlefile→ standalone binary (chmod +x) +# tag → direct tag (bypasses Release API) +# $4 VERSION - Optional release tag (default: latest) +# $5 TARGET_DIR - Optional install path (default: /opt/) +# $6 ASSET_FILENAME - Required for: +# - prebuild → archive filename or pattern +# - singlefile→ binary filename or pattern +# +# Examples: +# # 1. Minimal: Fetch and deploy source tarball +# fetch_and_deploy_codeberg_release "autocaliweb" "gelbphoenix/autocaliweb" +# +# # 2. Binary install via .deb asset (architecture auto-detected) +# fetch_and_deploy_codeberg_release "myapp" "myuser/myapp" "binary" +# +# # 3. Prebuilt archive (.tar.gz) with asset filename match +# fetch_and_deploy_codeberg_release "myapp" "myuser/myapp" "prebuild" "latest" "/opt/myapp" "myapp_Linux_x86_64.tar.gz" +# +# # 4. Single binary (chmod +x) +# fetch_and_deploy_codeberg_release "myapp" "myuser/myapp" "singlefile" "v1.0.0" "/opt/myapp" "myapp-linux-amd64" +# +# # 5. Explicit tag version +# fetch_and_deploy_codeberg_release "autocaliweb" "gelbphoenix/autocaliweb" "tag" "v0.11.3" "/opt/autocaliweb" +# ------------------------------------------------------------------------------ + +function fetch_and_deploy_codeberg_release() { + local app="$1" + local repo="$2" + local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile | tag + local version="${4:-latest}" + local target="${5:-/opt/$app}" + local asset_pattern="${6:-}" + + local app_lc=$(echo "${app,,}" | tr -d ' ') + local version_file="$HOME/.${app_lc}" + + local api_timeout="--connect-timeout 10 --max-time 60" + local download_timeout="--connect-timeout 15 --max-time 900" + + local current_version="" + [[ -f "$version_file" ]] && current_version=$(<"$version_file") + + ensure_dependencies jq + + ### Tag Mode (bypass Release API) ### + if [[ "$mode" == "tag" ]]; then + if [[ "$version" == "latest" ]]; then + msg_error "Mode 'tag' requires explicit version (not 'latest')" + return 1 + fi + + local tag_name="$version" + [[ "$tag_name" =~ ^v ]] && version="${tag_name:1}" || version="$tag_name" + + if [[ "$current_version" == "$version" ]]; then + $STD msg_ok "$app is already up-to-date (v$version)" + return 0 + fi + + # DNS check + if ! getent hosts "codeberg.org" &>/dev/null; then + msg_error "DNS resolution failed for codeberg.org – check /etc/resolv.conf or networking" + return 1 + fi + + local tmpdir + tmpdir=$(mktemp -d) || return 1 + + msg_info "Fetching Codeberg tag: $app ($tag_name)" + + local safe_version="${version//@/_}" + safe_version="${safe_version//\//_}" + local filename="${app_lc}-${safe_version}.tar.gz" + local download_success=false + + # Codeberg archive URL format: https://codeberg.org/{owner}/{repo}/archive/{tag}.tar.gz + local archive_url="https://codeberg.org/$repo/archive/${tag_name}.tar.gz" + if curl $download_timeout -fsSL -o "$tmpdir/$filename" "$archive_url"; then + download_success=true + fi + + if [[ "$download_success" != "true" ]]; then + msg_error "Download failed for $app ($tag_name)" + rm -rf "$tmpdir" + return 1 + fi + + mkdir -p "$target" + if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then + rm -rf "${target:?}/"* + fi + + tar --no-same-owner -xzf "$tmpdir/$filename" -C "$tmpdir" || { + msg_error "Failed to extract tarball" + rm -rf "$tmpdir" + return 1 + } + + local unpack_dir + unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1) + + shopt -s dotglob nullglob + cp -r "$unpack_dir"/* "$target/" + shopt -u dotglob nullglob + + echo "$version" >"$version_file" + msg_ok "Deployed: $app ($version)" + rm -rf "$tmpdir" + return 0 + fi + + # Codeberg API: https://codeberg.org/api/v1/repos/{owner}/{repo}/releases + local api_url="https://codeberg.org/api/v1/repos/$repo/releases" + if [[ "$version" != "latest" ]]; then + # Get release by tag: /repos/{owner}/{repo}/releases/tags/{tag} + api_url="https://codeberg.org/api/v1/repos/$repo/releases/tags/$version" + fi + + # dns pre check + if ! getent hosts "codeberg.org" &>/dev/null; then + msg_error "DNS resolution failed for codeberg.org – check /etc/resolv.conf or networking" + return 1 + fi + + local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code + + while ((attempt <= max_retries)); do + resp=$(curl $api_timeout -fsSL -w "%{http_code}" -o /tmp/codeberg_rel.json "$api_url") && success=true && break + sleep "$retry_delay" + ((attempt++)) + done + + if ! $success; then + msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts" + return 1 + fi + + http_code="${resp:(-3)}" + [[ "$http_code" != "200" ]] && { + msg_error "Codeberg API returned HTTP $http_code" + return 1 + } + + local json tag_name + json=$(/dev/null || uname -m) + [[ "$arch" == "x86_64" ]] && arch="amd64" + [[ "$arch" == "aarch64" ]] && arch="arm64" + + local assets url_match="" + # Codeberg assets are in .assets[].browser_download_url + assets=$(echo "$json" | jq -r '.assets[].browser_download_url') + + # If explicit filename pattern is provided, match that first + if [[ -n "$asset_pattern" ]]; then + for u in $assets; do + case "${u##*/}" in + $asset_pattern) + url_match="$u" + break + ;; + esac + done + fi + + # Fall back to architecture heuristic + if [[ -z "$url_match" ]]; then + for u in $assets; do + if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then + url_match="$u" + break + fi + done + fi + + # Fallback: any .deb file + if [[ -z "$url_match" ]]; then + for u in $assets; do + [[ "$u" =~ \.deb$ ]] && url_match="$u" && break + done + fi + + if [[ -z "$url_match" ]]; then + msg_error "No suitable .deb asset found for $app" + rm -rf "$tmpdir" + return 1 + fi + + filename="${url_match##*/}" + curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url_match" || { + msg_error "Download failed: $url_match" + rm -rf "$tmpdir" + return 1 + } + + chmod 644 "$tmpdir/$filename" + $STD apt install -y "$tmpdir/$filename" || { + $STD dpkg -i "$tmpdir/$filename" || { + msg_error "Both apt and dpkg installation failed" + rm -rf "$tmpdir" + return 1 + } + } + + ### Prebuild Mode ### + elif [[ "$mode" == "prebuild" ]]; then + local pattern="${6%\"}" + pattern="${pattern#\"}" + [[ -z "$pattern" ]] && { + msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)" + rm -rf "$tmpdir" + return 1 + } + + local asset_url="" + for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do + filename_candidate="${u##*/}" + case "$filename_candidate" in + $pattern) + asset_url="$u" + break + ;; + esac + done + + [[ -z "$asset_url" ]] && { + msg_error "No asset matching '$pattern' found" + rm -rf "$tmpdir" + return 1 + } + + filename="${asset_url##*/}" + curl $download_timeout -fsSL -o "$tmpdir/$filename" "$asset_url" || { + msg_error "Download failed: $asset_url" + rm -rf "$tmpdir" + return 1 + } + + local unpack_tmp + unpack_tmp=$(mktemp -d) + mkdir -p "$target" + if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then + rm -rf "${target:?}/"* + fi + + if [[ "$filename" == *.zip ]]; then + ensure_dependencies unzip + unzip -q "$tmpdir/$filename" -d "$unpack_tmp" || { + msg_error "Failed to extract ZIP archive" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then + tar --no-same-owner -xf "$tmpdir/$filename" -C "$unpack_tmp" || { + msg_error "Failed to extract TAR archive" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Unsupported archive format: $filename" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + + local top_dirs + top_dirs=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1 -type d | wc -l) + local top_entries inner_dir + top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1) + if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then + inner_dir="$top_entries" + shopt -s dotglob nullglob + if compgen -G "$inner_dir/*" >/dev/null; then + cp -r "$inner_dir"/* "$target/" || { + msg_error "Failed to copy contents from $inner_dir to $target" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Inner directory is empty: $inner_dir" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + shopt -u dotglob nullglob + else + shopt -s dotglob nullglob + if compgen -G "$unpack_tmp/*" >/dev/null; then + cp -r "$unpack_tmp"/* "$target/" || { + msg_error "Failed to copy contents to $target" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + } + else + msg_error "Unpacked archive is empty" + rm -rf "$tmpdir" "$unpack_tmp" + return 1 + fi + shopt -u dotglob nullglob + fi + + ### Singlefile Mode ### + elif [[ "$mode" == "singlefile" ]]; then + local pattern="${6%\"}" + pattern="${pattern#\"}" + [[ -z "$pattern" ]] && { + msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)" + rm -rf "$tmpdir" + return 1 + } + + local asset_url="" + for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do + filename_candidate="${u##*/}" + case "$filename_candidate" in + $pattern) + asset_url="$u" + break + ;; + esac + done + + [[ -z "$asset_url" ]] && { + msg_error "No asset matching '$pattern' found" + rm -rf "$tmpdir" + return 1 + } + + filename="${asset_url##*/}" + mkdir -p "$target" + + local use_filename="${USE_ORIGINAL_FILENAME:-false}" + local target_file="$app" + [[ "$use_filename" == "true" ]] && target_file="$filename" + + curl $download_timeout -fsSL -o "$target/$target_file" "$asset_url" || { + msg_error "Download failed: $asset_url" + rm -rf "$tmpdir" + return 1 + } + + if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then + chmod +x "$target/$target_file" + fi + + else + msg_error "Unknown mode: $mode" + rm -rf "$tmpdir" + return 1 + fi + + echo "$version" >"$version_file" + msg_ok "Deployed: $app ($version)" + rm -rf "$tmpdir" +} + # ------------------------------------------------------------------------------ # Loads LOCAL_IP from persistent store or detects if missing. # @@ -6056,4 +6643,3 @@ function fetch_and_deploy_archive() { msg_ok "Successfully deployed archive to $directory" return 0 } - diff --git a/tools/pve/update-apps.sh b/tools/pve/update-apps.sh index b4977e130..b57e7e822 100644 --- a/tools/pve/update-apps.sh +++ b/tools/pve/update-apps.sh @@ -6,6 +6,105 @@ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/refs/heads/main/misc/core.func) +# ============================================================================= +# CONFIGURATION VARIABLES +# Set these variables to skip interactive prompts (Whiptail dialogs) +# ============================================================================= +# var_backup: Enable/disable backup before update +# Options: "yes" | "no" | "" (empty = interactive prompt) +var_backup="${var_backup:-}" + +# var_backup_storage: Storage location for backups (only used if var_backup=yes) +# Options: Storage name from /etc/pve/storage.cfg (e.g., "local", "nas-backup") +# Leave empty for interactive selection +var_backup_storage="${var_backup_storage:-}" + +# var_container: Which containers to update +# Options: +# - "all" : All containers with community-scripts tags +# - "all_running" : Only running containers with community-scripts tags +# - "all_stopped" : Only stopped containers with community-scripts tags +# - "101,102,109" : Comma-separated list of specific container IDs +# - "" : Interactive selection via Whiptail +var_container="${var_container:-}" + +# var_unattended: Run updates without user interaction inside containers +# Options: "yes" | "no" | "" (empty = interactive prompt) +var_unattended="${var_unattended:-}" + +# var_skip_confirm: Skip initial confirmation dialog +# Options: "yes" | "no" (default: no) +var_skip_confirm="${var_skip_confirm:-no}" + +# var_auto_reboot: Automatically reboot containers that require it after update +# Options: "yes" | "no" | "" (empty = interactive prompt) +var_auto_reboot="${var_auto_reboot:-}" + +# ============================================================================= +# JSON CONFIG EXPORT +# Run with --export-config to output current configuration as JSON +# ============================================================================= + +function export_config_json() { + cat <&2 2>&1 1>&3 | tr -d '"') +# Determine container selection based on var_container +if [[ -n "$var_container" ]]; then + case "$var_container" in + all) + # Select all containers with matching tags + CHOICE="" + for ((i=0; i<${#menu_items[@]}; i+=3)); do + CHOICE="$CHOICE ${menu_items[$i]}" + done + CHOICE=$(echo "$CHOICE" | xargs) + ;; + all_running) + # Select only running containers with matching tags + CHOICE="" + for ((i=0; i<${#menu_items[@]}; i+=3)); do + cid="${menu_items[$i]}" + if pct status "$cid" 2>/dev/null | grep -q "running"; then + CHOICE="$CHOICE $cid" + fi + done + CHOICE=$(echo "$CHOICE" | xargs) + ;; + all_stopped) + # Select only stopped containers with matching tags + CHOICE="" + for ((i=0; i<${#menu_items[@]}; i+=3)); do + cid="${menu_items[$i]}" + if pct status "$cid" 2>/dev/null | grep -q "stopped"; then + CHOICE="$CHOICE $cid" + fi + done + CHOICE=$(echo "$CHOICE" | xargs) + ;; + *) + # Assume comma-separated list of container IDs + CHOICE=$(echo "$var_container" | tr ',' ' ') + ;; + esac -if [ -z "$CHOICE" ]; then - whiptail --title "LXC Container Update" \ - --msgbox "No containers selected!" 10 60 - exit 1 + if [[ -z "$CHOICE" ]]; then + msg_error "No containers matched the selection criteria: $var_container" + exit 1 + fi + msg_ok "Selected containers: $CHOICE" +else + CHOICE=$(whiptail --title "LXC Container Update" \ + --checklist "Select LXC containers to update:" 25 60 13 \ + "${menu_items[@]}" 3>&2 2>&1 1>&3 | tr -d '"') + + if [ -z "$CHOICE" ]; then + whiptail --title "LXC Container Update" \ + --msgbox "No containers selected!" 10 60 + exit 1 + fi fi header_info -BACKUP_CHOICE="no" -if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "LXC Container Update" --yesno "Do you want to backup your containers before update?" 10 58); then - BACKUP_CHOICE="yes" + +# Determine backup choice based on var_backup +if [[ -n "$var_backup" ]]; then + BACKUP_CHOICE="$var_backup" +else + BACKUP_CHOICE="no" + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "LXC Container Update" --yesno "Do you want to backup your containers before update?" 10 58); then + BACKUP_CHOICE="yes" + fi fi -UNATTENDED_UPDATE="no" -if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "LXC Container Update" --yesno "Run updates unattended?" 10 58); then - UNATTENDED_UPDATE="yes" +# Determine unattended update based on var_unattended +if [[ -n "$var_unattended" ]]; then + UNATTENDED_UPDATE="$var_unattended" +else + UNATTENDED_UPDATE="no" + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "LXC Container Update" --yesno "Run updates unattended?" 10 58); then + UNATTENDED_UPDATE="yes" + fi fi if [ "$BACKUP_CHOICE" == "yes" ]; then - #STORAGES=$(awk '/^(\S+):/ {storage=$2} /content.*backup/ {print storage}' /etc/pve/storage.cfg) get_backup_storages if [ -z "$STORAGES" ]; then - whiptail --msgbox "No storage with 'backup' found!" 8 40 + msg_error "No storage with 'backup' support found!" exit 1 fi - MENU_ITEMS=() - for STORAGE in $STORAGES; do - MENU_ITEMS+=("$STORAGE" "") - done + # Determine storage based on var_backup_storage + if [[ -n "$var_backup_storage" ]]; then + # Validate that the specified storage exists and supports backups + if echo "$STORAGES" | grep -qw "$var_backup_storage"; then + STORAGE_CHOICE="$var_backup_storage" + msg_ok "Using backup storage: $STORAGE_CHOICE" + else + msg_error "Specified backup storage '$var_backup_storage' not found or doesn't support backups!" + msg_info "Available storages: $(echo $STORAGES | tr '\n' ' ')" + exit 1 + fi + else + MENU_ITEMS=() + for STORAGE in $STORAGES; do + MENU_ITEMS+=("$STORAGE" "") + done - STORAGE_CHOICE=$(whiptail --title "Select storage device" --menu "Select a storage device (Only storage devices with 'backup' support are listed):" 15 50 5 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) + STORAGE_CHOICE=$(whiptail --title "Select storage device" --menu "Select a storage device (Only storage devices with 'backup' support are listed):" 15 50 5 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) - if [ -z "$STORAGE_CHOICE" ]; then - msg_error "No storage selected!" - exit 1 + if [ -z "$STORAGE_CHOICE" ]; then + msg_error "No storage selected!" + exit 1 + fi fi fi @@ -270,9 +442,20 @@ if [ "${#containers_needing_reboot[@]}" -gt 0 ]; then for container_name in "${containers_needing_reboot[@]}"; do echo "$container_name" done - echo -ne "${INFO} Do you wish to reboot these containers? " - read -r prompt - if [[ ${prompt,,} =~ ^(yes)$ ]]; then + + # Determine reboot choice based on var_auto_reboot + REBOOT_CHOICE="no" + if [[ -n "$var_auto_reboot" ]]; then + REBOOT_CHOICE="$var_auto_reboot" + else + echo -ne "${INFO} Do you wish to reboot these containers? " + read -r prompt + if [[ ${prompt,,} =~ ^(yes)$ ]]; then + REBOOT_CHOICE="yes" + fi + fi + + if [[ "$REBOOT_CHOICE" == "yes" ]]; then echo -e "${CROSS}${HOLD} ${YWB}Rebooting containers.${CL}" for container_name in "${containers_needing_reboot[@]}"; do container=$(echo $container_name | cut -d " " -f 1) diff --git a/vm/truenas-vm.sh b/vm/truenas-vm.sh new file mode 100644 index 000000000..295dc6954 --- /dev/null +++ b/vm/truenas-vm.sh @@ -0,0 +1,592 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: juronja +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://www.truenas.com/truenas-community-edition/ + +source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/api.func) + +function header_info() { + clear + cat <<"EOF" + ______ _ _____ _____ + /_ __/______ _____ / | / / | / ___/ + / / / ___/ / / / _ \/ |/ / /| | \__ \ + / / / / / /_/ / __/ /| / ___ |___/ / +/_/ /_/ \__,_/\___/_/ |_/_/ |_/____/ + (Community Edition) +EOF +} +header_info +echo -e "\n Loading..." +GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//') +RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" +METHOD="" + +YW=$(echo "\033[33m") +BL=$(echo "\033[36m") +RD=$(echo "\033[01;31m") +BGN=$(echo "\033[4;92m") +GN=$(echo "\033[1;92m") +DGN=$(echo "\033[32m") +CL=$(echo "\033[m") + +CL=$(echo "\033[m") +BOLD=$(echo "\033[1m") +BFR="\\r\\033[K" +HOLD=" " +TAB=" " + +CM="${TAB}✔️${TAB}${CL}" +CROSS="${TAB}✖️${TAB}${CL}" +INFO="${TAB}💡${TAB}${CL}" +OS="${TAB}🖥️${TAB}${CL}" +CONTAINERTYPE="${TAB}📦${TAB}${CL}" +ISO="${TAB}📀${TAB}${CL}" +DISKSIZE="${TAB}💾${TAB}${CL}" +CPUCORE="${TAB}🧠${TAB}${CL}" +RAMSIZE="${TAB}🛠️${TAB}${CL}" +CONTAINERID="${TAB}🆔${TAB}${CL}" +HOSTNAME="${TAB}🏠${TAB}${CL}" +BRIDGE="${TAB}🌉${TAB}${CL}" +GATEWAY="${TAB}🌐${TAB}${CL}" +DISK="${TAB}💽${TAB}${CL}" +DEFAULT="${TAB}⚙️${TAB}${CL}" +MACADDRESS="${TAB}🔗${TAB}${CL}" +VLANTAG="${TAB}🏷️${TAB}${CL}" +CREATING="${TAB}🚀${TAB}${CL}" +ADVANCED="${TAB}🧩${TAB}${CL}" +CLOUD="${TAB}☁️${TAB}${CL}" + +set -e +trap 'error_handler $LINENO "$BASH_COMMAND"' ERR +trap cleanup EXIT +trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT +trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM +function error_handler() { + local exit_code="$?" + local line_number="$1" + local command="$2" + local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" + post_update_to_api "failed" "${command}" + echo -e "\n$error_message\n" + cleanup_vmid +} + +function truenas_iso_lookup() { + local BASE_URL="https://download.truenas.com" + local current_year=$(date +%y) + local last_year=$(date -d "1 year ago" +%y) + local year_pattern="${current_year}\.|${last_year}\." + + declare -A latest_stables + local pre_releases=() + + local all_paths=$( + curl -sL "$BASE_URL" | + grep -oE 'href="[^"]+\.iso"' | + sed 's/href="//; s/"$//' | + grep -vE '(nightly|ALPHA)' | + grep -E "$year_pattern" + ) + + while read -r path; do + local filename=$(basename "$path") + local version=$(echo "$filename" | sed -E 's/.*TrueNAS-SCALE-([0-9]{2}\.[0-9]{2}(\.[0-9]+)*(-RC[0-9]|-BETA[0-9])?)\.iso.*/\1/') + if [[ "$version" =~ (RC|BETA) ]]; then + pre_releases+=("$path") + else + local major_version=$(echo "$version" | cut -d'.' -f1,2) + local current_stored_path=${latest_stables["$major_version"]} + if [[ -z "$current_stored_path" ]]; then + latest_stables["$major_version"]="$path" + else + local stored_version=$(basename "$current_stored_path" | sed -E 's/.*TrueNAS-SCALE-([0-9]{2}\.[0-9]{2}(\.[0-9]+)*)\.iso.*/\1/') + if printf '%s\n' "$version" "$stored_version" | sort -V | tail -n 1 | grep -q "$version"; then + latest_stables["$major_version"]="$path" + fi + fi + fi + done <<<"$all_paths" + + for key in "${!latest_stables[@]}"; do + echo "${latest_stables[$key]#/}" + done + + for pre in "${pre_releases[@]}"; do + echo "${pre#/}" + done | sort -V +} + +function get_valid_nextid() { + local try_id + try_id=$(pvesh get /cluster/nextid) + while true; do + if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then + try_id=$((try_id + 1)) + continue + fi + if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then + try_id=$((try_id + 1)) + continue + fi + break + done + echo "$try_id" +} + +function cleanup_vmid() { + if qm status $VMID &>/dev/null; then + qm stop $VMID &>/dev/null + qm destroy $VMID &>/dev/null + fi +} + +function cleanup() { + popd >/dev/null + post_update_to_api "done" "none" + rm -rf $TEMP_DIR +} + +TEMP_DIR=$(mktemp -d) +pushd $TEMP_DIR >/dev/null +if whiptail --backtitle "Proxmox VE Helper Scripts" --title "TrueNAS VM" --yesno "This will create a New TrueNAS VM. Proceed?" 10 58; then + : +else + header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit +fi + +function msg_info() { + local msg="$1" + echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}" +} + +function msg_ok() { + local msg="$1" + echo -e "${BFR}${CM}${GN}${msg}${CL}" +} + +function msg_error() { + local msg="$1" + echo -e "${BFR}${CROSS}${RD}${msg}${CL}" +} + +function check_root() { + if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then + clear + msg_error "Please run this script as root." + echo -e "\nExiting..." + sleep 2 + exit + fi +} + +pve_check() { + local PVE_VER + PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" + + if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then + local MINOR="${BASH_REMATCH[1]}" + if ((MINOR < 0 || MINOR > 9)); then + msg_error "This version of Proxmox VE is not supported." + msg_error "Supported: Proxmox VE version 8.0 – 8.9" + exit 1 + fi + return 0 + fi + + if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then + local MINOR="${BASH_REMATCH[1]}" + if ((MINOR < 0 || MINOR > 1)); then + msg_error "This version of Proxmox VE is not yet supported." + msg_error "Supported: Proxmox VE version 9.0 – 9.1" + exit 1 + fi + return 0 + fi + + msg_error "This version of Proxmox VE is not supported." + msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0 – 9.1" + exit 1 +} + +function arch_check() { + if [ "$(dpkg --print-architecture)" != "amd64" ]; then + echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n" + echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n" + echo -e "Exiting..." + sleep 2 + exit + fi +} + +function ssh_check() { + if command -v pveversion >/dev/null 2>&1; then + if [ -n "${SSH_CLIENT:+x}" ]; then + if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then + echo "you've been warned" + else + clear + exit + fi + fi + fi +} + +function exit-script() { + clear + echo -e "\n${CROSS}${RD}User exited script${CL}\n" + exit +} + +function default_settings() { + VMID=$(get_valid_nextid) + ISO_DEFAULT="latest stable" + FORMAT="" + MACHINE="q35" + DISK_SIZE="16" + HN="truenas" + CPU_TYPE="host" + CORE_COUNT="2" + RAM_SIZE="8192" + BRG="vmbr0" + MAC="$GEN_MAC" + VLAN="" + MTU="" + START_VM="yes" + METHOD="default" + echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}" + echo -e "${ISO}${BOLD}${DGN}ISO Chosen: ${BGN}${ISO_DEFAULT}${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}${MACHINE}${CL}" + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}" + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}" + echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}${CPU_TYPE}${CL}" + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}" + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}" + echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}" + echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}" + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}" + echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}" + echo -e "${CREATING}${BOLD}${DGN}Creating a TrueNAS VM using the above default settings${CL}" +} + +function advanced_settings() { + METHOD="advanced" + [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid) + while true; do + if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$VMID" ]; then + VMID=$(get_valid_nextid) + fi + if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then + echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" + sleep 2 + continue + fi + echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" + break + else + exit-script + fi + done + + ISOARRAY=() + while read -r ISOPATH; do + FILENAME=$(basename "$ISOPATH") + ISOARRAY+=("$ISOPATH" "$FILENAME" "OFF") + done < <(truenas_iso_lookup | sort -V) + if [ ${#ISOARRAY[@]} -eq 0 ]; then + echo "No ISOs found." + exit 1 + fi + + if SELECTED_ISO=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT ISO TO INSTALL" --notags --radiolist "\nSelect version (BETA/RC + Latest stables):" 20 58 12 "${ISOARRAY[@]}" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + echo -e "${ISO}${BOLD}${DGN}ISO Chosen: ${BGN}$(basename "$SELECTED_ISO")${CL}" + else + exit-script + fi + + if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ') + if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}" + else + echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10).${CL}" + exit-script + fi + else + exit-script + fi + + if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 truenas --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z $VM_NAME ]; then + HN="truenas" + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" + else + HN=$(echo ${VM_NAME,,} | tr -d ' ') + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" + fi + else + exit-script + fi + + if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose CPU Model" --cancel-button Exit-Script 10 58 2 \ + "KVM64" "Default – safe for migration/compatibility" OFF \ + "Host" "Use host CPU features (faster, no migration)" ON \ + 3>&1 1>&2 2>&3); then + case "$CPU_TYPE1" in + Host) + echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}" + CPU_TYPE="host" + ;; + *) + echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}" + CPU_TYPE="" + ;; + esac + else + exit-script + fi + + if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z $CORE_COUNT ]; then + CORE_COUNT="2" + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" + else + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" + fi + else + exit-script + fi + + if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 8192 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z $RAM_SIZE ]; then + RAM_SIZE="8192" + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}" + else + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}" + fi + else + exit-script + fi + + if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z $BRG ]; then + BRG="vmbr0" + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + else + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + fi + else + exit-script + fi + + if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z $MAC1 ]; then + MAC="$GEN_MAC" + echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}" + else + MAC="$MAC1" + echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" + fi + else + exit-script + fi + + if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z $VLAN1 ]; then + VLAN1="Default" + VLAN="" + echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}" + else + VLAN=",tag=$VLAN1" + echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}" + fi + else + exit-script + fi + + if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z $MTU1 ]; then + MTU1="Default" + MTU="" + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" + else + MTU=",mtu=$MTU1" + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" + fi + else + exit-script + fi + + if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "IMPORT ONBOARD DISKS" --yesno "Would you like to import onboard disks?" 10 58); then + echo -e "${DISK}${BOLD}${DGN}Import onboard disks: ${BGN}yes${CL}" + IMPORT_DISKS="yes" + else + echo -e "${DISK}${BOLD}${DGN}Import onboard disks: ${BGN}no${CL}" + IMPORT_DISKS="no" + fi + + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then + echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}" + START_VM="yes" + else + echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}" + START_VM="no" + fi + + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a TrueNAS VM?" --no-button Do-Over 10 58); then + echo -e "${CREATING}${BOLD}${DGN}Creating a TrueNAS VM using the above advanced settings${CL}" + else + header_info + echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}" + advanced_settings + fi +} + +function start_script() { + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}" + default_settings + else + header_info + echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}" + advanced_settings + fi +} +check_root +arch_check +pve_check +ssh_check +start_script +post_to_api_vm + +msg_info "Validating Storage" +while read -r line; do + TAG=$(echo $line | awk '{print $1}') + TYPE=$(echo $line | awk '{printf "%-10s", $2}') + FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}') + ITEM=" Type: $TYPE Free: $FREE " + OFFSET=2 + if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then + MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET)) + fi + STORAGE_MENU+=("$TAG" "$ITEM" "OFF") +done < <(pvesm status -content images | awk 'NR>1') +VALID=$(pvesm status -content images | awk 'NR>1') +if [ -z "$VALID" ]; then + msg_error "Unable to detect a valid storage location." + exit +elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then + STORAGE=${STORAGE_MENU[0]} +else + while [ -z "${STORAGE:+x}" ]; do + if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null; fi + printf "\e[?25h" + STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \ + "Which storage pool would you like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \ + 16 $(($MSG_MAX_LENGTH + 23)) 6 \ + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) + done +fi +msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." +msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}." + +if [ -z "${SELECTED_ISO:-}" ]; then + SELECTED_ISO=$(truenas_iso_lookup | grep -vE 'RC|BETA' | sort -V | tail -n 1) + + if [ -z "$SELECTED_ISO" ]; then + msg_error "Could not find a stable ISO for fallback." + exit 1 + fi +fi + +FULL_URL="https://download.truenas.com/${SELECTED_ISO#/}" +ISO_NAME=$(basename "$FULL_URL") +CACHE_DIR="/var/lib/vz/template/iso" +CACHE_FILE="$CACHE_DIR/$ISO_NAME" + +if [[ ! -s "$CACHE_FILE" ]]; then + msg_info "Retrieving the ISO for the TrueNAS Disk Image" + curl -f#SL -o "$CACHE_FILE" "$FULL_URL" + msg_ok "Downloaded ${CL}${BL}$(basename "$CACHE_FILE")${CL}" +else + msg_ok "Using cached image ${CL}${BL}$(basename "$CACHE_FILE")${CL}" +fi + +set -o pipefail +msg_info "Creating TrueNAS VM shell" +qm create "$VMID" -machine q35 -bios ovmf -agent enabled=1 -tablet 0 -localtime 1 -cpu "$CPU_TYPE" \ + -cores "$CORE_COUNT" -memory "$RAM_SIZE" -balloon 0 -name "$HN" -tags community-script \ + -net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 \ + -efidisk0 $STORAGE:1,efitype=4m,pre-enrolled-keys=0 -sata0 $STORAGE:$DISK_SIZE,ssd=1 \ + -scsihw virtio-scsi-single -cdrom local:iso/$ISO_NAME -vga virtio >/dev/null +msg_ok "Created VM shell" + +if [ "$IMPORT_DISKS" == "yes" ]; then + msg_info "Importing onboard disks" + DISKARRAY=() + SCSI_NR=0 + + while read -r LSOUTPUT; do + DISKARRAY+=("$LSOUTPUT" "" "OFF") + done < <(ls /dev/disk/by-id | grep -E '^ata-|^nvme-' | grep -v 'part') + + SELECTIONS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT DISKS TO IMPORT" --checklist "\nSelect disk IDs to import. (Use Spacebar to select)\n" --cancel-button "Exit Script" 20 58 10 "${DISKARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"') || exit + + for SELECTION in $SELECTIONS; do + ((++SCSI_NR)) + + ID_SERIAL=$(echo "$SELECTION" | rev | cut -d'_' -f1 | rev) + ID_SERIAL=${ID_SERIAL:0:20} + + qm set $VMID --scsi$SCSI_NR /dev/disk/by-id/$SELECTION,serial=$ID_SERIAL + done + msg_ok "Disks imported successfully" +fi + +DESCRIPTION=$( + cat < + + Logo + + +

TrueNAS Community Edition

+ +

+ + spend Coffee + +

+ + + + GitHub + + + + Discussions + + + + Issues + + +EOF +) +qm set "$VMID" -description "$DESCRIPTION" >/dev/null + +sleep 3 + +msg_ok "Created a TrueNAS VM ${CL}${BL}(${HN})" +if [ "$START_VM" == "yes" ]; then + msg_info "Starting TrueNAS VM" + qm start $VMID + msg_ok "Started TrueNAS VM" +fi + +msg_ok "Completed Successfully!\n"