diff --git a/README.md b/README.md index e9f7180..51c0a64 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,20 @@ replacing `/bin/gzip`. APT remains dual-stack by default; its conditional IPv4 compatibility check is optional. Global vzdump bandwidth and I/O-priority changes are available through a separate explicit menu. +Pulse can also be deployed without installing TA-ProxMenu. Run the standalone +bootstrap as root on a Proxmox VE host: + +```bash +bash <(curl -fsSL https://git.schroedercity.com/TAI/TA-ProxMenu/raw/branch/V2/install-pulse.sh) +``` + +The bootstrap downloads the Pulse deployment module and its LXC storage helper +from the same branch into a protected temporary directory, validates their Bash +syntax and expected entry points, runs the normal interactive Pulse workflow, +and removes the temporary files afterward. It does not clone or install +TA-ProxMenu. Set `TAPM_PULSE_SOURCE_BRANCH` before running it to use another +branch. + VirtIO downloads are managed from a dedicated submenu. The stable release is checked only when that submenu is opened, and curated compatibility ISOs are available for Windows Server 2008, 2008 R2, 2012/R2, and 2016. Downloads are diff --git a/defaults.inc b/defaults.inc index c738125..3d0486e 100755 --- a/defaults.inc +++ b/defaults.inc @@ -3,7 +3,7 @@ action="${1:-}" FOLDER='/opt/idssys/ta-proxmenu' -VERS='2026.7.26-9' +VERS='2026.7.28-1' noupdate=' ' diff --git a/install-pulse.sh b/install-pulse.sh new file mode 100755 index 0000000..d86c097 --- /dev/null +++ b/install-pulse.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +# Standalone TA-managed Pulse LXC deployment bootstrap for Proxmox VE. + +set -u -o pipefail + +TAPM_PULSE_SOURCE_BRANCH="${TAPM_PULSE_SOURCE_BRANCH:-V2}" +TAPM_PULSE_SOURCE_BASE="${TAPM_PULSE_SOURCE_BASE:-https://git.schroedercity.com/TAI/TA-ProxMenu/raw/branch/${TAPM_PULSE_SOURCE_BRANCH}}" +TAPM_PULSE_BOOTSTRAP_DIR='' +TAPM_TEMP_DIR='' +declare -a TAPM_TEMP_DIRS=() +declare -A idsCL=() + +TAPM_PULSE_VALID_SOURCE_BRANCH() { + local branch="${1:-}" + + [[ -n "$branch" && + "$branch" =~ ^[A-Za-z0-9._/-]+$ && + "$branch" != /* && + "$branch" != */ && + "$branch" != *..* ]] +} + +TAPM_PULSE_VALID_SOURCE_BASE() { + local url="${1:-}" + + [[ "$url" == https://* && + "$url" != *$'\n'* && + "$url" != *$'\r'* && + "$url" != *'"'* && + "$url" != *\\* && + "$url" != *[[:space:]]* ]] +} + +TAPM_PULSE_DEFINE_COLORS() { + if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then + idsCL[Default]=$'\e[0m' + idsCL[Red]=$'\e[31m' + idsCL[Green]=$'\e[32m' + idsCL[White]=$'\e[97m' + idsCL[LightRed]=$'\e[91m' + idsCL[LightGreen]=$'\e[92m' + idsCL[LightYellow]=$'\e[93m' + idsCL[LightCyan]=$'\e[96m' + else + idsCL[Default]='' + idsCL[Red]='' + idsCL[Green]='' + idsCL[White]='' + idsCL[LightRed]='' + idsCL[LightGreen]='' + idsCL[LightYellow]='' + idsCL[LightCyan]='' + fi +} + +TAPM_CLEAN_TEMP_DIR() { + local temp_dir="${1:-}" + + if [[ "$temp_dir" == /tmp/ta-proxmenu-* && -d "$temp_dir" ]]; then + rm -rf -- "$temp_dir" + fi +} + +TAPM_CLEAN_ALL_TEMP_DIRS() { + local temp_dir + + for temp_dir in "${TAPM_TEMP_DIRS[@]}"; do + TAPM_CLEAN_TEMP_DIR "$temp_dir" + done + TAPM_CLEAN_TEMP_DIR "$TAPM_PULSE_BOOTSTRAP_DIR" +} + +TAPM_CREATE_TEMP_DIR() { + local label="${1:-installer}" + + TAPM_TEMP_DIR="$(mktemp -d "/tmp/ta-proxmenu-${label}.XXXXXX")" || { + echo -e "${idsCL[LightRed]}Unable to create a temporary installer directory.${idsCL[Default]}" + return 1 + } + if ! chmod 0700 "$TAPM_TEMP_DIR"; then + TAPM_CLEAN_TEMP_DIR "$TAPM_TEMP_DIR" + echo -e "${idsCL[LightRed]}Unable to secure the temporary installer directory.${idsCL[Default]}" + return 1 + fi + TAPM_TEMP_DIRS+=("$TAPM_TEMP_DIR") +} + +TAPM_DOWNLOAD_HTTPS() { + local url="$1" + local destination="$2" + local label="${3:-Installer}" + + if ! TAPM_PULSE_VALID_SOURCE_BASE "$url"; then + echo -e "${idsCL[LightRed]}${label} requires a valid HTTPS URL.${idsCL[Default]}" + return 1 + fi + if ! printf 'url = "%s"\n' "$url" | + curl --fail --location --silent --show-error \ + --proto '=https' --proto-redir '=https' \ + --output "$destination" --config -; then + echo -e "${idsCL[LightRed]}${label} download failed.${idsCL[Default]}" + return 1 + fi + if [[ ! -s "$destination" ]]; then + echo -e "${idsCL[LightRed]}${label} download was empty.${idsCL[Default]}" + return 1 + fi +} + +EXIT1() { + stty echo 2>/dev/null || true + printf '%s' "${idsCL[Default]}" +} + +SELECT_MENU() { + local title="$1" + local labels_name="$2" + local values_name="$3" + local allow_back="${4:-1}" + local -n labels_ref="$labels_name" + local -n values_ref="$values_name" + local selected=0 + local key sequence index + + while true; do + if [[ -t 1 ]]; then + clear 2>/dev/null || printf '\e[H\e[2J' + fi + echo + echo -e " ${idsCL[LightCyan]}${title}${idsCL[Default]}" + echo + for index in "${!labels_ref[@]}"; do + if (( index == selected )); then + printf '\e[7m %d %-64s\e[0m\n' \ + "$((index + 1))" "${labels_ref[$index]}" + else + printf ' %d %s\n' "$((index + 1))" "${labels_ref[$index]}" + fi + done + echo + if (( allow_back == 1 )); then + echo " ↑/↓ Navigate Enter Select Number Quick Select ←/Esc/B Back Q Quit" + else + echo " ↑/↓ Navigate Enter Select Number Quick Select Q Quit" + fi + + IFS= read -rsn1 key + case "$key" in + "") + MENU_SELECTION="${values_ref[$selected]}" + return 0 + ;; + [1-9]) + index=$((10#$key - 1)) + if (( index < ${#values_ref[@]} )); then + MENU_SELECTION="${values_ref[$index]}" + return 0 + fi + ;; + [Qq]) + MENU_SELECTION=quit + return 0 + ;; + [Bb]) + if (( allow_back == 1 )); then + MENU_SELECTION=back + return 0 + fi + ;; + $'\e') + sequence='' + IFS= read -rsn2 -t 0.1 sequence || true + case "$sequence" in + "[A"|"OA") + ((selected = (selected - 1 + ${#labels_ref[@]}) % ${#labels_ref[@]})) + ;; + "[B"|"OB") + ((selected = (selected + 1) % ${#labels_ref[@]})) + ;; + "[C"|"OC") + MENU_SELECTION="${values_ref[$selected]}" + return 0 + ;; + "[D"|"OD"|"") + if (( allow_back == 1 )); then + MENU_SELECTION=back + return 0 + fi + ;; + esac + ;; + esac + done +} + +TAPM_PULSE_STANDALONE_MAIN() { + local iso_helpers pulse_module + + TAPM_PULSE_DEFINE_COLORS + if (( BASH_VERSINFO[0] < 4 || + (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3) )); then + echo "Pulse deployment requires Bash 4.3 or newer." >&2 + return 1 + fi + if (( EUID != 0 )); then + echo "Run this installer as root on a Proxmox VE host." >&2 + return 1 + fi + for command in curl pct pvesh; do + if ! command -v "$command" >/dev/null 2>&1; then + echo "Required command '${command}' was not found." >&2 + return 1 + fi + done + if ! TAPM_PULSE_VALID_SOURCE_BRANCH "$TAPM_PULSE_SOURCE_BRANCH" || + ! TAPM_PULSE_VALID_SOURCE_BASE "$TAPM_PULSE_SOURCE_BASE"; then + echo "The configured TA-ProxMenu source branch or URL is invalid." >&2 + return 1 + fi + + TAPM_PULSE_BOOTSTRAP_DIR="$( + mktemp -d /tmp/ta-proxmenu-pulse-bootstrap.XXXXXX + )" || return 1 + chmod 0700 "$TAPM_PULSE_BOOTSTRAP_DIR" || return 1 + iso_helpers="${TAPM_PULSE_BOOTSTRAP_DIR}/deploy-iso-nfs-lxc.sh" + pulse_module="${TAPM_PULSE_BOOTSTRAP_DIR}/deploy-pulse-lxc.sh" + + echo "Downloading the TA-managed Pulse deployment components..." + TAPM_DOWNLOAD_HTTPS \ + "${TAPM_PULSE_SOURCE_BASE}/inc/deploy-iso-nfs-lxc.sh" \ + "$iso_helpers" "LXC storage helper" || return 1 + TAPM_DOWNLOAD_HTTPS \ + "${TAPM_PULSE_SOURCE_BASE}/inc/deploy-pulse-lxc.sh" \ + "$pulse_module" "Pulse deployment module" || return 1 + bash -n "$iso_helpers" "$pulse_module" || { + echo "A downloaded Pulse deployment component failed validation." >&2 + return 1 + } + grep -q '^TAPM_ISO_NFS_SELECT_STORAGE()' "$iso_helpers" && + grep -q '^TAPM_DEPLOY_PULSE_LXC()' "$pulse_module" || { + echo "The downloaded files were not recognized as TAPM deployment components." >&2 + return 1 + } + + # shellcheck disable=SC1090 + source "$iso_helpers" + # shellcheck disable=SC1090 + source "$pulse_module" + TAPM_DEPLOY_PULSE_LXC +} + +if [[ "${TAPM_PULSE_STANDALONE_NO_MAIN:-0}" != 1 ]]; then + trap TAPM_CLEAN_ALL_TEMP_DIRS EXIT + TAPM_PULSE_STANDALONE_MAIN "$@" +fi diff --git a/tests/test-pulse-standalone.sh b/tests/test-pulse-standalone.sh new file mode 100755 index 0000000..8363613 --- /dev/null +++ b/tests/test-pulse-standalone.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -u -o pipefail + +TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${TEST_ROOT}/tests/testlib.sh" +TAPM_PULSE_STANDALONE_NO_MAIN=1 +source "${TEST_ROOT}/install-pulse.sh" + +assert_success "default V2 source branch accepted" \ + TAPM_PULSE_VALID_SOURCE_BRANCH V2 +assert_success "nested release branch accepted" \ + TAPM_PULSE_VALID_SOURCE_BRANCH releases/pulse-6 +assert_failure "empty source branch rejected" \ + TAPM_PULSE_VALID_SOURCE_BRANCH '' +assert_failure "source branch traversal rejected" \ + TAPM_PULSE_VALID_SOURCE_BRANCH '../main' +assert_success "HTTPS source base accepted" \ + TAPM_PULSE_VALID_SOURCE_BASE \ + https://git.schroedercity.com/TAI/TA-ProxMenu/raw/branch/V2 +assert_failure "HTTP source base rejected" \ + TAPM_PULSE_VALID_SOURCE_BASE \ + http://git.schroedercity.com/TAI/TA-ProxMenu/raw/branch/V2 + +finish_tests