Files
DynastyRevolution-Scripts/core/build.func

4889 lines
162 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
variables() {
NSAPP=$(echo "${APP,,}" | tr -d ' ')
var_install="${NSAPP}-install"
INTEGER='^[0-9]+([.][0-9]+)?$'
PVEHOST_NAME=$(hostname)
DIAGNOSTICS="no"
METHOD="default"
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
EXECUTION_ID="${RANDOM_UUID}"
SESSION_ID="${RANDOM_UUID:0:8}"
BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log"
CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
parse_dev_mode
if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then
mkdir -p /var/log/dynr
BUILD_LOG="/var/log/dynr/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log"
fi
if command -v pveversion >/dev/null 2>&1; then
PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
else
PVEVERSION="N/A"
fi
KERNEL_VERSION=$(uname -r)
if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then
export APP_DEFAULT_CPU="${var_cpu}"
fi
if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then
export APP_DEFAULT_RAM="${var_ram}"
fi
if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then
export APP_DEFAULT_DISK="${var_disk}"
fi
}
source <(curl -fsSL https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/core/api.func)
if command -v curl >/dev/null 2>&1; then
source <(curl -fsSL https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/core/core.func)
source <(curl -fsSL https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/core/error_handler.func)
load_functions
catch_errors
elif command -v wget >/dev/null 2>&1; then
source <(wget -qO- https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/core/core.func)
source <(wget -qO- https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/core/error_handler.func)
load_functions
catch_errors
fi
maxkeys_check() {
per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
msg_error "Unable to read kernel key parameters. Ensure proper permissions."
exit 107
fi
used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
threshold_keys=$((per_user_maxkeys - 100))
threshold_bytes=$((per_user_maxbytes - 1000))
new_limit_keys=$((per_user_maxkeys * 2))
new_limit_bytes=$((per_user_maxbytes * 2))
failure=0
if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
msg_warn "Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys})"
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-dynr.conf${CL}."
failure=1
fi
if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
msg_warn "Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes})"
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-dynr.conf${CL}."
failure=1
fi
if [[ "$failure" -eq 1 ]]; then
msg_error "Kernel key limits exceeded - see suggestions above"
exit 108
fi
}
get_current_ip() {
CURRENT_IP=""
if [ -f /etc/os-release ]; then
if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)
if [[ -z "$CURRENT_IP" ]]; then
CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1 || true)
fi
elif grep -q 'ID=alpine' /etc/os-release; then
CURRENT_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
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
fi
echo "$CURRENT_IP"
}
update_motd_ip() {
MOTD_FILE="/etc/motd"
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
if [ -f "$MOTD_FILE" ]; then
sed -i '/IP Address:/d' "$MOTD_FILE"
IP=$(get_current_ip)
echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
fi
if [ -f "$PROFILE_FILE" ] && grep -q "dynr" "$PROFILE_FILE" 2>/dev/null; then
local current_os="$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - Version: $(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')"
local current_hostname="$(hostname)"
local current_ip="$(hostname -I | awk '{print $1}')"
current_os="${current_os//\\/\\\\}"
current_os="${current_os//&/\\&}"
current_hostname="${current_hostname//\\/\\\\}"
current_hostname="${current_hostname//&/\\&}"
current_ip="${current_ip//\\/\\\\}"
current_ip="${current_ip//&/\\&}"
if ! grep -q "OS:.*$current_os" "$PROFILE_FILE" 2>/dev/null; then
sed -i "s|OS:.*|OS: \${GN}$current_os\${CL}\\\"|" "$PROFILE_FILE"
fi
if ! grep -q "Hostname:.*$current_hostname" "$PROFILE_FILE" 2>/dev/null; then
sed -i "s|Hostname:.*|Hostname: \${GN}$current_hostname\${CL}\\\"|" "$PROFILE_FILE"
fi
if ! grep -q "IP Address:.*$current_ip" "$PROFILE_FILE" 2>/dev/null; then
sed -i "s|IP Address:.*|IP Address: \${GN}$current_ip\${CL}\\\"|" "$PROFILE_FILE"
fi
fi
}
install_ssh_keys_into_ct() {
[[ "${SSH:-no}" != "yes" ]] && return 0
: "${SSH_KEYS_FILE:=}"
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' || {
msg_error "prepare /root/.ssh failed"
return 252
}
pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 ||
pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || {
msg_error "write authorized_keys failed"
return 252
}
pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
msg_ok "Installed SSH keys into CT ${CTID}"
return 0
fi
msg_warn "No SSH keys to install (skipping)."
return 0
}
validate_container_id() {
local ctid="$1"
if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then
return 1
fi
if command -v pvesh &>/dev/null; then
local cluster_ids
cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true)
if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then
return 1
fi
fi
if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
return 1
fi
if [[ -d "/etc/pve/nodes" ]]; then
for node_dir in /etc/pve/nodes/*/; do
if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then
return 1
fi
done
fi
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
return 1
fi
return 0
}
get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}"
if ! [[ "$suggested_id" =~ ^[0-9]+$ ]]; then
suggested_id=$(pvesh get /cluster/nextid 2>/dev/null || echo 100)
fi
local max_attempts=1000
local attempts=0
while ! validate_container_id "$suggested_id"; do
suggested_id=$((suggested_id + 1))
attempts=$((attempts + 1))
if [[ $attempts -ge $max_attempts ]]; then
msg_error "Could not find available container ID after $max_attempts attempts"
exit 109
fi
done
echo "$suggested_id"
}
validate_hostname() {
local hostname="$1"
if [[ ${#hostname} -gt 253 ]] || [[ -z "$hostname" ]]; then
return 1
fi
local IFS='.'
read -ra labels <<<"$hostname"
for label in "${labels[@]}"; do
if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then
return 1
fi
if [[ ! "$label" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] && [[ ! "$label" =~ ^[a-z0-9]$ ]]; then
return 1
fi
done
return 0
}
validate_mac_address() {
local mac="$1"
[[ -z "$mac" ]] && return 0
if [[ ! "$mac" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then
return 1
fi
return 0
}
validate_vlan_tag() {
local vlan="$1"
[[ -z "$vlan" ]] && return 0
if ! [[ "$vlan" =~ ^[0-9]+$ ]] || ((vlan < 1 || vlan > 4094)); then
return 1
fi
return 0
}
validate_mtu() {
local mtu="$1"
[[ -z "$mtu" ]] && return 0
if ! [[ "$mtu" =~ ^[0-9]+$ ]] || ((mtu < 576 || mtu > 65535)); then
return 1
fi
return 0
}
validate_ipv6_address() {
local ipv6="$1"
[[ -z "$ipv6" ]] && return 0
local addr="${ipv6%%/*}"
local cidr="${ipv6##*/}"
if [[ "$ipv6" == */* ]]; then
if ! [[ "$cidr" =~ ^[0-9]+$ ]] || ((cidr < 1 || cidr > 128)); then
return 1
fi
fi
if [[ ! "$addr" =~ ^[0-9a-fA-F:]+$ ]]; then
return 1
fi
if [[ ! "$addr" == *:* ]]; then
return 1
fi
if [[ "$addr" == *::*::* ]]; then
return 1
fi
local IFS=':'
local -a segments
read -ra segments <<<"$addr"
for seg in "${segments[@]}"; do
if [[ ${#seg} -gt 4 ]]; then
return 1
fi
done
return 0
}
validate_bridge() {
local bridge="$1"
[[ -z "$bridge" ]] && return 1
if ! ip link show "$bridge" &>/dev/null; then
return 1
fi
return 0
}
validate_gateway_in_subnet() {
local static_ip="$1"
local gateway="$2"
[[ -z "$static_ip" || -z "$gateway" ]] && return 0
local ip="${static_ip%%/*}"
local cidr="${static_ip##*/}"
((cidr >= 31)) && return 0
local mask=$((0xFFFFFFFF << (32 - cidr) & 0xFFFFFFFF))
local IFS='.'
read -r i1 i2 i3 i4 <<<"$ip"
read -r g1 g2 g3 g4 <<<"$gateway"
local ip_int=$(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
local gw_int=$(((g1 << 24) + (g2 << 16) + (g3 << 8) + g4))
if (((ip_int & mask) != (gw_int & mask))); then
return 1
fi
return 0
}
validate_ip_address() {
local ip="$1"
[[ -z "$ip" ]] && return 1
if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$ ]]; then
return 1
fi
local o1="${BASH_REMATCH[1]}"
local o2="${BASH_REMATCH[2]}"
local o3="${BASH_REMATCH[3]}"
local o4="${BASH_REMATCH[4]}"
local cidr="${BASH_REMATCH[5]}"
for octet in "$o1" "$o2" "$o3" "$o4"; do
if ((octet > 255)); then
return 1
fi
done
if ((cidr < 1 || cidr > 32)); then
return 1
fi
return 0
}
validate_gateway_ip() {
local ip="$1"
[[ -z "$ip" ]] && return 0
if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
return 1
fi
local o1="${BASH_REMATCH[1]}"
local o2="${BASH_REMATCH[2]}"
local o3="${BASH_REMATCH[3]}"
local o4="${BASH_REMATCH[4]}"
for octet in "$o1" "$o2" "$o3" "$o4"; do
if ((octet > 255)); then
return 1
fi
done
return 0
}
validate_timezone() {
local tz="$1"
[[ -z "$tz" || "$tz" == "host" ]] && return 0
if [[ ! -f "/usr/share/zoneinfo/$tz" ]]; then
return 1
fi
return 0
}
validate_tags() {
local tags="$1"
[[ -z "$tags" ]] && return 0
if [[ ! "$tags" =~ ^[a-zA-Z0-9_\;-]+$ ]]; then
return 1
fi
return 0
}
find_host_ssh_keys() {
local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
local -a files=() cand=()
local g="${var_ssh_import_glob:-}"
local total=0 f base c
shopt -s nullglob
if [[ -n "$g" ]]; then
for pat in $g; do cand+=($pat); done
else
cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
cand+=(/root/.ssh/*.pub)
cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
fi
shopt -u nullglob
for f in "${cand[@]}"; do
[[ -f "$f" && -r "$f" ]] || continue
base="$(basename -- "$f")"
case "$base" in
known_hosts | known_hosts.* | config) continue ;;
id_*) [[ "$f" != *.pub ]] && continue ;;
esac
c=$(tr -d '\r' <"$f" | awk '
/^[[:space:]]*#/ {next}
/^[[:space:]]*$/ {next}
{print}
' | grep -E -c "$re" || true)
if ((c > 0)); then
files+=("$f")
total=$((total + c))
fi
done
if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
if grep -E -q "$re" /root/.ssh/authorized_keys; then
files+=(/root/.ssh/authorized_keys)
total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
fi
fi
FOUND_HOST_KEY_COUNT="$total"
(
IFS=:
echo "${files[*]}"
)
}
ip_to_int() {
local IFS=.
read -r i1 i2 i3 i4 <<<"$1"
echo $(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
}
int_to_ip() {
local ip=$1
echo "$(((ip >> 24) & 0xFF)).$(((ip >> 16) & 0xFF)).$(((ip >> 8) & 0xFF)).$((ip & 0xFF))"
}
resolve_ip_from_range() {
local range="$1"
local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$'
local ip_start ip_end
ip_start="${range%%-*}"
ip_end="${range##*-}"
if [[ ! "$ip_start" =~ $ip_cidr_regex ]] || [[ ! "$ip_end" =~ $ip_cidr_regex ]]; then
NET_RESOLVED=""
return 1
fi
local ip1="${ip_start%%/*}"
local ip2="${ip_end%%/*}"
local cidr="${ip_start##*/}"
local start_int=$(ip_to_int "$ip1")
local end_int=$(ip_to_int "$ip2")
for ((ip_int = start_int; ip_int <= end_int; ip_int++)); do
local ip=$(int_to_ip $ip_int)
msg_info "Checking IP: $ip"
if ! ping -c 1 -W 1 "$ip" >/dev/null 2>&1; then
NET_RESOLVED="$ip/$cidr"
msg_ok "Found free IP: ${BGN}$NET_RESOLVED${CL}"
return 0
fi
done
NET_RESOLVED=""
msg_error "No free IP found in range $range"
return 1
}
is_ip_range() {
local value="$1"
local ip_start ip_end
if [[ "$value" == *-* ]] && [[ "$value" != "dhcp" ]]; then
local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$'
ip_start="${value%%-*}"
ip_end="${value##*-}"
if [[ "$ip_start" =~ $ip_cidr_regex ]] && [[ "$ip_end" =~ $ip_cidr_regex ]]; then
return 0
fi
fi
return 1
}
_write_storage_to_vars() {
local vf="$1" key="$2" val="$3"
sed -i "/^[#[:space:]]*${key}=/d" "$vf"
echo "${key}=${val}" >>"$vf"
}
choose_and_set_storage_for_file() {
local vf="$1" class="$2" key="" current=""
case "$class" in
container) key="var_container_storage" ;;
template) key="var_template_storage" ;;
*)
msg_error "Unknown storage class: $class"
return 65
;;
esac
current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
local content="rootdir"
[[ "$class" == "template" ]] && content="vztmpl"
local count
count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
if [[ "$count" -eq 1 ]]; then
STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
STORAGE_INFO=""
if [[ "$class" == "container" && -n "${DISK_SIZE:-}" ]]; then
validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes"
fi
else
select_storage "$class" || return 150
fi
_write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
if [[ "$class" == "container" ]]; then
export var_container_storage="$STORAGE_RESULT"
export CONTAINER_STORAGE="$STORAGE_RESULT"
else
export var_template_storage="$STORAGE_RESULT"
export TEMPLATE_STORAGE="$STORAGE_RESULT"
fi
}
base_settings() {
CT_TYPE=${var_unprivileged:-"1"}
local final_disk="${var_disk:-4}"
local final_cpu="${var_cpu:-1}"
local final_ram="${var_ram:-1024}"
if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then
if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then
final_disk="${APP_DEFAULT_DISK}"
fi
fi
if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then
if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then
final_cpu="${APP_DEFAULT_CPU}"
fi
fi
if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then
if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then
final_ram="${APP_DEFAULT_RAM}"
fi
fi
DISK_SIZE="${final_disk}"
CORE_COUNT="${final_cpu}"
RAM_SIZE="${final_ram}"
VERBOSE=${var_verbose:-"${1:-no}"}
PW=""
if [[ -n "${var_pw:-}" ]]; then
local _pw_raw="${var_pw}"
case "$_pw_raw" in
--password\ *) _pw_raw="${_pw_raw#--password }" ;;
-password\ *) _pw_raw="${_pw_raw#-password }" ;;
esac
while [[ "$_pw_raw" == -* ]]; do
_pw_raw="${_pw_raw#-}"
done
if [[ -z "$_pw_raw" ]]; then
msg_warn "Password was only dashes after cleanup; leaving empty."
else
PW="--password $_pw_raw"
fi
fi
local requested_id="${var_ctid:-$NEXTID}"
if ! validate_container_id "$requested_id"; then
if [[ -n "${var_ctid:-}" ]]; then
msg_warn "Container ID $requested_id is already in use. Using next available ID: $(get_valid_container_id "$requested_id")"
fi
requested_id=$(get_valid_container_id "$requested_id")
fi
CT_ID="$requested_id"
local requested_hostname="${var_hostname:-$NSAPP}"
requested_hostname=$(echo "${requested_hostname,,}" | tr -d ' ')
if ! validate_hostname "$requested_hostname"; then
if [[ -n "${var_hostname:-}" ]]; then
msg_warn "Invalid hostname '$requested_hostname'. Using default: $NSAPP"
fi
requested_hostname="$NSAPP"
fi
HN="$requested_hostname"
BRG=${var_brg:-"vmbr0"}
NET=${var_net:-"dhcp"}
if is_ip_range "$NET"; then
msg_info "Scanning IP range: $NET"
if resolve_ip_from_range "$NET"; then
NET="$NET_RESOLVED"
else
msg_error "Could not find free IP in range. Falling back to DHCP."
NET="dhcp"
fi
fi
IPV6_METHOD=${var_ipv6_method:-"none"}
IPV6_STATIC=${var_ipv6_static:-""}
GATE=${var_gateway:-""}
APT_CACHER=${var_apt_cacher:-""}
APT_CACHER_IP=${var_apt_cacher_ip:-""}
if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then
if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then
msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142"
msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation"
APT_CACHER=""
APT_CACHER_IP=""
else
msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142"
fi
fi
MTU=${var_mtu:-""}
_sd_val="${var_searchdomain:-""}"
[[ -n "$_sd_val" ]] && SD="-searchdomain=$_sd_val" || SD=""
_ns_val="${var_ns:-""}"
[[ -n "$_ns_val" ]] && NS="-nameserver=$_ns_val" || NS=""
MAC=${var_mac:-""}
VLAN=${var_vlan:-""}
SSH=${var_ssh:-"no"}
SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
UDHCPC_FIX=${var_udhcpc_fix:-""}
TAGS="dynasty-script,${var_tags:-}"
ENABLE_FUSE=${var_fuse:-"${1:-no}"}
ENABLE_TUN=${var_tun:-"${1:-no}"}
ENABLE_GPU=${var_gpu:-"no"}
ENABLE_NESTING=${var_nesting:-"1"}
ENABLE_KEYCTL=${var_keyctl:-"0"}
ENABLE_MKNOD=${var_mknod:-"0"}
PROTECT_CT=${var_protection:-"no"}
CT_TIMEZONE=${var_timezone:-"$timezone"}
[[ "${CT_TIMEZONE:-}" == Etc/* ]] && CT_TIMEZONE="host"
if [ -z "$var_os" ]; then
var_os="debian"
fi
if [ -z "$var_version" ]; then
var_version="12"
fi
}
load_vars_file() {
local file="$1"
local force="${2:-no}"
[ -f "$file" ] || return 0
msg_info "Loading defaults from ${file}"
local VAR_WHITELIST=(
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain
)
_is_whitelisted() {
local k="$1" w
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
return 1
}
local line key val
while IFS= read -r line || [ -n "$line" ]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" || "$line" == \#* ]] && continue
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
local var_key="${BASH_REMATCH[1]}"
local var_val="${BASH_REMATCH[2]}"
[[ "$var_key" != var_* ]] && continue
_is_whitelisted "$var_key" || continue
if [[ ! "$var_val" =~ ^[\"\'] ]]; then
var_val="${var_val%%#*}"
fi
if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
var_val="${BASH_REMATCH[1]}"
elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
var_val="${BASH_REMATCH[1]}"
fi
var_val="${var_val%"${var_val##*[![:space:]]}"}"
if [[ -n "$var_val" ]]; then
case "$var_key" in
var_mac)
if ! validate_mac_address "$var_val"; then
msg_warn "Invalid MAC address '$var_val' in $file, ignoring"
continue
fi
;;
var_vlan)
if ! validate_vlan_tag "$var_val"; then
msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring"
continue
fi
;;
var_mtu)
if ! validate_mtu "$var_val"; then
msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring"
continue
fi
;;
var_tags)
if ! validate_tags "$var_val"; then
msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring"
continue
fi
;;
var_timezone)
if ! validate_timezone "$var_val"; then
msg_warn "Invalid timezone '$var_val' in $file, ignoring"
continue
fi
;;
var_brg)
if ! validate_bridge "$var_val"; then
msg_warn "Bridge '$var_val' not found in $file, ignoring"
continue
fi
;;
var_gateway)
if ! validate_gateway_ip "$var_val"; then
msg_warn "Invalid gateway IP '$var_val' in $file, ignoring"
continue
fi
;;
var_hostname)
if ! validate_hostname "$var_val"; then
msg_warn "Invalid hostname '$var_val' in $file, ignoring"
continue
fi
;;
var_cpu)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then
msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring"
continue
fi
;;
var_ram)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then
msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring"
continue
fi
;;
var_disk)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then
msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring"
continue
fi
;;
var_unprivileged)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_nesting)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
if [[ "$var_val" == "0" && "${var_os:-debian}" != "alpine" ]]; then
msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation"
fi
;;
var_keyctl)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_net)
if [[ "$var_val" != "dhcp" ]]; then
if is_ip_range "$var_val"; then
:
elif ! validate_ip_address "$var_val"; then
msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring"
continue
fi
fi
;;
var_fuse | var_tun | var_gpu | var_ssh | var_verbose | var_protection)
if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then
msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring"
continue
fi
;;
var_ipv6_method)
if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then
msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring"
continue
fi
;;
var_container_storage | var_template_storage)
local _storage_status
_storage_status=$(pvesm status 2>/dev/null | awk -v s="$var_val" '$1 == s { print $3 }')
if [[ -z "$_storage_status" ]]; then
msg_warn "Storage '$var_val' from $file not found on this node, ignoring"
continue
elif [[ "$_storage_status" == "disabled" ]]; then
msg_warn "Storage '$var_val' from $file is disabled on this node, ignoring"
continue
fi
;;
esac
fi
if [[ "$force" == "yes" ]]; then
export "${var_key}=${var_val}"
else
[[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
fi
fi
done <"$file"
msg_ok "Loaded ${file}"
}
default_var_settings() {
local VAR_WHITELIST=(
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
)
declare -A _HARD_ENV=()
local _k
for _k in "${VAR_WHITELIST[@]}"; do
if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
done
local _find_default_vars
_find_default_vars() {
local f
for f in \
/opt/dynr/default.vars \
"$HOME/.dynr/default.vars" \
"./default.vars"; do
[ -f "$f" ] && {
echo "$f"
return 0
}
done
return 252
}
[ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
[ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
local _ensure_default_vars
_ensure_default_vars() {
_find_default_vars >/dev/null 2>&1 && return 0
local canonical="/opt/dynr/default.vars"
mkdir -p /opt/dynr
: >"$canonical"
cat >"$canonical" <<'EOF'
var_unprivileged=1
var_cpu=1
var_disk=4
var_ram=1024
var_brg=vmbr0
var_net=dhcp
var_ipv6_method=none
var_ssh=no
var_fuse=no
var_tun=no
var_nesting=1
var_keyctl=0
var_mknod=0
var_protection=no
var_tags=dynasty-script
var_verbose=no
EOF
choose_and_set_storage_for_file "$canonical" template
choose_and_set_storage_for_file "$canonical" container
chmod 0644 "$canonical"
}
local _is_whitelisted_key
_is_whitelisted_key() {
local k="$1"
local w
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
return 1
}
_ensure_default_vars
local dv
dv="$(_find_default_vars)" || {
msg_error "default.vars not found after ensure step"
return 252
}
load_vars_file "$dv"
if [[ -n "${var_verbose:-}" ]]; then
case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
else
VERBOSE="no"
fi
METHOD="mydefaults-global"
base_settings "$VERBOSE"
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using User Defaults (default.vars) on node $PVEHOST_NAME${CL}"
echo_default
}
get_app_defaults_path() {
local n="${NSAPP:-${APP,,}}"
echo "/opt/dynr/defaults/${n}.vars"
}
if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
declare -ag VAR_WHITELIST=(
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu
var_gateway var_hostname var_ipv6_method var_mac var_mtu
var_net var_ns var_os var_pw var_ram var_tags var_tun var_unprivileged
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
)
fi
_is_whitelisted_key() {
local k="$1"
local w
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
return 1
}
_sanitize_value() {
case "$1" in
*'$('* | *'`'* | *';'* | *'&'* | *'<('*)
echo ""
return 0
;;
esac
echo "$1"
}
declare -A _VARS_IN
_load_vars_file_to_map() {
local file="$1"
[ -f "$file" ] || return 0
_VARS_IN=()
local line key val
while IFS= read -r line || [ -n "$line" ]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[ -z "$line" ] && continue
case "$line" in
\#*) continue ;;
esac
key=$(printf "%s" "$line" | cut -d= -f1)
val=$(printf "%s" "$line" | cut -d= -f2-)
case "$key" in
var_*)
if _is_whitelisted_key "$key"; then
_VARS_IN["$key"]="$val"
fi
;;
esac
done <"$file"
}
_build_vars_diff() {
local oldf="$1" newf="$2"
local k
local -A OLD=() NEW=()
_load_vars_file_to_map "$oldf"
for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
_load_vars_file_to_map "$newf"
for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
local out
out+="# Diff for ${APP} (${NSAPP})\n"
out+="# Old: ${oldf}\n# New: ${newf}\n\n"
local found_change=0
for k in "${!OLD[@]}"; do
if [[ -v NEW["$k"] ]]; then
if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
found_change=1
fi
else
out+="- ${k}\n - old: ${OLD[$k]}\n"
found_change=1
fi
done
for k in "${!NEW[@]}"; do
if [[ ! -v OLD["$k"] ]]; then
out+="+ ${k}\n + new: ${NEW[$k]}\n"
found_change=1
fi
done
if [[ $found_change -eq 0 ]]; then
out+="(No differences)\n"
fi
printf "%b" "$out"
}
_build_current_app_vars_tmp() {
tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
_net="${NET:-}"
_gate=""
case "${GATE:-}" in
,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
esac
_ipv6_method="${IPV6_METHOD:-auto}"
_ipv6_static=""
_ipv6_gateway=""
if [ "$_ipv6_method" = "static" ]; then
_ipv6_static="${IPV6_ADDR:-}"
_ipv6_gateway="${IPV6_GATE:-}"
fi
_mtu=""
_vlan=""
_mac=""
case "${MTU:-}" in
,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
esac
case "${VLAN:-}" in
,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
esac
case "${MAC:-}" in
,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
esac
_ns=""
_searchdomain=""
case "${NS:-}" in
-nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
esac
case "${SD:-}" in
-searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
esac
_ssh="${SSH:-no}"
_ssh_auth="${SSH_AUTHORIZED_KEY:-}"
_apt_cacher="${APT_CACHER:-}"
_apt_cacher_ip="${APT_CACHER_IP:-}"
_fuse="${ENABLE_FUSE:-no}"
_tun="${ENABLE_TUN:-no}"
_gpu="${ENABLE_GPU:-no}"
_nesting="${ENABLE_NESTING:-1}"
_keyctl="${ENABLE_KEYCTL:-0}"
_mknod="${ENABLE_MKNOD:-0}"
_mount_fs="${ALLOW_MOUNT_FS:-}"
_protect="${PROTECT_CT:-no}"
_timezone="${CT_TIMEZONE:-}"
_tags="${TAGS:-}"
_verbose="${VERBOSE:-no}"
_unpriv="${CT_TYPE:-1}"
_cpu="${CORE_COUNT:-1}"
_ram="${RAM_SIZE:-1024}"
_disk="${DISK_SIZE:-4}"
_hostname="${HN:-$NSAPP}"
_tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
_ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
{
echo "var_os=$(_sanitize_value "${var_os:-}")"
echo "var_version=$(_sanitize_value "${var_version:-}")"
echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
echo "var_cpu=$(_sanitize_value "$_cpu")"
echo "var_ram=$(_sanitize_value "$_ram")"
echo "var_disk=$(_sanitize_value "$_disk")"
[ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
[ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
[ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
[ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
[ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
[ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
[ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
[ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
[ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
[ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
[ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
[ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
[ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
[ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
[ -n "$_gpu" ] && echo "var_gpu=$(_sanitize_value "$_gpu")"
[ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")"
[ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")"
[ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")"
[ -n "$_mount_fs" ] && echo "var_mount_fs=$(_sanitize_value "$_mount_fs")"
[ -n "$_protect" ] && echo "var_protection=$(_sanitize_value "$_protect")"
[ -n "$_timezone" ] && echo "var_timezone=$(_sanitize_value "$_timezone")"
[ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
[ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
[ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
[ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
[ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
[ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
} >"$tmpf"
echo "$tmpf"
}
maybe_offer_save_app_defaults() {
local app_vars_path
app_vars_path="$(get_app_defaults_path)"
local new_tmp diff_tmp
new_tmp="$(_build_current_app_vars_tmp)"
diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
if [[ ! -f "$app_vars_path" ]]; then
if whiptail --backtitle "Dynasty OS Framework" \
--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")"
/usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
msg_ok "Saved app defaults: ${app_vars_path}"
fi
rm -f "$new_tmp" "$diff_tmp"
return 0
fi
_build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
if grep -q "^(No differences)$" "$diff_tmp"; then
rm -f "$new_tmp" "$diff_tmp"
return 0
fi
local app_vars_file
app_vars_file="$(basename "$app_vars_path")"
while true; do
local sel
sel="$(whiptail --backtitle "Dynasty OS Framework" \
--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}" \
"Keep Current" "Keep existing defaults (no changes)" \
"View Diff" "Show a detailed diff" \
"Cancel" "Abort without changes" \
--default-item "Update Defaults" \
3>&1 1>&2 2>&3)" || { sel="Cancel"; }
case "$sel" in
"Update Defaults")
/usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
msg_ok "Updated app defaults: ${app_vars_path}"
break
;;
"Keep Current")
msg_custom "" "${BL}" "Keeping current app defaults: ${app_vars_path}"
break
;;
"View Diff")
whiptail --backtitle "Dynasty OS Framework" \
--title "Diff ${APP}" \
--scrolltext --textbox "$diff_tmp" 25 100
;;
"Cancel" | *)
msg_custom "🚫" "${YW}" "Canceled. No changes to app defaults."
break
;;
esac
done
rm -f "$new_tmp" "$diff_tmp"
}
ensure_storage_selection_for_vars_file() {
local vf="$1"
local tpl ct
tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2- || true)
ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2- || true)
if [[ -n "$tpl" && -n "$ct" ]]; then
TEMPLATE_STORAGE="$tpl"
CONTAINER_STORAGE="$ct"
if [[ -n "${DISK_SIZE:-}" ]]; then
validate_storage_space "$ct" "$DISK_SIZE" "yes"
fi
return 0
fi
choose_and_set_storage_for_file "$vf" template
choose_and_set_storage_for_file "$vf" container
}
ensure_global_default_vars_file() {
local vars_path="/opt/dynr/default.vars"
if [[ ! -f "$vars_path" ]]; then
mkdir -p "$(dirname "$vars_path")"
touch "$vars_path"
fi
echo "$vars_path"
}
advanced_settings() {
tput smcup 2>/dev/null || true
trap 'tput rmcup 2>/dev/null || true' RETURN
TAGS="dynasty-script;${var_tags:-}"
local STEP=1
local MAX_STEP=28
local _ct_type="${var_unprivileged:-1}"
local _pw=""
local _pw_display="Automatic Login"
local _ct_id="$NEXTID"
local _hostname="$NSAPP"
local _disk_size="${var_disk:-4}"
local _core_count="${var_cpu:-1}"
local _ram_size="${var_ram:-1024}"
local _bridge="${var_brg:-vmbr0}"
local _net="${var_net:-dhcp}"
local _gate="${var_gateway:-}"
local _ipv6_method="${var_ipv6_method:-auto}"
local _ipv6_addr=""
local _ipv6_gate=""
local _apt_cacher="${var_apt_cacher:-no}"
local _apt_cacher_ip="${var_apt_cacher_ip:-}"
local _mtu="${var_mtu:-}"
local _sd="${var_searchdomain:-}"
local _ns="${var_ns:-}"
local _mac="${var_mac:-}"
local _vlan="${var_vlan:-}"
local _tags="$TAGS"
local _enable_fuse="${var_fuse:-no}"
local _enable_tun="${var_tun:-no}"
local _enable_gpu="${var_gpu:-no}"
local _enable_nesting="${var_nesting:-1}"
local _verbose="${var_verbose:-no}"
local _enable_keyctl="${var_keyctl:-0}"
local _enable_mknod="${var_mknod:-0}"
local _mount_fs="${var_mount_fs:-}"
local _protect_ct="${var_protection:-no}"
local _host_timezone=""
if command -v timedatectl >/dev/null 2>&1; then
_host_timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "")
elif [ -f /etc/timezone ]; then
_host_timezone=$(cat /etc/timezone 2>/dev/null || echo "")
fi
[[ "${_host_timezone:-}" == Etc/* ]] && _host_timezone="host"
local _ct_timezone="${var_timezone:-$_host_timezone}"
[[ "${_ct_timezone:-}" == Etc/* ]] && _ct_timezone="host"
show_progress() {
local current=$1
local total=$MAX_STEP
echo -e "\n${INFO}${BOLD}${DGN}Step $current of $total${CL}"
}
local BRIDGES=""
local BRIDGE_MENU_OPTIONS=()
_detect_bridges() {
IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f 2>/dev/null)
BRIDGES=""
local OLD_IFS=$IFS
IFS=$'\n'
for iface_filepath in ${IFACE_FILEPATH_LIST}; do
local iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
(grep -Pn '^\s*iface' "${iface_filepath}" 2>/dev/null | cut -d':' -f1 && wc -l "${iface_filepath}" 2>/dev/null | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" 2>/dev/null || true
if [ -f "${iface_indexes_tmpfile}" ]; then
while read -r pair; do
local start=$(echo "${pair}" | cut -d':' -f1)
local end=$(echo "${pair}" | cut -d':' -f2)
if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" 2>/dev/null | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
local iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
fi
done <"${iface_indexes_tmpfile}"
rm -f "${iface_indexes_tmpfile}"
fi
done
IFS=$OLD_IFS
BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
BRIDGE_MENU_OPTIONS=()
if [[ -n "$BRIDGES" ]]; then
while IFS= read -r bridge; do
if [[ -n "$bridge" ]]; then
local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//;s/^[- ]*//' || true)
BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }")
fi
done <<<"$BRIDGES"
fi
}
_detect_bridges
while [ $STEP -le $MAX_STEP ]; do
case $STEP in
1)
local default_on="ON"
local default_off="OFF"
[[ "$_ct_type" == "0" ]] && {
default_on="OFF"
default_off="ON"
}
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "CONTAINER TYPE" \
--ok-button "Next" --cancel-button "Exit" \
--radiolist "\nChoose container type:\n\nUse SPACE to select, ENTER to confirm." 14 58 2 \
"1" "Unprivileged (recommended)" $default_on \
"0" "Privileged" $default_off \
3>&1 1>&2 2>&3); then
[[ -n "$result" ]] && _ct_type="$result"
((STEP++))
else
exit_script
fi
;;
2)
if PW1=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "ROOT PASSWORD" \
--ok-button "Next" --cancel-button "Back" \
--passwordbox "\nSet Root Password (needed for root ssh access)\n\nLeave blank for automatic login (no password)" 12 58 \
3>&1 1>&2 2>&3); then
if [[ -z "$PW1" ]]; then
_pw=""
_pw_display="Automatic Login"
((STEP++))
elif [[ "$PW1" == *" "* ]]; then
whiptail --msgbox "Password cannot contain spaces." 8 58
else
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
continue
fi
if PW2=$(whiptail --backtitle "Dynasty OS Framework [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
whiptail --msgbox "Passwords do not match. Please try again." 8 58
fi
else
((STEP--))
fi
fi
else
((STEP--))
fi
;;
3)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "CONTAINER ID" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Container ID" 10 58 "$_ct_id" \
3>&1 1>&2 2>&3); then
local input_id="${result:-$NEXTID}"
if ! [[ "$input_id" =~ ^[0-9]+$ ]]; then
whiptail --backtitle "Dynasty OS Framework" --title "Invalid ID" --msgbox "Container ID must be numeric." 8 58
continue
fi
if ! validate_container_id "$input_id"; then
if whiptail --backtitle "Dynasty OS Framework" --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")
else
continue
fi
else
_ct_id="$input_id"
fi
((STEP++))
else
((STEP--))
fi
;;
4)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "HOSTNAME" \
--ok-button "Next" --cancel-button "Back" \
--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 validate_hostname "$hn_test"; then
_hostname="$hn_test"
((STEP++))
else
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--))
fi
;;
5)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "DISK SIZE" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Disk Size in GB" 10 58 "$_disk_size" \
3>&1 1>&2 2>&3); then
local disk_test="${result:-$var_disk}"
if [[ "$disk_test" =~ ^[1-9][0-9]*$ ]]; then
_disk_size="$disk_test"
((STEP++))
else
whiptail --msgbox "Disk size must be a positive integer!" 8 58
fi
else
((STEP--))
fi
;;
6)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "CPU CORES" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nAllocate CPU Cores" 10 58 "$_core_count" \
3>&1 1>&2 2>&3); then
local cpu_test="${result:-$var_cpu}"
if [[ "$cpu_test" =~ ^[1-9][0-9]*$ ]]; then
_core_count="$cpu_test"
((STEP++))
else
whiptail --msgbox "CPU core count must be a positive integer!" 8 58
fi
else
((STEP--))
fi
;;
7)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "RAM SIZE" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nAllocate RAM in MiB" 10 58 "$_ram_size" \
3>&1 1>&2 2>&3); then
local ram_test="${result:-$var_ram}"
if [[ "$ram_test" =~ ^[1-9][0-9]*$ ]]; then
_ram_size="$ram_test"
((STEP++))
else
whiptail --msgbox "RAM size must be a positive integer!" 8 58
fi
else
((STEP--))
fi
;;
8)
if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then
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
msg_error "Default bridge 'vmbr0' not found"
exit 116
fi
else
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "NETWORK BRIDGE" \
--ok-button "Next" --cancel-button "Back" \
--menu "\nSelect network bridge:" 16 58 6 \
"${BRIDGE_MENU_OPTIONS[@]}" \
3>&1 1>&2 2>&3); then
local bridge_test="${result:-vmbr0}"
if [[ "$bridge_test" == "__other__" || "$bridge_test" == -* ]]; then
continue
fi
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
fi
;;
9)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "IPv4 CONFIGURATION" \
--ok-button "Next" --cancel-button "Back" \
--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
local static_ip
if static_ip=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "STATIC IPv4 ADDRESS" \
--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 validate_ip_address "$static_ip"; then
local gateway_ip
if gateway_ip=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "GATEWAY IP" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nEnter Gateway IP address" 10 58 "" \
3>&1 1>&2 2>&3); then
if validate_gateway_ip "$gateway_ip"; then
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.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
fi
fi
else
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
local ip_range
if ip_range=$(whiptail --backtitle "Dynasty OS Framework [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
clear
header_info
echo -e "${INFO}${BOLD}${DGN}Scanning IP range for free address...${CL}\n"
if resolve_ip_from_range "$ip_range"; then
local gateway_ip
if gateway_ip=$(whiptail --backtitle "Dynasty OS Framework [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
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
_net="dhcp"
_gate=""
((STEP++))
fi
else
((STEP--))
fi
;;
10)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "IPv6 CONFIGURATION" \
--ok-button "Next" --cancel-button "Back" \
--menu "\nSelect IPv6 Address Management:" 16 70 5 \
"auto" "SLAAC/AUTO (recommended) - Dynamic IPv6 from network" \
"dhcp" "DHCPv6 - DHCP-assigned IPv6 address" \
"static" "Static - Manual IPv6 address configuration" \
"none" "None - No IPv6 assignment (most containers)" \
"disable" "Fully Disabled - (breaks some services)" \
3>&1 1>&2 2>&3); then
_ipv6_method="$result"
case "$result" in
static)
local ipv6_addr
if ipv6_addr=$(whiptail --backtitle "Dynasty OS Framework" \
--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 validate_ipv6_address "$ipv6_addr"; then
_ipv6_addr="$ipv6_addr"
local ipv6_gw_valid=false
while [[ "$ipv6_gw_valid" == "false" ]]; do
local ipv6_gw
ipv6_gw=$(whiptail --backtitle "Dynasty OS Framework" \
--title "IPv6 GATEWAY" \
--inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \
3>&1 1>&2 2>&3) || true
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.\n\nExample: 2001:db8::1/64\nCIDR must be 1-128." 10 58
fi
fi
;;
dhcp)
_ipv6_addr="dhcp"
_ipv6_gate=""
((STEP++))
;;
none)
_ipv6_addr="none"
_ipv6_gate=""
((STEP++))
;;
*)
_ipv6_addr=""
_ipv6_gate=""
((STEP++))
;;
esac
else
((STEP--))
fi
;;
11)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "MTU SIZE" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Interface MTU Size\n(leave blank for default 1500, common values: 1500, 9000)" 12 62 "" \
3>&1 1>&2 2>&3); then
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
;;
12)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "DNS SEARCH DOMAIN" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet DNS Search Domain\n(leave blank to use host setting)" 12 58 "" \
3>&1 1>&2 2>&3); then
_sd="$result"
((STEP++))
else
((STEP--))
fi
;;
13)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "DNS SERVER" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet DNS Server IP\n(leave blank to use host setting)" 12 58 "" \
3>&1 1>&2 2>&3); then
_ns="$result"
((STEP++))
else
((STEP--))
fi
;;
14)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "MAC ADDRESS" \
--ok-button "Next" --cancel-button "Back" \
--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
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
;;
15)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "VLAN TAG" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet VLAN Tag (1-4094)\n(leave blank for no VLAN)" 12 58 "" \
3>&1 1>&2 2>&3); then
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
;;
16)
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "CONTAINER TAGS" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Custom Tags (semicolon-separated)\n(alphanumeric, hyphens, underscores only)" 12 58 "$_tags" \
3>&1 1>&2 2>&3); then
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
;;
17)
configure_ssh_settings "Step $STEP/$MAX_STEP"
((STEP++))
;;
18)
local fuse_default_flag="--defaultno"
[[ "$_enable_fuse" == "yes" || "$_enable_fuse" == "1" ]] && fuse_default_flag=""
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "FUSE SUPPORT" \
--ok-button "Next" --cancel-button "Back" \
$fuse_default_flag \
--yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc.\n\n(App default: ${var_fuse:-no})" 14 58; then
_enable_fuse="yes"
else
if [ $? -eq 1 ]; then
_enable_fuse="no"
else
((STEP--))
continue
fi
fi
((STEP++))
;;
19)
local tun_default_flag="--defaultno"
[[ "$_enable_tun" == "yes" || "$_enable_tun" == "1" ]] && tun_default_flag=""
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "TUN/TAP SUPPORT" \
--ok-button "Next" --cancel-button "Back" \
$tun_default_flag \
--yesno "\nEnable TUN/TAP device support?\n\nRequired for: VPN apps (WireGuard, OpenVPN, Tailscale),\nnetwork tunneling, and containerized networking.\n\n(App default: ${var_tun:-no})" 14 62; then
_enable_tun="yes"
else
if [ $? -eq 1 ]; then
_enable_tun="no"
else
((STEP--))
continue
fi
fi
((STEP++))
;;
20)
local nesting_default_flag=""
[[ "$_enable_nesting" == "0" || "$_enable_nesting" == "no" ]] && nesting_default_flag="--defaultno"
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "NESTING SUPPORT" \
--ok-button "Next" --cancel-button "Back" \
$nesting_default_flag \
--yesno "\nEnable Nesting?\n\nRequired for: Docker, LXC inside LXC, Podman,\nand other containerization tools.\n\n(App default: ${var_nesting:-1})" 14 58; then
_enable_nesting="1"
else
if [ $? -eq 1 ]; then
_enable_nesting="0"
if [[ "$var_os" != "alpine" ]]; then
whiptail --backtitle "Dynasty OS Framework" \
--title "⚠️ NESTING WARNING" \
--msgbox "Modern systemd-based distributions (Debian 13+, Ubuntu 24.04+, etc.) may require nesting to be enabled for proper operation.\n\nWithout nesting, the container may start in a degraded state with failing services (error 243/CREDENTIALS).\n\nIf you experience issues, enable nesting in the container options." 14 68
fi
else
((STEP--))
continue
fi
fi
((STEP++))
;;
21)
local gpu_default_flag="--defaultno"
[[ "$_enable_gpu" == "yes" ]] && gpu_default_flag=""
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "GPU PASSTHROUGH" \
--ok-button "Next" --cancel-button "Back" \
$gpu_default_flag \
--yesno "\nEnable GPU Passthrough?\n\nAutomatically detects and passes through available GPUs\n(Intel/AMD/NVIDIA) for hardware acceleration.\n\nRecommended for: Media servers, AI/ML, Transcoding\n\n(App default: ${var_gpu:-no})" 16 62; then
_enable_gpu="yes"
else
if [ $? -eq 1 ]; then
_enable_gpu="no"
else
((STEP--))
continue
fi
fi
((STEP++))
;;
22)
local keyctl_default_flag="--defaultno"
[[ "$_enable_keyctl" == "1" ]] && keyctl_default_flag=""
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "KEYCTL SUPPORT" \
--ok-button "Next" --cancel-button "Back" \
$keyctl_default_flag \
--yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\nNote: Automatically enabled for unprivileged containers.\n\n(App default: ${var_keyctl:-0})" 16 62; then
_enable_keyctl="1"
else
if [ $? -eq 1 ]; then
_enable_keyctl="0"
else
((STEP--))
continue
fi
fi
((STEP++))
;;
23)
local apt_cacher_default_flag="--defaultno"
[[ "$_apt_cacher" == "yes" ]] && apt_cacher_default_flag=""
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "APT CACHER PROXY" \
--ok-button "Next" --cancel-button "Back" \
$apt_cacher_default_flag \
--yesno "\nUse APT Cacher-NG proxy?\n\nSpeeds up package downloads by caching them locally.\nRequires apt-cacher-ng running on your network.\n\n(App default: ${var_apt_cacher:-no})" 14 62; then
_apt_cacher="yes"
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "APT CACHER IP" \
--inputbox "\nEnter APT Cacher-NG server IP address:" 10 58 "$_apt_cacher_ip" \
3>&1 1>&2 2>&3); then
_apt_cacher_ip="$result"
fi
else
if [ $? -eq 1 ]; then
_apt_cacher="no"
_apt_cacher_ip=""
else
((STEP--))
continue
fi
fi
((STEP++))
;;
24)
local tz_hint="$_ct_timezone"
[[ -z "$tz_hint" ]] && tz_hint="(empty - will use host timezone)"
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "CONTAINER TIMEZONE" \
--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
local tz_test="$result"
[[ "${tz_test:-}" == Etc/* ]] && tz_test="host"
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
;;
25)
local protect_default_flag="--defaultno"
[[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_default_flag=""
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "CONTAINER PROTECTION" \
--ok-button "Next" --cancel-button "Back" \
$protect_default_flag \
--yesno "\nEnable Container Protection?\n\nPrevents accidental deletion of this container.\nYou must disable protection before removing.\n\n(App default: ${var_protection:-no})" 14 62; then
_protect_ct="yes"
else
if [ $? -eq 1 ]; then
_protect_ct="no"
else
((STEP--))
continue
fi
fi
((STEP++))
;;
26)
local mknod_default_flag="--defaultno"
[[ "$_enable_mknod" == "1" ]] && mknod_default_flag=""
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "DEVICE NODE CREATION" \
--ok-button "Next" --cancel-button "Back" \
$mknod_default_flag \
--yesno "\nAllow device node creation (mknod)?\n\nRequired for: Creating device files inside container.\nExperimental feature (requires kernel 5.3+).\n\n(App default: ${var_mknod:-0})" 14 62; then
_enable_mknod="1"
else
if [ $? -eq 1 ]; then
_enable_mknod="0"
else
((STEP--))
continue
fi
fi
((STEP++))
;;
27)
local mount_hint=""
[[ -n "$_mount_fs" ]] && mount_hint="$_mount_fs" || mount_hint="(none)"
if result=$(whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "MOUNT FILESYSTEMS" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint" 14 62 "$_mount_fs" \
3>&1 1>&2 2>&3); then
_mount_fs="$result"
((STEP++))
else
((STEP--))
fi
;;
28)
local verbose_default_flag="--defaultno"
[[ "$_verbose" == "yes" ]] && verbose_default_flag=""
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "VERBOSE MODE" \
$verbose_default_flag \
--yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then
_verbose="yes"
else
_verbose="no"
fi
local ct_type_desc="Unprivileged"
[[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged"
local nesting_desc="Disabled"
[[ "$_enable_nesting" == "1" ]] && nesting_desc="Enabled"
local keyctl_desc="Disabled"
[[ "$_enable_keyctl" == "1" ]] && keyctl_desc="Enabled"
local protect_desc="No"
[[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_desc="Yes"
local tz_display="${_ct_timezone:-Host TZ}"
local apt_display="${_apt_cacher:-no}"
[[ "$_apt_cacher" == "yes" && -n "$_apt_cacher_ip" ]] && apt_display="$_apt_cacher_ip"
local summary="Container Type: $ct_type_desc
Container ID: $_ct_id
Hostname: $_hostname
Resources:
Disk: ${_disk_size} GB
CPU: $_core_count cores
RAM: $_ram_size MiB
Network:
Bridge: $_bridge
IPv4: $_net
IPv6: $_ipv6_method
Features:
FUSE: $_enable_fuse | TUN: $_enable_tun
Nesting: $nesting_desc | Keyctl: $keyctl_desc
GPU: $_enable_gpu | Protection: $protect_desc
Advanced:
Timezone: $tz_display
APT Cacher: $apt_display
Verbose: $_verbose"
if whiptail --backtitle "Dynasty OS Framework [Step $STEP/$MAX_STEP]" \
--title "CONFIRM SETTINGS" \
--ok-button "Create LXC" --cancel-button "Back" \
--yesno "$summary\n\nCreate ${APP} LXC with these settings?" 32 62; then
((STEP++))
else
((STEP--))
fi
;;
esac
done
CT_TYPE="$_ct_type"
PW="$_pw"
CT_ID="$_ct_id"
HN="$_hostname"
DISK_SIZE="$_disk_size"
CORE_COUNT="$_core_count"
RAM_SIZE="$_ram_size"
BRG="$_bridge"
NET="$_net"
GATE="$_gate"
IPV6_METHOD="$_ipv6_method"
IPV6_ADDR="$_ipv6_addr"
IPV6_GATE="$_ipv6_gate"
TAGS="$_tags"
ENABLE_FUSE="$_enable_fuse"
ENABLE_TUN="$_enable_tun"
ENABLE_GPU="$_enable_gpu"
ENABLE_NESTING="$_enable_nesting"
ENABLE_KEYCTL="$_enable_keyctl"
ENABLE_MKNOD="$_enable_mknod"
ALLOW_MOUNT_FS="$_mount_fs"
PROTECT_CT="$_protect_ct"
CT_TIMEZONE="$_ct_timezone"
APT_CACHER="$_apt_cacher"
APT_CACHER_IP="$_apt_cacher_ip"
VERBOSE="$_verbose"
var_gpu="$_enable_gpu"
var_fuse="$_enable_fuse"
var_tun="$_enable_tun"
var_nesting="$_enable_nesting"
var_keyctl="$_enable_keyctl"
var_mknod="$_enable_mknod"
var_mount_fs="$_mount_fs"
var_protection="$_protect_ct"
var_timezone="$_ct_timezone"
var_apt_cacher="$_apt_cacher"
var_apt_cacher_ip="$_apt_cacher_ip"
[[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU=""
[[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD=""
[[ -n "$_ns" ]] && NS="-nameserver=$_ns" || NS=""
[[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC=""
[[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN=""
if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ -n "$_ns" ]; then
UDHCPC_FIX="yes"
else
UDHCPC_FIX="no"
fi
export UDHCPC_FIX
export SSH_KEYS_FILE
tput rmcup 2>/dev/null || true
trap - RETURN
echo -e "\n${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")${CL}"
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
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}"
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:-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 an LXC of ${APP} using the above advanced settings${CL}"
log_section "CONTAINER SETTINGS (ADVANCED) - ${APP}"
log_msg "Application: ${APP}"
log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
log_msg "Operating System: $var_os ($var_version)"
log_msg "Container Type: $([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")"
log_msg "Container ID: $CT_ID"
log_msg "Hostname: $HN"
log_msg "Disk Size: ${DISK_SIZE} GB"
log_msg "CPU Cores: $CORE_COUNT"
log_msg "RAM Size: ${RAM_SIZE} MiB"
log_msg "Bridge: $BRG"
log_msg "IPv4: $NET"
log_msg "IPv6: $IPV6_METHOD"
log_msg "FUSE Support: ${ENABLE_FUSE:-no}"
log_msg "Nesting: $([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")"
log_msg "GPU Passthrough: ${ENABLE_GPU:-no}"
log_msg "Verbose Mode: $VERBOSE"
log_msg "Session ID: ${SESSION_ID}"
}
diagnostics_check() {
local config_dir="/opt/dynr"
local config_file="${config_dir}/diagnostics"
mkdir -p "$config_dir"
if [[ -f "$config_file" ]]; then
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' "$config_file") || true
DIAGNOSTICS="${DIAGNOSTICS:-no}"
return
fi
local result
result=$(whiptail --backtitle "Dynasty OS Framework" \
--title "TELEMETRY & DIAGNOSTICS" \
--ok-button "Confirm" --cancel-button "Exit" \
--radiolist "\nHelp improve the Framework by sharing anonymous data.\n\nWhat we collect:\n - Container resources (CPU, RAM, disk), OS & PVE version\n - Application name, install method and status\n\nWhat we DON'T collect:\n - No IP addresses, hostnames, or personal data\n\nYou can change this anytime in the Settings menu.\nPrivacy: https://dynastyrevolution.com\n\nUse SPACE to select, ENTER to confirm." 22 76 2 \
"yes" "Yes, share anonymous data" OFF \
"no" "No, opt out" OFF \
3>&1 1>&2 2>&3) || result="no"
DIAGNOSTICS="${result:-no}"
cat <<EOF >"$config_file"
DIAGNOSTICS=${DIAGNOSTICS}
EOF
}
diagnostics_menu() {
local current="${DIAGNOSTICS:-no}"
local status_text="DISABLED"
[[ "$current" == "yes" ]] && status_text="ENABLED"
local dialog_text=(
"Telemetry is currently: ${status_text}\n\n"
"Anonymous data helps us improve scripts and track issues.\n"
"No personal data is ever collected.\n\n"
"More info: https://dynastyrevolution.com\n\n"
"Do you want to ${current:+change this setting}?"
)
if [[ "$current" == "yes" ]]; then
if whiptail --backtitle "Dynasty OS Framework" \
--title "TELEMETRY SETTINGS" \
--yesno "${dialog_text[*]}" 14 64 \
--yes-button "Disable" --no-button "Keep enabled"; then
DIAGNOSTICS="no"
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /opt/dynr/diagnostics
whiptail --msgbox "Telemetry disabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
fi
else
if whiptail --backtitle "Dynasty OS Framework" \
--title "TELEMETRY SETTINGS" \
--yesno "${dialog_text[*]}" 14 64 \
--yes-button "Enable" --no-button "Keep disabled"; then
DIAGNOSTICS="yes"
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /opt/dynr/diagnostics
whiptail --msgbox "Telemetry enabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
fi
fi
}
echo_default() {
CT_TYPE_DESC="Unprivileged"
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
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 [[ -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}"
fi
echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
echo -e " "
log_section "CONTAINER SETTINGS - ${APP}"
log_msg "Application: ${APP}"
log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
log_msg "Container ID: ${CT_ID}"
log_msg "Operating System: $var_os ($var_version)"
log_msg "Container Type: $CT_TYPE_DESC"
log_msg "Disk Size: ${DISK_SIZE} GB"
log_msg "CPU Cores: ${CORE_COUNT}"
log_msg "RAM Size: ${RAM_SIZE} MiB"
[[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]] && log_msg "GPU Passthrough: Enabled"
[[ "$VERBOSE" == "yes" ]] && log_msg "Verbose Mode: Enabled"
log_msg "Session ID: ${SESSION_ID}"
}
install_script() {
pve_check
shell_check
root_check
arch_check
ssh_check
maxkeys_check
diagnostics_check
if systemctl is-active -q ping-instances.service; then
systemctl -q stop ping-instances.service
fi
NEXTID=$(pvesh get /cluster/nextid)
if command -v timedatectl >/dev/null 2>&1; then
timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "UTC")
elif [ -f /etc/timezone ]; then
timezone=$(cat /etc/timezone)
else
timezone="UTC"
fi
[[ "${timezone:-}" == Etc/* ]] && timezone="host"
header_info
CHOICE="${mode:-${1:-}}"
local appdefaults_option=""
local settings_option=""
local menu_items=(
"1" "Default Install"
"2" "Advanced Install"
"3" "User Defaults"
)
if [ -f "$(get_app_defaults_path)" ]; then
appdefaults_option="4"
menu_items+=("4" "App Defaults for ${APP}")
settings_option="5"
menu_items+=("5" "Settings")
else
settings_option="4"
menu_items+=("4" "Settings")
fi
APPDEFAULTS_OPTION="$appdefaults_option"
SETTINGS_OPTION="$settings_option"
while true; do
if [ -z "$CHOICE" ]; then
TMP_CHOICE=$(whiptail \
--backtitle "Dynasty OS Framework" \
--title "Dynasty Revolution Options" \
--ok-button "Select" --cancel-button "Exit Script" \
--notags \
--menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
20 60 9 \
"${menu_items[@]}" \
--default-item "1" \
3>&1 1>&2 2>&3) || exit_script
CHOICE="$TMP_CHOICE"
fi
local defaults_target=""
local run_maybe_offer="no"
case "$CHOICE" in
1 | default | DEFAULT)
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
VERBOSE="no"
METHOD="default"
base_settings "$VERBOSE"
echo_default
defaults_target="$(ensure_global_default_vars_file)"
break
;;
2 | advanced | ADVANCED)
header_info
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
METHOD="advanced"
base_settings
advanced_settings
defaults_target="$(ensure_global_default_vars_file)"
run_maybe_offer="yes"
break
;;
3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS)
default_var_settings || {
msg_error "Failed to apply default.vars"
exit 110
}
defaults_target="/opt/dynr/default.vars"
break
;;
"$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS)
if [ -f "$(get_app_defaults_path)" ]; then
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"
base_settings
echo_default
defaults_target="$(get_app_defaults_path)"
break
else
msg_error "No App Defaults available for ${APP}"
exit 111
fi
;;
"$SETTINGS_OPTION" | settings | SETTINGS)
settings_menu
header_info
CHOICE=""
;;
generated | GENERATED)
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using Generated Settings on node $PVEHOST_NAME${CL}"
VERBOSE="no"
METHOD="generated"
base_settings "$VERBOSE"
echo_default
break
;;
*)
msg_error "Invalid option: $CHOICE"
exit 112
;;
esac
done
if [[ -n "$defaults_target" ]]; then
ensure_storage_selection_for_vars_file "$defaults_target"
fi
if [[ "$run_maybe_offer" == "yes" ]]; then
maybe_offer_save_app_defaults
fi
}
edit_default_storage() {
local vf="/opt/dynr/default.vars"
if [[ ! -f "$vf" ]]; then
mkdir -p "$(dirname "$vf")"
touch "$vf"
fi
ensure_storage_selection_for_vars_file "$vf"
}
settings_menu() {
while true; do
local settings_items=(
"1" "Manage API-Diagnostic Setting"
"2" "Edit Default.vars"
)
if [ -f "$(get_app_defaults_path)" ]; then
settings_items+=("3" "Edit App.vars for ${APP}")
settings_items+=("4" "Back to Main Menu")
else
settings_items+=("3" "Back to Main Menu")
fi
local choice
choice=$(whiptail --backtitle "Dynasty OS Framework" \
--title "Dynasty SETTINGS Menu" \
--ok-button "Select" --cancel-button "Exit Script" \
--menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \
"${settings_items[@]}" \
3>&1 1>&2 2>&3) || exit_script
case "$choice" in
1) diagnostics_menu ;;
2) ${EDITOR:-nano} /opt/dynr/default.vars ;;
3)
if [ -f "$(get_app_defaults_path)" ]; then
${EDITOR:-nano} "$(get_app_defaults_path)"
else
return
fi
;;
4)
return
;;
esac
done
}
check_container_resources() {
current_ram=$(free -m | awk 'NR==2{print $2}')
current_cpu=$(nproc)
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
msg_warn "Under-provisioned: Required ${var_cpu} CPU/${var_ram}MB RAM, Current ${current_cpu} CPU/${current_ram}MB RAM"
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? <yes/No> "
read -r prompt </dev/tty
if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
msg_error "Aborted: under-provisioned LXC (${current_cpu} CPU/${current_ram}MB RAM < ${var_cpu} CPU/${var_ram}MB RAM)"
exit 113
fi
else
echo -e ""
fi
}
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
msg_warn "Storage is dangerously low (${usage}% used on /boot)"
echo -ne "Continue anyway? <y/N> "
read -r prompt </dev/tty
if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
msg_error "Aborted: storage too low (${usage}% used)"
exit 114
fi
fi
}
ssh_extract_keys_from_file() {
local f="$1"
[[ -r "$f" ]] || return 0
tr -d '\r' <"$f" | awk '
/^[[:space:]]*#/ {next}
/^[[:space:]]*$/ {next}
/^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
{
match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
if (RSTART>0) { print substr($0, RSTART) }
}
'
}
ssh_build_choices_from_files() {
local -a files=("$@")
CHOICES=()
COUNT=0
MAPFILE="$(mktemp)"
local id key typ fp cmt base ln=0
for f in "${files[@]}"; do
[[ -f "$f" && -r "$f" ]] || continue
base="$(basename -- "$f")"
case "$base" in
known_hosts | known_hosts.* | config) continue ;;
id_*) [[ "$f" != *.pub ]] && continue ;;
esac
while IFS= read -r key; do
[[ -n "$key" ]] || continue
typ=""
fp=""
cmt=""
read -r _typ _b64 _cmt <<<"$key"
typ="${_typ:-key}"
cmt="${_cmt:-}"
if command -v ssh-keygen >/dev/null 2>&1; then
fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
fi
[[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
ln=$((ln + 1))
COUNT=$((COUNT + 1))
id="K${COUNT}"
echo "${id}|${key}" >>"$MAPFILE"
CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }${base}" "OFF")
done < <(ssh_extract_keys_from_file "$f")
done
}
ssh_discover_default_files() {
local -a cand=()
shopt -s nullglob
cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
cand+=(/root/.ssh/*.pub)
cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
shopt -u nullglob
printf '%s\0' "${cand[@]}"
}
configure_ssh_settings() {
local step_info="${1:-}"
local backtitle="Dynasty OS Framework"
[[ -n "$step_info" ]] && backtitle="Dynasty OS Framework [${step_info}]"
SSH_KEYS_FILE="$(mktemp)"
: >"$SSH_KEYS_FILE"
IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
ssh_build_choices_from_files "${_def_files[@]}"
local default_key_count="$COUNT"
local ssh_key_mode
if [[ "$default_key_count" -gt 0 ]]; then
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
"Provision SSH keys for root:" 14 72 4 \
"found" "Select from detected keys (${default_key_count})" \
"manual" "Paste a single public key" \
"folder" "Scan another folder (path or glob)" \
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
else
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
"No host keys detected; choose manual/none:" 12 72 2 \
"manual" "Paste a single public key" \
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
fi
case "$ssh_key_mode" in
found)
local selection
selection=$(whiptail --backtitle "$backtitle" --title "SELECT HOST KEYS" \
--checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
for tag in $selection; do
tag="${tag%\"}"
tag="${tag#\"}"
local line
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2- || true)
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
done
;;
manual)
SSH_AUTHORIZED_KEY="$(whiptail --backtitle "$backtitle" \
--inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
[[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
;;
folder)
local glob_path
glob_path=$(whiptail --backtitle "$backtitle" \
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
if [[ -n "$glob_path" ]]; then
shopt -s nullglob
read -r -a _scan_files <<<"$glob_path"
shopt -u nullglob
if [[ "${#_scan_files[@]}" -gt 0 ]]; then
ssh_build_choices_from_files "${_scan_files[@]}"
if [[ "$COUNT" -gt 0 ]]; then
local folder_selection
folder_selection=$(whiptail --backtitle "$backtitle" --title "SELECT FOLDER KEYS" \
--checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
for tag in $folder_selection; do
tag="${tag%\"}"
tag="${tag#\"}"
local line
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2- || true)
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
done
else
whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60
fi
else
whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60
fi
fi
;;
none)
:
;;
esac
if [[ -s "$SSH_KEYS_FILE" ]]; then
sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
printf '\n' >>"$SSH_KEYS_FILE"
fi
if (whiptail --backtitle "$backtitle" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
SSH="yes"
else
SSH="no"
fi
}
msg_menu() {
local title="$1"
shift
local -a tags=()
local -a descs=()
while [[ $# -ge 2 ]]; do
tags+=("$1")
descs+=("$2")
shift 2
done
local default_tag="${tags[0]}"
local count=${#tags[@]}
if [[ -n "${PHS_SILENT+x}" ]] && [[ "${PHS_SILENT}" == "1" ]]; then
echo "$default_tag"
return 0
fi
{
echo ""
msg_custom "📋" "${BL}" "${title}"
echo ""
for i in "${!tags[@]}"; do
local marker=" "
[[ $i -eq 0 ]] && marker="* "
printf "${TAB3}${marker}%s) %s\n" "${tags[$i]}" "${descs[$i]}"
done
echo ""
} >/dev/tty
local selection=""
read -r -t 10 -p "${TAB3}Select [default=${default_tag}, timeout 10s]: " selection </dev/tty >/dev/tty || true
if [[ -n "$selection" ]]; then
for tag in "${tags[@]}"; do
if [[ "$selection" == "$tag" ]]; then
echo "$selection"
return 0
fi
done
msg_warn "Invalid selection '${selection}' - using default: ${default_tag}"
fi
echo "$default_tag"
return 0
}
start() {
source <(curl -fsSL https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/core/tools.func)
if command -v pveversion >/dev/null 2>&1; then
install_script || return 0
return 0
elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
VERBOSE="no"
set_std_mode
ensure_profile_loaded
get_lxc_ip
update_script
update_motd_ip
cleanup_lxc
else
CHOICE=$(whiptail --backtitle "Dynasty OS Framework" --title "${APP} LXC Update/Setting" --menu \
"Support/Update functions for ${APP} LXC. Choose an option:" \
12 60 3 \
"1" "YES (Silent Mode)" \
"2" "YES (Verbose Mode)" \
"3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
case "$CHOICE" in
1)
VERBOSE="no"
set_std_mode
;;
2)
VERBOSE="yes"
set_std_mode
;;
3)
clear
exit_script
exit 0
;;
esac
ensure_profile_loaded
get_lxc_ip
update_script
update_motd_ip
cleanup_lxc
fi
}
build_container() {
NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
if [[ -n "$MAC" ]]; then
case "$MAC" in
,hwaddr=*) NET_STRING+="$MAC" ;;
*) NET_STRING+=",hwaddr=$MAC" ;;
esac
fi
NET_STRING+=",ip=${NET:-dhcp}"
if [[ -n "$GATE" ]]; then
case "$GATE" in
,gw=) ;;
,gw=*) NET_STRING+="$GATE" ;;
*) NET_STRING+=",gw=$GATE" ;;
esac
fi
if [[ -n "$VLAN" ]]; then
case "$VLAN" in
,tag=*) NET_STRING+="$VLAN" ;;
*) NET_STRING+=",tag=$VLAN" ;;
esac
fi
if [[ -n "$MTU" ]]; then
case "$MTU" in
,mtu=*) NET_STRING+="$MTU" ;;
*) NET_STRING+=",mtu=$MTU" ;;
esac
fi
case "$IPV6_METHOD" in
auto) NET_STRING="$NET_STRING,ip6=auto" ;;
dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
static)
if [[ -n "$IPV6_ADDR" ]]; then
NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
[ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
fi
;;
none) ;;
esac
FEATURES=""
if [ "${ENABLE_NESTING:-1}" == "1" ]; then
FEATURES="nesting=1"
fi
if [ "$CT_TYPE" == "1" ]; then
[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
FEATURES="${FEATURES}keyctl=1"
fi
if [ "$ENABLE_FUSE" == "yes" ]; then
[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
FEATURES="${FEATURES}fuse=1"
fi
TEMP_DIR=$(mktemp -d)
pushd "$TEMP_DIR" >/dev/null
local _func_url
if [ "$var_os" == "alpine" ]; then
_func_url="https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/core/alpine-install.func"
else
_func_url="https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/core/install.func"
fi
export FUNCTIONS_FILE_PATH="$(curl -fsSL "$_func_url")"
if [[ -z "$FUNCTIONS_FILE_PATH" || ${#FUNCTIONS_FILE_PATH} -lt 100 ]]; then
msg_error "Failed to download install functions from: $_func_url"
exit 115
fi
export DIAGNOSTICS="$DIAGNOSTICS"
export RANDOM_UUID="$RANDOM_UUID"
export EXECUTION_ID="$EXECUTION_ID"
export SESSION_ID="$SESSION_ID"
export CACHER="$APT_CACHER"
export CACHER_IP="$APT_CACHER_IP"
export tz="$timezone"
export APPLICATION="$APP"
export app="$NSAPP"
export PASSWORD="$PW"
export VERBOSE="$VERBOSE"
export SSH_ROOT="${SSH}"
export SSH_AUTHORIZED_KEY
export CTID="$CT_ID"
export CTTYPE="$CT_TYPE"
export ENABLE_FUSE="$ENABLE_FUSE"
export ENABLE_TUN="$ENABLE_TUN"
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"
export BUILD_LOG="$BUILD_LOG"
export INSTALL_LOG="/root/.install-${SESSION_ID}.log"
_HOST_LOGFILE="$BUILD_LOG"
export dev_mode="${dev_mode:-}"
export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}"
export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}"
export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}"
export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}"
export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}"
export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}"
export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}"
PCT_OPTIONS_STRING=" -hostname $HN"
if [ -n "$TAGS" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
-tags $TAGS"
fi
if [ -n "$FEATURES" ]; then
PCT_OPTIONS_STRING=" -features $FEATURES
$PCT_OPTIONS_STRING"
fi
if [ -n "$SD" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
$SD"
fi
if [ -n "$NS" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
$NS"
fi
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
$NET_STRING
-onboot 1
-cores $CORE_COUNT
-memory $RAM_SIZE
-unprivileged $CT_TYPE"
if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
-protection 1"
fi
if [ -n "${CT_TIMEZONE:-}" ]; then
local _pct_timezone="$CT_TIMEZONE"
[[ "$_pct_timezone" == Etc/* ]] && _pct_timezone="host"
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
-timezone $_pct_timezone"
fi
if [ -n "$PW" ]; then
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
$PW"
fi
export PCT_OPTIONS="$PCT_OPTIONS_STRING"
export TEMPLATE_STORAGE="${var_template_storage:-}"
export CONTAINER_STORAGE="${var_container_storage:-}"
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 $?
post_progress_to_api "configuring"
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
is_gpu_app() {
[[ "${var_gpu:-no}" == "yes" ]] && return 0
return 1
}
detect_gpu_devices() {
INTEL_DEVICES=()
AMD_DEVICES=()
NVIDIA_DEVICES=()
local pci_vga_info
pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D" || true)
if [[ -z "$pci_vga_info" ]]; then
msg_debug "No VGA/Display/3D PCI devices found"
return 0
fi
if grep -q "\[8086:" <<<"$pci_vga_info"; then
msg_custom "🎮" "${BL}" "Detected Intel GPU"
if [[ -d /dev/dri ]]; then
for d in /dev/dri/renderD* /dev/dri/card*; do
[[ -e "$d" ]] && INTEL_DEVICES+=("$d")
done
fi
fi
if grep -qE "\[1002:|\[1022:" <<<"$pci_vga_info"; then
msg_custom "🎮" "${RD}" "Detected AMD GPU"
if [[ -d /dev/dri ]]; then
if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
for d in /dev/dri/renderD* /dev/dri/card*; do
[[ -e "$d" ]] && AMD_DEVICES+=("$d")
done
fi
fi
fi
if grep -q "\[10de:" <<<"$pci_vga_info"; then
msg_custom "🎮" "${GN}" "Detected NVIDIA GPU"
for d in /dev/nvidia*; do
[[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
done
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"
else
msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found"
msg_custom "" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)"
fi
fi
msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
msg_debug "AMD devices: ${AMD_DEVICES[*]}"
msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
}
configure_usb_passthrough() {
if [[ "$CT_TYPE" != "0" ]]; then
return 0
fi
msg_info "Configuring automatic USB passthrough (privileged container)"
cat <<EOF >>"$LXC_CONFIG"
lxc.cgroup2.devices.allow: a
lxc.cap.drop:
lxc.cgroup2.devices.allow: c 188:* rwm
lxc.cgroup2.devices.allow: c 189:* rwm
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
EOF
msg_ok "USB passthrough configured"
}
configure_gpu_passthrough() {
if ! is_gpu_app "$APP"; then
return 0
fi
detect_gpu_devices
local gpu_count=0
local available_gpus=()
if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
available_gpus+=("INTEL")
gpu_count=$((gpu_count + 1))
fi
if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
available_gpus+=("AMD")
gpu_count=$((gpu_count + 1))
fi
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
available_gpus+=("NVIDIA")
gpu_count=$((gpu_count + 1))
fi
if [[ $gpu_count -eq 0 ]]; then
msg_custom "" "${YW}" "No GPU devices found for passthrough"
return 0
fi
local selected_gpu=""
if [[ $gpu_count -eq 1 ]]; then
selected_gpu="${available_gpus[0]}"
msg_ok "Automatically configuring ${selected_gpu} GPU passthrough"
else
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 </dev/tty
selected_gpu="${selected_gpu^^}"
local valid=0
for gpu in "${available_gpus[@]}"; do
[[ "$selected_gpu" == "$gpu" ]] && valid=1
done
if [[ $valid -eq 0 ]]; then
msg_warn "Invalid selection. Skipping GPU passthrough."
return 0
fi
fi
local dev_idx=0
case "$selected_gpu" in
INTEL | AMD)
local devices=()
[[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
[[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
local dev_index=0
for dev in "${devices[@]}"; do
echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
dev_index=$((dev_index + 1))
done
export GPU_TYPE="$selected_gpu"
msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)"
;;
NVIDIA)
if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
msg_warn "No NVIDIA devices available for passthrough"
return 0
fi
local dev_index=0
for dev in "${NVIDIA_DEVICES[@]}"; do
echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
dev_index=$((dev_index + 1))
done
export GPU_TYPE="NVIDIA"
msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed"
;;
esac
}
configure_additional_devices() {
if [ "$ENABLE_TUN" == "yes" ]; then
cat <<EOF >>"$LXC_CONFIG"
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
EOF
fi
if [[ -e /dev/apex_0 ]]; then
msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough"
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
fi
}
configure_usb_passthrough
configure_gpu_passthrough
configure_additional_devices
if [[ "${GPU_TYPE:-}" == "AMD" ]]; then
local rocm_extra=4
local new_disk_size=$((PCT_DISK_SIZE + rocm_extra))
if pct resize "$CTID" rootfs "${new_disk_size}G" >/dev/null 2>&1; then
msg_ok "Disk resized ${PCT_DISK_SIZE}GB → ${new_disk_size}GB for ROCm"
else
msg_warn "Failed to resize disk for ROCm — installation may fail if space is insufficient"
fi
fi
msg_info "Starting LXC Container"
pct start "$CTID"
for i in {1..10}; do
if pct status "$CTID" | grep -q "status: running"; then
msg_ok "Started LXC Container"
break
fi
sleep 1
if [ "$i" -eq 10 ]; then
local ct_status
ct_status=$(pct status "$CTID" 2>/dev/null || echo "unknown")
msg_error "LXC Container did not reach running state (status: ${ct_status})"
exit 117
fi
done
if [ "$var_os" != "alpine" ]; then
msg_info "Waiting for network in LXC container"
local ip_in_lxc=""
for i in {1..20}; do
ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
if [ -z "$ip_in_lxc" ]; then
ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
fi
[ -n "$ip_in_lxc" ] && break
sleep 1
done
if [ -z "$ip_in_lxc" ]; then
msg_error "No IP assigned to CT $CTID after 20s"
msg_custom "🔧" "${YW}" "Troubleshooting:"
echo " • Verify bridge ${BRG} exists and has connectivity"
echo " • Check if DHCP server is reachable (if using DHCP)"
echo " • Verify static IP configuration (if using static IP)"
echo " • Check Proxmox firewall rules"
echo " • If using Tailscale: Disable MagicDNS temporarily"
exit 118
fi
local ping_success=false
for retry in {1..3}; do
if pct exec "$CTID" -- ping -c 1 -W 2 1.1.1.1 &>/dev/null ||
pct exec "$CTID" -- ping -c 1 -W 2 8.8.8.8 &>/dev/null ||
pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then
ping_success=true
break
fi
sleep 2
done
if [ "$ping_success" = false ]; then
msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed - installation will continue"
else
msg_ok "Network in LXC is reachable (ping)"
fi
fi
get_container_gid() {
local group="$1"
local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
echo "${gid:-44}"
}
fix_gpu_gids
local rootfs
rootfs=$(pct config "$CTID" | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1 || true)
if [[ -n "$rootfs" ]]; then
local mount_point="/var/lib/lxc/${CTID}/rootfs"
if [[ -d "$mount_point" ]] && [[ "$(stat -c '%U' "$mount_point")" != "root" ]]; then
chown root:root "$mount_point" 2>/dev/null || true
fi
fi
msg_info "Customizing LXC Container"
set +Eeuo pipefail
trap - ERR
local install_exit_code=0
if [ "$var_os" == "alpine" ]; then
sleep 3
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
https://dl-cdn.alpinelinux.org/alpine/latest-stable/main
https://dl-cdn.alpinelinux.org/alpine/latest-stable/community
EOF'
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq" >>"$BUILD_LOG" 2>&1 || {
msg_warn "apk install failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
local alpine_exit=0
pct exec "$CTID" -- ash -c '
ALPINE_MIRRORS="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
for m in $(printf "%s\n" $ALPINE_MIRRORS | shuf); do
if wget -q --spider --timeout=2 "http://$m/alpine/latest-stable/main/" 2>/dev/null; then
echo " Attempting mirror: $m"
cat <<EOF >/etc/apk/repositories
http://$m/alpine/latest-stable/main
http://$m/alpine/latest-stable/community
EOF
if apk update >/dev/null 2>&1 && apk add bash newt curl openssh nano mc ncurses jq >/dev/null 2>&1; then
echo " CDN set to $m: tests passed"
exit 0
else
echo " Mirror $m failed"
fi
fi
done
exit 2
' && alpine_exit=0 || alpine_exit=$?
if [[ $alpine_exit -ne 0 ]]; then
msg_error "Failed to install base packages in Alpine container"
install_exit_code=1
fi
}
else
sleep 3
LANG=${LANG:-en_US.UTF-8}
local LANG_ESC="${LANG//./\\.}"
LANG_ESC="${LANG_ESC//|/\\|}"
pct exec "$CTID" -- bash -c "sed -i \"/$LANG_ESC/ 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"
if [[ -z "${tz:-}" ]]; then
tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC")
fi
[[ "${tz:-}" == Etc/* ]] && tz="UTC"
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
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
if ! pct exec "$CTID" -- bash -c "getent hosts deb.debian.org >/dev/null 2>&1 && getent hosts archive.ubuntu.com >/dev/null 2>&1"; then
msg_warn "APT repository DNS resolution failed in container, injecting public DNS servers"
pct exec "$CTID" -- bash -c "echo -e 'nameserver 8.8.8.8\nnameserver 1.1.1.1' >/etc/resolv.conf"
fi
pct exec "$CTID" -- bash -c "apt-get update 2>&1 && apt-get install -y sudo curl mc gnupg2 jq 2>&1" >>"$BUILD_LOG" 2>&1 || {
local failed_mirror
failed_mirror=$(pct exec "$CTID" -- bash -c "grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null" 2>/dev/null || echo "unknown")
msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..."
local mirror_exit=0
pct exec "$CTID" -- bash -c '
APT_BASE="sudo curl mc gnupg2 jq"
DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")
if [ "$DISTRO" = "ubuntu" ]; then
EU_MIRRORS="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de"
US_MIRRORS="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu"
AP_MIRRORS="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au"
else
EU_MIRRORS="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
US_MIRRORS="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
AP_MIRRORS="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
fi
TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC")
case "$TZ" in
Europe/*|Arctic/*) REGIONAL="$EU_MIRRORS"; OTHERS="$US_MIRRORS $AP_MIRRORS" ;;
America/*) REGIONAL="$US_MIRRORS"; OTHERS="$EU_MIRRORS $AP_MIRRORS" ;;
Asia/*|Australia/*|Pacific/*) REGIONAL="$AP_MIRRORS"; OTHERS="$EU_MIRRORS $US_MIRRORS" ;;
*) REGIONAL=""; OTHERS="$EU_MIRRORS $US_MIRRORS $AP_MIRRORS" ;;
esac
echo "Acquire::By-Hash \"no\";" >/etc/apt/apt.conf.d/99no-by-hash
try_mirrors() {
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
[ -f "$src" ] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${1}/|g; s|deb http[s]*://[^/]*/|deb http://${1}/|g" "$src"
done
rm -rf /var/lib/apt/lists/*
APT_OUT=$(apt-get update 2>&1)
APT_RC=$?
if echo "$APT_OUT" | grep -qi "hashsum\|hash sum"; then
echo " Mirror $1 failed (hash mismatch)"
return 1
elif echo "$APT_OUT" | grep -qi "SSL\|certificate"; then
echo " Mirror $1 failed (SSL/certificate error)"
return 1
elif [ $APT_RC -ne 0 ]; then
echo " Mirror $1 failed (apt-get update error)"
return 1
elif apt-get install -y $APT_BASE >/dev/null 2>&1; then
echo " CDN set to $1: tests passed"
return 0
else
echo " Mirror $1 failed (package install error)"
return 1
fi
}
scan_reachable() {
local result=""
for m in $1; do
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
result="$result $m"
fi
done
echo "$result" | xargs
}
OTHERS_OK=$(scan_reachable "$OTHERS")
OTHERS_PICK=$(printf "%s\n" $OTHERS_OK | shuf | head -3 | xargs)
for mirror in $OTHERS_PICK; do
echo " Attempting mirror: $mirror"
try_mirrors "$mirror" && exit 0
done
if [ "$DISTRO" = "ubuntu" ]; then
PRIMARY="archive.ubuntu.com"
else
PRIMARY="ftp.debian.org"
fi
if timeout 2 bash -c "echo >/dev/tcp/$PRIMARY/80" 2>/dev/null; then
echo " Attempting mirror: $PRIMARY"
try_mirrors "$PRIMARY" && exit 0
fi
REGIONAL_OK=$(scan_reachable "$REGIONAL")
REGIONAL_PICK=$(printf "%s\n" $REGIONAL_OK | shuf | head -3 | xargs)
for mirror in $REGIONAL_PICK; do
echo " Attempting mirror: $mirror"
try_mirrors "$mirror" && exit 0
done
exit 2
' && mirror_exit=0 || mirror_exit=$?
if [[ $mirror_exit -eq 2 ]]; then
msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
if [[ "$var_os" == "ubuntu" ]]; then
msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors"
else
msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list"
fi
local custom_mirror=""
while true; do
read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror </dev/tty
[[ -z "$custom_mirror" ]] && continue
[[ "$custom_mirror" == "skip" ]] && break
[[ ! "$custom_mirror" =~ ^[a-zA-Z0-9._-]+$ ]] && {
msg_warn "Invalid hostname format."
continue
}
pct exec "$CTID" -- bash -c "
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
[ -f \"\$src\" ] && sed -i \"s|URIs: http[s]*://[^/]*/|URIs: http://${custom_mirror}/|g; s|deb http[s]*://[^/]*/|deb http://${custom_mirror}/|g\" \"\$src\"
done
rm -rf /var/lib/apt/lists/*
apt-get update >/dev/null 2>&1 && apt-get install -y sudo curl mc gnupg2 jq >/dev/null 2>&1
" && break
msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
done
if [[ "$custom_mirror" == "skip" ]]; then
msg_error "apt-get base packages installation failed"
install_exit_code=1
fi
elif [[ $mirror_exit -ne 0 ]]; then
msg_error "apt-get base packages installation failed"
install_exit_code=1
fi
}
fi
if [[ $install_exit_code -eq 0 ]]; then
msg_ok "Customized LXC Container"
if [[ "${DNS_RETRY_OVERRIDE:-false}" == "true" ]]; then
msg_info "Applying DNS retry override in LXC (8.8.8.8, 1.1.1.1)"
pct exec "$CTID" -- bash -c "printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' >/etc/resolv.conf" >/dev/null 2>&1 || true
msg_ok "DNS override applied in LXC"
fi
install_ssh_keys_into_ct
start_install_timer
export CONTAINER_INSTALLING=true
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/install/${var_install}.sh)"
local lxc_exit=$?
unset CONTAINER_INSTALLING
if [[ -n "${SESSION_ID:-}" ]]; then
local error_flag="/root/.install-${SESSION_ID}.failed"
if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then
install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1")
pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true
fi
fi
if [[ $install_exit_code -eq 0 && ${lxc_exit:-0} -ne 0 ]]; then
install_exit_code=${lxc_exit:-0}
fi
fi
if [[ $install_exit_code -ne 0 ]]; then
trap '' TSTP TTIN TTOU
msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})"
local build_log_copied=false
local install_log_copied=false
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
{
echo "================================================================================"
echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
echo "Container ID: ${CTID}"
echo "Session ID: ${SESSION_ID}"
echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
echo "================================================================================"
echo ""
} >"$combined_log"
if [[ -f "${BUILD_LOG}" ]]; then
{
echo "================================================================================"
echo "PHASE 1: CONTAINER CREATION (Host)"
echo "================================================================================"
cat "${BUILD_LOG}"
echo ""
} >>"$combined_log"
build_log_copied=true
fi
local temp_install_log="/tmp/.install-temp-${SESSION_ID}.log"
if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_install_log" 2>/dev/null; then
{
echo "================================================================================"
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
echo "================================================================================"
cat "$temp_install_log"
echo ""
} >>"$combined_log"
rm -f "$temp_install_log"
install_log_copied=true
INSTALL_LOG="$combined_log"
fi
fi
set +Eeuo pipefail
trap - ERR
local is_oom=false
local is_network_issue=false
local is_apt_issue=false
local is_cmd_not_found=false
local is_disk_full=false
if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then
if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then
is_apt_issue=true
fi
if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then
is_oom=true
fi
if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then
is_network_issue=true
fi
if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then
is_cmd_not_found=true
fi
if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
is_disk_full=true
fi
fi
if [[ "$is_apt_issue" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="dependency"
elif [[ "$is_oom" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="resource"
elif [[ "$is_network_issue" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="network"
elif [[ "$is_disk_full" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="storage"
elif [[ "$is_cmd_not_found" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="dependency"
fi
$STD echo -e "${TAB}⏳ Reporting failure to telemetry..."
post_update_to_api "failed" "$install_exit_code"
$STD echo -e "${TAB}${CM:-} Failure reported"
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
msg_custom "📋" "${YW}" "Installation log: ${combined_log}"
fi
if [[ "${DEV_MODE_KEEP:-false}" == "true" ]]; then
msg_dev "Keep mode active - container ${CTID} preserved"
return 0
elif [[ "${DEV_MODE_BREAKPOINT:-false}" == "true" ]]; then
msg_dev "Breakpoint mode - opening shell in container ${CTID}"
echo -e "${YW}Type 'exit' to return to host${CL}"
pct enter "$CTID"
echo ""
echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}"
if read -r response </dev/tty && [[ "$response" =~ ^[Yy]$ ]]; then
pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true
msg_ok "Container ${CTID} removed"
else
msg_dev "Container ${CTID} kept for debugging"
fi
exit $install_exit_code
fi
echo ""
local error_explanation=""
if declare -f explain_exit_code >/dev/null 2>&1; then
error_explanation="$(explain_exit_code "$install_exit_code")"
fi
if [[ $install_exit_code -eq 134 || $install_exit_code -eq 137 || $install_exit_code -eq 243 ]]; then
is_oom=true
fi
case "$install_exit_code" in
100 | 101 | 102) is_apt_issue=true ;;
255)
if [[ -f "$combined_log" ]] && grep -qiE 'dpkg|apt-get|apt\.conf|broken packages|unmet dependencies|E: Sub-process|E: Failed' "$combined_log"; then
is_apt_issue=true
fi
;;
esac
if [[ $install_exit_code -eq 228 || $install_exit_code -eq 23 ]]; then
is_disk_full=true
fi
if [[ -f "$combined_log" ]] && grep -qiE 'ENOSPC|no space left on device|No space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
is_disk_full=true
fi
if [[ $install_exit_code -eq 127 ]]; then
is_cmd_not_found=true
fi
case "$install_exit_code" in
6 | 7 | 22 | 28 | 35 | 52 | 56 | 57 | 75 | 78) is_network_issue=true ;;
100)
if [[ -f "$combined_log" ]] && grep -qiE 'Failed to fetch|Could not resolve|Connection failed|Network is unreachable|Temporary failure resolving' "$combined_log"; then
is_network_issue=true
fi
;;
128)
if [[ -f "$combined_log" ]] && grep -qiE 'RPC failed|early EOF|fetch-pack|HTTP/2 stream|Could not resolve host|Temporary failure resolving|Failed to fetch|Connection reset|Network is unreachable' "$combined_log"; then
is_network_issue=true
fi
;;
esac
if [[ -n "$error_explanation" ]]; then
echo -e "${TAB}${RD}Error: ${error_explanation}${CL}"
echo ""
fi
if [[ $install_exit_code -eq 10 ]]; then
echo -e "${TAB}${INFO} This error usually means the container needs ${GN}privileged${CL} mode or Docker/nesting support."
echo -e "${TAB}${INFO} Recreate with: Advanced Install → Container Type: ${GN}Privileged${CL}"
echo ""
fi
if [[ $install_exit_code -eq 125 || $install_exit_code -eq 126 ]]; then
echo -e "${TAB}${INFO} The command exists but cannot be executed. This may be a ${GN}permission${CL} issue."
echo -e "${TAB}${INFO} If using Docker, ensure the container is ${GN}privileged${CL} or has correct permissions."
echo ""
fi
if [[ "$is_disk_full" == true ]]; then
echo -e "${TAB}${INFO} The container ran out of disk space during installation (${GN}ENOSPC${CL})."
echo -e "${TAB}${INFO} Current disk size: ${GN}${DISK_SIZE} GB${CL}. A rebuild with doubled disk may resolve this."
echo ""
fi
if [[ "$is_cmd_not_found" == true ]]; then
local missing_cmd=""
if [[ -f "$combined_log" ]]; then
missing_cmd=$(grep -oiE '[a-zA-Z0-9_.-]+: command not found' "$combined_log" 2>/dev/null | tail -1 | sed 's/: command not found//') || true
fi
if [[ -n "$missing_cmd" ]]; then
echo -e "${TAB}${INFO} Missing command: ${GN}${missing_cmd}${CL}"
fi
echo ""
fi
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 (full rebuild)"
local next_option=4
local APT_OPTION="" OOM_OPTION="" DNS_OPTION="" DISK_OPTION=""
if [[ "$is_apt_issue" == true ]]; then
if [[ "$var_os" == "alpine" ]]; then
echo -e " ${GN}${next_option})${CL} Repair APK state and re-run install (in-place)"
else
echo -e " ${GN}${next_option})${CL} Repair APT/DPKG state and re-run install (in-place)"
fi
APT_OPTION=$next_option
next_option=$((next_option + 1))
fi
if [[ "$is_oom" == true ]]; then
local recovery_attempt="${RECOVERY_ATTEMPT:-0}"
if [[ $recovery_attempt -lt 2 ]]; then
local new_ram=$((RAM_SIZE * 2))
local new_cpu=$((CORE_COUNT * 2))
echo -e " ${GN}${next_option})${CL} Retry with more resources (RAM: ${RAM_SIZE}${new_ram} MiB, CPU: ${CORE_COUNT}${new_cpu} cores)"
OOM_OPTION=$next_option
next_option=$((next_option + 1))
else
echo -e " ${DGN}-)${CL} ${DGN}OOM retry exhausted (already retried ${recovery_attempt}x)${CL}"
fi
fi
if [[ "$is_disk_full" == true ]]; then
local disk_recovery_attempt="${DISK_RECOVERY_ATTEMPT:-0}"
if [[ $disk_recovery_attempt -lt 2 ]]; then
local new_disk=$((DISK_SIZE * 2))
echo -e " ${GN}${next_option})${CL} Retry with more disk space (Disk: ${DISK_SIZE}${new_disk} GB)"
DISK_OPTION=$next_option
next_option=$((next_option + 1))
else
echo -e " ${DGN}-)${CL} ${DGN}Disk resize retry exhausted (already retried ${disk_recovery_attempt}x)${CL}"
fi
fi
if [[ "$is_network_issue" == true ]]; then
echo -e " ${GN}${next_option})${CL} Retry with DNS override in LXC (8.8.8.8 / 1.1.1.1)"
DNS_OPTION=$next_option
next_option=$((next_option + 1))
fi
local max_option=$((next_option - 1))
echo ""
echo -en "${YW}Select option [1-${max_option}] (default: 1, auto-remove in 60s): ${CL}"
local response=""
if read -t 60 -r response; then
case "${response:-1}" in
1)
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}"
;;
2)
echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}"
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.dynastyrevolution.com/DYNR/core/raw/branch/main/core/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
exit $install_exit_code
;;
3)
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 ""
local old_ctid="$CTID"
export CTID=$(get_valid_container_id "$CTID")
export VERBOSE="yes"
export var_verbose="yes"
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..."
build_container
return $?
;;
*)
local handled=false
if [[ -n "${APT_OPTION}" && "${response}" == "${APT_OPTION}" ]]; then
handled=true
if [[ "$var_os" == "alpine" ]]; then
echo -e "\n${TAB}${HOLD}${YW}Repairing APK state in container ${CTID}...${CL}"
pct exec "$CTID" -- ash -c "
apk fix 2>/dev/null || true
apk cache clean 2>/dev/null || true
apk update 2>/dev/null || true
" >/dev/null 2>&1 || true
echo -e "${BFR}${CM}${GN}APK state repaired in container ${CTID}${CL}"
else
echo -e "\n${TAB}${HOLD}${YW}Repairing APT/DPKG state in container ${CTID}...${CL}"
pct exec "$CTID" -- bash -c "
DEBIAN_FRONTEND=noninteractive dpkg --configure -a 2>/dev/null || true
apt-get -f install -y 2>/dev/null || true
apt-get clean 2>/dev/null
apt-get update 2>/dev/null || true
" >/dev/null 2>&1 || true
echo -e "${BFR}${CM}${GN}APT/DPKG state repaired in container ${CTID}${CL}"
fi
echo ""
export VERBOSE="yes"
export var_verbose="yes"
echo -e "${YW}Re-running installation in existing container ${CTID}:${CL}"
echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB"
echo -e " Verbose: ${GN}enabled${CL}"
echo ""
msg_info "Re-running installation script..."
set +Eeuo pipefail
trap - ERR
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://git.dynastyrevolution.com/DYNR/core/raw/branch/main/install/${var_install}.sh)"
local apt_retry_exit=$?
set -Eeuo pipefail
trap 'error_handler' ERR
local apt_retry_code=0
if [[ -n "${SESSION_ID:-}" ]]; then
local retry_error_flag="/root/.install-${SESSION_ID}.failed"
if pct exec "$CTID" -- test -f "$retry_error_flag" 2>/dev/null; then
apt_retry_code=$(pct exec "$CTID" -- cat "$retry_error_flag" 2>/dev/null || echo "1")
pct exec "$CTID" -- rm -f "$retry_error_flag" 2>/dev/null || true
fi
fi
if [[ $apt_retry_code -eq 0 && $apt_retry_exit -ne 0 ]]; then
apt_retry_code=$apt_retry_exit
fi
if [[ $apt_retry_code -eq 0 ]]; then
msg_ok "Installation completed successfully after APT repair!"
INSTALL_COMPLETE=true
post_update_to_api "done" "0" "force"
return 0
else
msg_error "Installation still failed after APT repair (exit code: ${apt_retry_code})"
install_exit_code=$apt_retry_code
fi
fi
if [[ -n "${OOM_OPTION}" && "${response}" == "${OOM_OPTION}" ]]; then
handled=true
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 ""
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 * 2))
export CORE_COUNT=$((CORE_COUNT * 2))
export var_ram="$RAM_SIZE"
export var_cpu="$CORE_COUNT"
export VERBOSE="yes"
export var_verbose="yes"
export RECOVERY_ATTEMPT=$((${RECOVERY_ATTEMPT:-0} + 1))
echo -e "${YW}Rebuilding with increased resources (attempt ${RECOVERY_ATTEMPT}/2):${CL}"
echo -e " Container ID: ${old_ctid}${CTID}"
echo -e " RAM: ${old_ram}${GN}${RAM_SIZE}${CL} MiB (x2)"
echo -e " CPU: ${old_cpu}${GN}${CORE_COUNT}${CL} cores (x2)"
echo -e " Disk: ${DISK_SIZE} GB | Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
echo -e " Verbose: ${GN}enabled${CL}"
echo ""
msg_info "Restarting installation..."
build_container
return $?
fi
if [[ -n "${DISK_OPTION}" && "${response}" == "${DISK_OPTION}" ]]; then
handled=true
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more disk space...${CL}"
pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
echo ""
local old_ctid="$CTID"
local old_disk="$DISK_SIZE"
export CTID=$(get_valid_container_id "$CTID")
export DISK_SIZE=$((DISK_SIZE * 2))
export var_disk="$DISK_SIZE"
export VERBOSE="yes"
export var_verbose="yes"
export DISK_RECOVERY_ATTEMPT=$((${DISK_RECOVERY_ATTEMPT:-0} + 1))
echo -e "${YW}Rebuilding with increased disk space (attempt ${DISK_RECOVERY_ATTEMPT}/2):${CL}"
echo -e " Container ID: ${old_ctid}${CTID}"
echo -e " Disk: ${old_disk}${GN}${DISK_SIZE}${CL} GB (x2)"
echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores"
echo -e " Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
echo -e " Verbose: ${GN}enabled${CL}"
echo ""
msg_info "Restarting installation..."
build_container
return $?
fi
if [[ -n "${DNS_OPTION}" && "${response}" == "${DNS_OPTION}" ]]; then
handled=true
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with DNS override...${CL}"
pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
echo ""
local old_ctid="$CTID"
export CTID=$(get_valid_container_id "$CTID")
export DNS_RETRY_OVERRIDE="true"
export VERBOSE="yes"
export var_verbose="yes"
echo -e "${YW}Rebuilding with DNS override in LXC:${CL}"
echo -e " Container ID: ${old_ctid}${CTID}"
echo -e " DNS: ${GN}8.8.8.8, 1.1.1.1${CL} (inside LXC only)"
echo -e " Verbose: ${GN}enabled${CL}"
echo ""
msg_info "Restarting installation..."
build_container
return $?
fi
if [[ "$handled" == false ]]; then
echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}"
exit $install_exit_code
fi
;;
esac
else
echo ""
msg_info "No response - removing container ${CTID}"
pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true
msg_ok "Container ${CTID} removed"
fi
$STD echo -e "${TAB}⏳ Finalizing telemetry report..."
post_update_to_api "failed" "$install_exit_code" "force"
$STD echo -e "${TAB}${CM:-} Telemetry finalized"
trap - TSTP TTIN TTOU
exit $install_exit_code
fi
set -Eeuo pipefail
trap 'error_handler' ERR
}
destroy_lxc() {
if [[ -z "$CT_ID" ]]; then
msg_error "No CT_ID found. Nothing to remove."
return 65
fi
trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
local prompt
if ! read -rp "Remove this Container? <y/N> " prompt </dev/tty; then
msg_error "Aborted input (Ctrl-D/ESC)"
return 130
fi
case "${prompt,,}" in
y | yes)
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)
msg_custom "" "${BL}" "Container was not removed."
;;
*)
msg_warn "Invalid response. Container was not removed."
;;
esac
}
resolve_storage_preselect() {
local class="$1" preselect="$2" required_content=""
case "$class" in
template) required_content="vztmpl" ;;
container) required_content="rootdir" ;;
*) return 65 ;;
esac
[[ -z "$preselect" ]] && return 1
if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then
msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)"
return 238
fi
local line total used free
line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
if [[ -z "$line" ]]; then
STORAGE_INFO="n/a"
else
total="$(awk '{print $4}' <<<"$line")"
used="$(awk '{print $5}' <<<"$line")"
free="$(awk '{print $6}' <<<"$line")"
local total_h used_h free_h
if command -v numfmt >/dev/null 2>&1; then
total_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
used_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
free_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
else
STORAGE_INFO="Free: ${free} Used: ${used}"
fi
fi
STORAGE_RESULT="$preselect"
return 0
}
fix_gpu_gids() {
if [[ -z "${GPU_TYPE:-}" ]]; then
return 0
fi
msg_info "Detecting and setting correct GPU group IDs"
local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
if [[ -z "$video_gid" ]]; then
pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>&1
video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
[[ -z "$video_gid" ]] && video_gid="44"
fi
if [[ -z "$render_gid" ]]; then
pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>&1
render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
[[ -z "$render_gid" ]] && render_gid="104"
fi
pct stop "$CTID" >/dev/null 2>&1
sleep 1
[[ "$render_gid" =~ ^[0-9]+$ ]] || render_gid="104"
[[ "$video_gid" =~ ^[0-9]+$ ]] || video_gid="44"
sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG"
sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG"
pct start "$CTID" >/dev/null 2>&1
sleep 2
msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})"
if [[ "$CT_TYPE" == "0" ]]; then
pct exec "$CTID" -- sh -c "
if [ -d /dev/dri ]; then
for dev in /dev/dri/*; do
if [ -e \"\$dev\" ]; then
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
fi
" >/dev/null 2>&1
fi
}
check_storage_support() {
local CONTENT="$1" VALID=0
while IFS= read -r line; do
local STORAGE_NAME
STORAGE_NAME=$(awk '{print $1}' <<<"$line")
[[ -n "$STORAGE_NAME" ]] && VALID=1
done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
[[ $VALID -eq 1 ]]
}
select_storage() {
local CLASS=$1 CONTENT CONTENT_LABEL
case $CLASS in
container)
CONTENT='rootdir'
CONTENT_LABEL='Container'
;;
template)
CONTENT='vztmpl'
CONTENT_LABEL='Container template'
;;
iso)
CONTENT='iso'
CONTENT_LABEL='ISO image'
;;
images)
CONTENT='images'
CONTENT_LABEL='VM Disk image'
;;
backup)
CONTENT='backup'
CONTENT_LABEL='Backup'
;;
snippets)
CONTENT='snippets'
CONTENT_LABEL='Snippets'
;;
*)
msg_error "Invalid storage class '$CLASS'"
return 65
;;
esac
declare -A STORAGE_MAP
local -a MENU=()
local COL_WIDTH=0
while read -r TAG TYPE _ TOTAL USED FREE _; do
[[ -n "$TAG" && -n "$TYPE" ]] || continue
local DISPLAY="${TAG} (${TYPE})"
local USED_FMT=$(numfmt --to=iec --from-unit=1024 --format %.1f <<<"$USED")
local FREE_FMT=$(numfmt --to=iec --from-unit=1024 --format %.1f <<<"$FREE")
local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
STORAGE_MAP["$DISPLAY"]="$TAG"
MENU+=("$DISPLAY" "$INFO" "OFF")
((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
if [[ ${#MENU[@]} -eq 0 ]]; then
msg_error "No storage found for content type '$CONTENT'."
return 2
fi
if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
STORAGE_INFO="${MENU[1]}"
return 0
fi
local WIDTH=$((COL_WIDTH + 42))
while true; do
local DISPLAY_SELECTED
DISPLAY_SELECTED=$(whiptail --backtitle "Dynasty OS Framework" \
--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; }
DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
whiptail --msgbox "No valid storage selected. Please try again." 8 58
continue
fi
STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
for ((i = 0; i < ${#MENU[@]}; i += 3)); do
if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
STORAGE_INFO="${MENU[$i + 1]}"
break
fi
done
if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then
validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes"
fi
return 0
done
}
validate_storage_space() {
local storage="$1"
local required_gb="${2:-8}"
local show_dialog="${3:-no}"
local storage_line
storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}')
if [[ -z "$storage_line" ]]; then
[[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' not found!\n\nThe storage may be unavailable or disabled." 10 60
return 236
fi
local status
status=$(awk '{print $3}' <<<"$storage_line")
if [[ "$status" == "disabled" ]]; then
[[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' is disabled!\n\nPlease enable the storage first." 10 60
return 236
fi
local storage_type storage_free
storage_type=$(awk '{print $2}' <<<"$storage_line")
storage_free=$(awk '{print $6}' <<<"$storage_line")
if [[ -z "$storage_free" || "$storage_free" == "0" ]]; then
return 0
fi
local required_kb=$((required_gb * 1024 * 1024))
local free_gb_fmt
free_gb_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$storage_free" 2>/dev/null || echo "${storage_free}KB")
if [[ "$storage_free" -lt "$required_kb" ]]; then
if [[ "$show_dialog" == "yes" ]]; then
whiptail --msgbox "⚠️ Warning: Storage '$storage' may not have enough space!\n\nStorage Type: ${storage_type}\nRequired: ${required_gb}GB\nAvailable: ${free_gb_fmt}\n\nYou can continue, but creation might fail." 14 70
fi
return 236
fi
return 0
}
create_lxc_container() {
if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
offer_lxc_stack_upgrade_and_maybe_retry() {
local do_retry="${1:-no}"
local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
_pvec_i="$(pkg_ver pve-container)"
_lxcp_i="$(pkg_ver lxc-pve)"
_pvec_c="$(pkg_cand pve-container)"
_lxcp_c="$(pkg_cand lxc-pve)"
if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
fi
if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
fi
if [[ $need -eq 0 ]]; then
msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
return 0
fi
msg_warn "An update for the Proxmox LXC stack is available"
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 </dev/tty
case "${_ans,,}" in
y | yes)
msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
apt_update_safe
if $STD apt-get install -y --only-upgrade pve-container lxc-pve; then
msg_ok "LXC stack upgraded."
if [[ "$do_retry" == "yes" ]]; then
msg_info "Retrying container creation after upgrade"
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
msg_ok "Container created successfully after upgrade."
return 0
else
msg_error "pct create still failed after upgrade. See $LOGFILE"
return 3
fi
fi
return 1
else
msg_error "Upgrade failed. Please check APT output."
return 3
fi
;;
*) return 2 ;;
esac
}
[[ "${CTID:-}" ]] || {
msg_error "You need to set 'CTID' variable."
exit 203
}
[[ "${PCT_OSTYPE:-}" ]] || {
msg_error "You need to set 'PCT_OSTYPE' variable."
exit 204
}
msg_debug "CTID=$CTID"
msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
[[ "$CTID" -ge 100 ]] || {
msg_error "ID cannot be less than 100."
exit 205
}
if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
unset CTID
msg_error "Cannot use ID that is already in use."
exit 206
fi
post_to_api
post_progress_to_api "validation"
check_storage_support "rootdir" || {
msg_error "No valid storage found for 'rootdir' [Container]"
exit 119
}
check_storage_support "vztmpl" || {
msg_error "No valid storage found for 'vztmpl' [Template]"
exit 120
}
if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
TEMPLATE_STORAGE="$STORAGE_RESULT"
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
else
while true; do
if [[ -z "${var_template_storage:-}" ]]; then
if select_storage template; then
TEMPLATE_STORAGE="$STORAGE_RESULT"
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
break
fi
fi
done
fi
if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
CONTAINER_STORAGE="$STORAGE_RESULT"
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
else
if [[ -z "${var_container_storage:-}" ]]; then
if select_storage container; then
CONTAINER_STORAGE="$STORAGE_RESULT"
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
fi
fi
fi
msg_info "Validating storage '$CONTAINER_STORAGE'"
STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1 || true)
if [[ -z "$STORAGE_TYPE" ]]; then
msg_error "Storage '$CONTAINER_STORAGE' not found in /etc/pve/storage.cfg"
exit 213
fi
case "$STORAGE_TYPE" in
iscsidirect)
msg_error "Storage '$CONTAINER_STORAGE' uses iSCSI-direct which does not support container rootfs."
exit 212
;;
iscsi | zfs)
msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support container rootdir content."
exit 213
;;
cephfs)
msg_error "Storage '$CONTAINER_STORAGE' uses CephFS which is not supported for LXC rootfs."
exit 219
;;
pbs)
msg_error "Storage '$CONTAINER_STORAGE' is a Proxmox Backup Server — cannot be used for containers."
exit 224
;;
linstor | rbd | nfs | cifs)
if ! pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null; then
msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) is not accessible or inactive."
exit 217
fi
;;
esac
if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then
msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support 'rootdir' content."
exit 213
fi
msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated"
msg_info "Validating template storage '$TEMPLATE_STORAGE'"
TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 || true)
if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then
msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'"
fi
msg_ok "Template storage '$TEMPLATE_STORAGE' validated"
if [[ -f /etc/pve/corosync.conf ]]; then
msg_info "Checking cluster quorum"
if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
exit 210
fi
msg_ok "Cluster is quorate"
fi
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
case "$PCT_OSTYPE" in
debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
*) TEMPLATE_PATTERN="" ;;
esac
msg_info "Searching for template '$TEMPLATE_SEARCH'"
ONLINE_TEMPLATE=""
ONLINE_TEMPLATES=()
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
)
if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
TEMPLATE="${LOCAL_TEMPLATES[-1]}"
TEMPLATE_SOURCE="local"
msg_ok "Template search completed"
else
msg_info "No local template found, checking online catalog..."
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 [[ -z "$TEMPLATE" ]]; then
msg_warn "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."
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 </dev/tty
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
TEMPLATE_SEARCH="${PCT_OSTYPE}-${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
)
if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
TEMPLATE="${ONLINE_TEMPLATES[-1]}"
TEMPLATE_SOURCE="online"
else
msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
exit 225
fi
else
msg_custom "🚫" "${YW}" "Installation cancelled"
exit 0
fi
else
msg_error "No ${PCT_OSTYPE} templates available at all"
exit 225
fi
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 [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
fi
[[ -n "$TEMPLATE_PATH" ]] || {
if [[ -z "$TEMPLATE" ]]; then
msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"
mapfile -t AVAILABLE_VERSIONS < <(
pveam available -section system 2>/dev/null |
grep "^${PCT_OSTYPE}-" |
sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
grep -E '^[0-9]+\.[0-9]+$' |
sort -u -V 2>/dev/null || sort -u
)
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
echo ""
read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice </dev/tty
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}"
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
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]}"
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 [[ -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_custom "🚫" "${YW}" "Installation cancelled"
exit 0
fi
else
msg_error "No ${PCT_OSTYPE} templates available"
exit 220
fi
fi
}
if [[ -z "$TEMPLATE" ]]; then
msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
msg_custom "" "${YW}" "Please check:"
msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)"
msg_custom " •" "${YW}" "Does the template exist for your OS version?"
exit 225
fi
msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
NEED_DOWNLOAD=0
if [[ ! -f "$TEMPLATE_PATH" ]]; then
msg_info "Template not present locally will download."
NEED_DOWNLOAD=1
elif [[ ! -r "$TEMPLATE_PATH" ]]; then
msg_error "Template file exists but is not readable check permissions."
exit 221
elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
if [[ -n "$ONLINE_TEMPLATE" ]]; then
msg_warn "Template file too small (<1MB) re-downloading."
NEED_DOWNLOAD=1
else
msg_warn "Template looks too small, but no online version exists. Keeping local file."
fi
elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
if [[ -n "$ONLINE_TEMPLATE" ]]; then
msg_warn "Template appears corrupted re-downloading."
NEED_DOWNLOAD=1
else
msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
fi
else
$STD msg_ok "Template $TEMPLATE is present and valid."
fi
if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
TEMPLATE="$ONLINE_TEMPLATE"
NEED_DOWNLOAD=1
else
msg_custom "" "${BL}" "Continuing with local template $TEMPLATE"
fi
fi
if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
[[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
for attempt in {1..3}; do
msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1; then
msg_ok "Template download successful."
break
fi
if [[ $attempt -eq 3 ]]; then
msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
exit 222
fi
sleep $((attempt * 5))
done
fi
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
exit 223
fi
if [[ "$PCT_OSTYPE" == "debian" ]]; then
OSVER="$(parse_template_osver "$TEMPLATE")"
if [[ -n "$OSVER" ]]; then
offer_lxc_stack_upgrade_and_maybe_retry "no" || true
fi
fi
msg_info "Creating LXC container"
grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then
PCT_OPTIONS="$PCT_OPTIONS
-rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}"
fi
lockfile="/tmp/template.${TEMPLATE}.lock"
if [[ -f "$lockfile" ]]; then
local lock_age=$(($(date +%s) - $(stat -c %Y "$lockfile" 2>/dev/null || echo 0)))
if [[ $lock_age -gt 3600 ]]; then
msg_warn "Removing stale template lock file (age: ${lock_age}s)"
rm -f "$lockfile"
fi
fi
exec 9>"$lockfile" || {
msg_error "Failed to create lock file '$lockfile'."
exit 200
}
local lock_attempts=0
local max_lock_attempts=10
local lock_wait_time=30
while ! flock -w "$lock_wait_time" 9; do
lock_attempts=$((lock_attempts + 1))
if [[ $lock_attempts -ge $max_lock_attempts ]]; then
msg_error "Timeout while waiting for template lock after ${max_lock_attempts} attempts."
msg_custom "💡" "${YW}" "Another container creation may be stuck. Check running processes or remove: $lockfile"
exit 211
fi
msg_custom "⏳" "${YW}" "Another container is being created with this template. Waiting... (attempt ${lock_attempts}/${max_lock_attempts})"
done
LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
_flush_pct_log() {
if [[ -s "${LOGFILE:-}" && -n "${BUILD_LOG:-}" ]]; then
{
echo ""
echo "--- pct create output (${LOGFILE}) ---"
cat "$LOGFILE"
echo "--- end pct create output ---"
} >>"$BUILD_LOG" 2>/dev/null || true
fi
}
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" >>"${BUILD_LOG:-/dev/null}" 2>&1 || {
msg_error "Failed to download template '$TEMPLATE' to storage '$TEMPLATE_STORAGE'"
exit 222
}
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" >>"${BUILD_LOG:-/dev/null}" 2>&1 || {
msg_error "Failed to re-download template '$TEMPLATE'"
exit 222
}
msg_ok "Template re-downloaded"
else
msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
fi
fi
exec 9>&-
msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS"
msg_debug "Logfile: $LOGFILE"
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..."
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" >>"${BUILD_LOG:-/dev/null}" 2>&1
msg_ok "Template re-downloaded"
fi
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
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_ok "Trying local storage fallback"
msg_info "Downloading template to local"
pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/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
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
msg_warn "pct reported 'unsupported version' LXC stack might be too old for this template"
offer_lxc_stack_upgrade_and_maybe_retry "yes"
rc=$?
case $rc in
0) : ;;
2)
msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve"
_flush_pct_log
exit 231
;;
3)
msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE"
_flush_pct_log
exit 231
;;
esac
else
msg_error "Container creation failed. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
set +x
fi
_flush_pct_log
exit 209
fi
else
msg_ok "Container successfully created using local fallback."
fi
else
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
msg_warn "pct reported 'unsupported version' LXC stack might be too old for this template"
offer_lxc_stack_upgrade_and_maybe_retry "yes"
rc=$?
case $rc in
0) : ;;
2)
msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve"
_flush_pct_log
exit 231
;;
3)
msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE"
_flush_pct_log
exit 231
;;
esac
else
msg_error "Container creation failed. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
set +x
fi
_flush_pct_log
exit 209
fi
fi
else
msg_ok "Container successfully created after template repair."
fi
fi
pct list | awk '{print $1}' | grep -qx "$CTID" || {
msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE"
_flush_pct_log
exit 215
}
grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
msg_error "RootFS entry missing in container config. See $LOGFILE"
_flush_pct_log
exit 216
}
msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
if [[ -s "$LOGFILE" && -n "${BUILD_LOG:-}" ]]; then
{
echo ""
echo "--- pct create output ---"
cat "$LOGFILE"
echo "--- end pct create output ---"
} >>"$BUILD_LOG" 2>/dev/null || true
fi
}
description() {
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<h2 style='font-size: 24px; margin: 20px 0; color: #1fd9f0;'>DYNASTY REVOLUTION</h2>
<p style='margin: 16px 0; color: #8700af;'>${APP} LXC</p>
<a href='https://git.dynastyrevolution.com' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #af8700;'>Dynasty Core</a>
</div>
EOF
)
pct set "$CTID" -description "$DESCRIPTION"
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
systemctl start ping-instances.service
fi
INSTALL_COMPLETE=true
post_update_to_api "done" "none"
}
ensure_log_on_host() {
[[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]] && return 0
if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]] && command -v pct &>/dev/null; then
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
if [[ ! -s "$combined_log" ]]; then
{
echo "================================================================================"
echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
echo "Container ID: ${CTID}"
echo "Session ID: ${SESSION_ID}"
echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
echo "================================================================================"
echo ""
} >"$combined_log" 2>/dev/null || return 0
if [[ -f "${BUILD_LOG:-}" ]]; then
{
echo "================================================================================"
echo "PHASE 1: CONTAINER CREATION (Host)"
echo "================================================================================"
cat "${BUILD_LOG}"
echo ""
} >>"$combined_log"
fi
local temp_log="/tmp/.install-temp-${SESSION_ID}.log"
if timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then
{
echo "================================================================================"
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
echo "================================================================================"
cat "$temp_log"
echo ""
} >>"$combined_log"
rm -f "$temp_log"
fi
fi
if [[ -s "$combined_log" ]]; then
INSTALL_LOG="$combined_log"
fi
fi
}