mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
feat(helper-scripts): an addon runs on the host or inside an LXC, as its catalog says
An addon is run where the catalog lists it: on the Proxmox host, inside an LXC chosen from a list, or wherever is chosen when it can be both. The LXC is started when it is stopped, and OCI containers are left out. Until now every addon ran on the host. A script is downloaded whole before anything runs, so a failed or empty download runs nothing. Descriptions and notes the catalog writes in HTML are shown as plain text. The menu opens one script given as argument, and is as tall as the main menu.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert catalogue HTML to terminal text without altering shell examples."""
|
||||
import re
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
class PlainText(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.parts = []
|
||||
self.hidden = 0
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag in ('script', 'style'):
|
||||
self.hidden += 1
|
||||
elif not self.hidden:
|
||||
if tag in ('p', 'div', 'br', 'ul', 'ol', 'pre', 'blockquote',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
|
||||
self.parts.append('\n')
|
||||
elif tag == 'li':
|
||||
self.parts.append('\n• ')
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag in ('script', 'style'):
|
||||
self.hidden = max(0, self.hidden - 1)
|
||||
elif not self.hidden and tag in ('p', 'div', 'li', 'ul', 'ol', 'pre',
|
||||
'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
|
||||
self.parts.append('\n')
|
||||
|
||||
def handle_data(self, data):
|
||||
if not self.hidden:
|
||||
self.parts.append(data)
|
||||
|
||||
|
||||
def plain_text(value):
|
||||
parser = PlainText()
|
||||
parser.feed(value)
|
||||
parser.close()
|
||||
result = ''.join(parser.parts).replace('\xa0', ' ')
|
||||
result = '\n'.join(line.strip() for line in result.splitlines())
|
||||
return re.sub(r'\n{3,}', '\n\n', result).strip()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout.write(plain_text(sys.stdin.read()))
|
||||
@@ -60,15 +60,22 @@ done
|
||||
# Without these the menu would hang silently for minutes when the network
|
||||
# is down or the upstream is slow, with no signal to the user. Audit
|
||||
# Tier 6 — `menu_Helper_Scripts.sh` curl sin --max-time.
|
||||
CACHE_JSON=$(curl -s --connect-timeout 5 --max-time 15 "$HELPERS_JSON_URL")
|
||||
CACHE_JSON=$(curl -fsSL --connect-timeout 5 --max-time 15 "$HELPERS_JSON_URL")
|
||||
|
||||
# Validate that the JSON loaded correctly
|
||||
if ! echo "$CACHE_JSON" | jq -e 'if type == "array" and length > 0 then true else false end' >/dev/null 2>&1; then
|
||||
dialog --title "Helper Scripts" \
|
||||
--msgbox "Error: Could not load helpers cache.\nCheck your internet connection and try again.\n\nURL: $HELPERS_JSON_URL" 10 70
|
||||
# Opened for one script, as the menu search does: back to where it came from.
|
||||
[[ -n "${1:-}" ]] && exit 1
|
||||
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||
fi
|
||||
|
||||
# The menus are as tall as the main menu, cut to the terminal.
|
||||
MENU_HEIGHT=$(( $(tput lines 2>/dev/null || echo 24) - 2 ))
|
||||
(( MENU_HEIGHT > 24 )) && MENU_HEIGHT=24
|
||||
MENU_ROWS=$(( MENU_HEIGHT - 8 ))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build category map directly from the cache (id → name).
|
||||
# Uses transpose to pair categories[] and category_names[] arrays — no
|
||||
@@ -107,15 +114,99 @@ get_type_label() {
|
||||
# Download and execute a script URL, with optional mirror fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
download_script() {
|
||||
local url="$1"
|
||||
local url="$1" script_content
|
||||
|
||||
# Same timeouts as the cache fetch above so a stalled mirror can't hang
|
||||
# the menu indefinitely. The HEAD probe and the GET both get bounded.
|
||||
if curl --silent --connect-timeout 5 --max-time 10 --head --fail "$url" >/dev/null; then
|
||||
bash <(curl -s --connect-timeout 5 --max-time 60 "$url")
|
||||
else
|
||||
# The script is downloaded whole before anything runs: a download that
|
||||
# fails or comes back empty runs nothing, on the host or in a container.
|
||||
if ! script_content=$(curl -fsSL --connect-timeout 5 --max-time 60 "$url") || [[ -z "${script_content//[[:space:]]/}" ]]; then
|
||||
dialog --title "Helper Scripts" --msgbox "$(translate "Error: Failed to download the script.")" 8 70
|
||||
return 1
|
||||
fi
|
||||
clear
|
||||
if [[ -n "${HELPER_LXC_TARGET:-}" ]]; then
|
||||
pct exec "$HELPER_LXC_TARGET" --keep-env 0 -- \
|
||||
env TERM="${TERM:-xterm}" bash -c "$script_content"
|
||||
else
|
||||
bash -c "$script_content"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Where an addon runs. The catalog says it for each one: on the Proxmox host,
|
||||
# inside a container, or either. Everything that is not an addon runs on the
|
||||
# host, which is where a script that creates a container or a VM belongs.
|
||||
# $1 type $2 the "execute_in" of the catalog, separated by spaces
|
||||
# Prints host, lxc or ask.
|
||||
# ---------------------------------------------------------------------------
|
||||
helper_target_choice() {
|
||||
local type="$1" places=" $2 " host=0 lxc=0
|
||||
[[ "$type" == addon ]] || { echo host; return; }
|
||||
[[ "$places" == *" pve "* ]] && host=1
|
||||
[[ "$places" == *" lxc "* ]] && lxc=1
|
||||
if (( host == 1 && lxc == 0 )); then
|
||||
echo host
|
||||
elif (( lxc == 1 && host == 0 )); then
|
||||
echo lxc
|
||||
else
|
||||
echo ask
|
||||
fi
|
||||
}
|
||||
|
||||
# The containers an addon can be run in: every LXC but the OCI ones, which
|
||||
# are an application image and not a system to add things to.
|
||||
helper_lxc_rows() {
|
||||
local ctid status lock name
|
||||
while read -r ctid status lock name; do
|
||||
[[ "$ctid" =~ ^[0-9]+$ ]] || continue
|
||||
pmx_lxc_is_oci "$ctid" && continue
|
||||
# pct list leaves the Lock column blank for a container that has none.
|
||||
[[ -n "$name" ]] || { name="$lock"; lock=""; }
|
||||
printf '%s\t%s · %s%s\n' "$ctid" "$name" "$status" "${lock:+ · $lock}"
|
||||
done < <(pct list)
|
||||
}
|
||||
|
||||
# Asks where to run an addon and leaves the container in HELPER_LXC_TARGET,
|
||||
# empty for the host. Returns 1 when nothing has to run.
|
||||
# $1 name of the addon $2 host, lxc or ask
|
||||
select_helper_target() {
|
||||
HELPER_LXC_TARGET=""
|
||||
local title="$1" target="$2" ctid label state
|
||||
[[ "$target" == host ]] && return 0
|
||||
if [[ "$target" == ask ]]; then
|
||||
target=$(dialog --clear --backtitle "ProxMenux" --title "$title" \
|
||||
--menu "$(translate "Where do you want to run this addon?")" 12 75 2 \
|
||||
lxc "$(translate "In an LXC")" \
|
||||
host "$(translate "On the host")" 3>&1 1>&2 2>&3) || return 1
|
||||
[[ "$target" == host ]] && return 0
|
||||
fi
|
||||
[[ "$target" == lxc ]] || return 1
|
||||
local -a containers=()
|
||||
while IFS=$'\t' read -r ctid label; do
|
||||
containers+=("$ctid" "$label")
|
||||
done < <(helper_lxc_rows)
|
||||
if [[ ${#containers[@]} -eq 0 ]]; then
|
||||
dialog --title "$title" --msgbox "$(translate "There are no LXC containers available. OCI containers are not included.")" 9 75
|
||||
return 1
|
||||
fi
|
||||
ctid=$(dialog --clear --backtitle "ProxMenux" --title "$title · LXC" \
|
||||
--menu "$(translate "Select the LXC. OCI containers are not included.")" "$MENU_HEIGHT" 75 "$MENU_ROWS" \
|
||||
"${containers[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
[[ "$ctid" =~ ^[0-9]+$ && -f "/etc/pve/lxc/$ctid.conf" ]] || return 1
|
||||
pmx_lxc_is_oci "$ctid" && return 1
|
||||
state=$(pct status "$ctid") || return 1
|
||||
if [[ "$state" != "status: running" ]]; then
|
||||
dialog --backtitle "ProxMenux" --title "$title · LXC $ctid" \
|
||||
--yesno "\n$(translate "The LXC is stopped. Do you want to start it to run the addon?")" 10 75 || return 1
|
||||
if ! pct start "$ctid"; then
|
||||
dialog --title "$title · LXC $ctid" --msgbox "$(translate "The LXC could not be started.")" 8 75
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if ! pct exec "$ctid" --keep-env 0 -- sh -c 'command -v bash >/dev/null'; then
|
||||
dialog --title "$title · LXC $ctid" --msgbox "$(translate "The LXC has no Bash to run this script with. The addon was not run.")" 9 75
|
||||
return 1
|
||||
fi
|
||||
HELPER_LXC_TARGET="$ctid"
|
||||
}
|
||||
|
||||
RETURN_TO_MAIN=false
|
||||
@@ -172,13 +263,15 @@ run_script_by_slug() {
|
||||
local first="${script_infos[0]}"
|
||||
local name desc notes warnings port website
|
||||
name=$(decode "$first" ".name")
|
||||
desc=$(decode "$first" ".desc")
|
||||
notes=$(decode "$first" '.notes // [] | join("\n")')
|
||||
# The catalog writes some of its texts in HTML.
|
||||
local plain="$LOCAL_SCRIPTS/menus/helper_plain_text.py"
|
||||
desc=$(decode "$first" ".desc" | python3 "$plain")
|
||||
notes=$(decode "$first" '.notes // [] | join("\n")' | python3 "$plain")
|
||||
# Sprint 11.7: PocketBase upstream tags certain notes with type=="warning".
|
||||
# The cache generator splits them into a separate `warnings` array so the
|
||||
# menu can render them in red with a dedicated WARNINGS header instead of
|
||||
# burying them inside the regular notes block.
|
||||
warnings=$(decode "$first" '.warnings // [] | join("\n")')
|
||||
warnings=$(decode "$first" '.warnings // [] | join("\n")' | python3 "$plain")
|
||||
port=$(decode "$first" ".port // 0")
|
||||
website=$(decode "$first" ".website // empty")
|
||||
|
||||
@@ -271,6 +364,10 @@ run_script_by_slug() {
|
||||
gh_url=$(decode "$selected" ".script_url")
|
||||
mirror_url=$(decode "$selected" ".script_url_mirror // empty")
|
||||
|
||||
# An addon is run where the catalog says it belongs.
|
||||
select_helper_target "$name" "$(helper_target_choice \
|
||||
"$(decode "$selected" '.type // empty')" "$(decode "$selected" '.execute_in // [] | join(" ")')")" || return
|
||||
|
||||
if [[ "$sel_src" == "GH" ]]; then
|
||||
download_script "$gh_url"
|
||||
elif [[ "$sel_src" == "MR" ]]; then
|
||||
@@ -388,7 +485,7 @@ search_and_filter_scripts() {
|
||||
selected=$(dialog --colors --backtitle "ProxMenux" \
|
||||
--title "$title" \
|
||||
--menu "$(translate "Select a script or action:")" \
|
||||
22 75 15 "${menu_items[@]}" 3>&1 1>&2 2>&3)
|
||||
"$MENU_HEIGHT" 75 "$MENU_ROWS" "${menu_items[@]}" 3>&1 1>&2 2>&3)
|
||||
|
||||
[[ $? -ne 0 ]] && return
|
||||
|
||||
@@ -416,6 +513,12 @@ search_and_filter_scripts() {
|
||||
done
|
||||
}
|
||||
|
||||
# A script given as argument, as the menu search does, is opened on its own.
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
run_script_by_slug "$1"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop — category list built from embedded category data.
|
||||
# We map scriptcatXXXXX IDs to short numeric indices so dialog doesn't show
|
||||
@@ -442,7 +545,7 @@ while true; do
|
||||
SELECTED_IDX=$(dialog --backtitle "ProxMenux" \
|
||||
--title "Proxmox VE Helper-Scripts" \
|
||||
--menu "$(translate "Select a category or search for scripts:")" \
|
||||
22 75 15 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
|
||||
"$MENU_HEIGHT" 75 "$MENU_ROWS" "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
|
||||
dialog --clear --title "ProxMenux" \
|
||||
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
|
||||
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||
@@ -484,7 +587,7 @@ while true; do
|
||||
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" \
|
||||
--title "$(translate "Scripts in") ${CATEGORY_NAMES[$SELECTED]}" \
|
||||
--menu "$(translate "Choose a script to execute:"):" \
|
||||
22 75 15 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
|
||||
"$MENU_HEIGHT" 75 "$MENU_ROWS" "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
|
||||
|
||||
SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}"
|
||||
run_script_by_slug "$SCRIPT_SELECTED"
|
||||
|
||||
Reference in New Issue
Block a user