-
-
Save jaminmc/7e786a8947746439f7b8a8e2726e629d to your computer and use it in GitHub Desktop.
| #!/bin/bash | |
| # Script to create an OpenWrt LXC container in Proxmox | |
| # Supports stable, release candidates (with prompt if newer), and snapshots | |
| # Robust template handling (reuse / redownload / corruption check) | |
| # Aborts cleanly on Esc/Cancel in dialogs | |
| # Default resource values | |
| DEFAULT_MEMORY="256" # MB | |
| DEFAULT_CORES="2" # CPU cores | |
| DEFAULT_STORAGE="0.5" # GB | |
| DEFAULT_SUBNET="10.23.45.1/24" # LAN subnet | |
| ARCH="x86_64" # Architecture | |
| TEMPLATE_DIR="/var/lib/vz/template/cache" # Default template location | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| NC='\033[0m' | |
| # Exit handler | |
| exit_script() { | |
| local code=$1 | |
| local msg=$2 | |
| [ -n "$msg" ] && echo -e "${RED}$msg${NC}" | |
| exit "$code" | |
| } | |
| # Must run as root | |
| [ "$EUID" -ne 0 ] && exit_script 1 "This script must be run as root" | |
| # Check required commands | |
| for cmd in wget pct pvesm ip curl whiptail pvesh bridge stat tar numfmt; do | |
| command -v "$cmd" &>/dev/null || exit_script 1 "Required command not found: $cmd" | |
| done | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Helper functions | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| whiptail_radiolist() { | |
| local title="$1" prompt="$2" height="$3" width="$4" items=("${@:5}") | |
| local selection | |
| selection=$(whiptail --title "$title" --radiolist "$prompt" "$height" "$width" "$((${#items[@]} / 3))" "${items[@]}" 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| echo "$selection" | |
| } | |
| whiptail_input() { | |
| local title="$1" prompt="$2" default="$3" var="$4" | |
| local input | |
| input=$(whiptail --title "$title" --inputbox "$prompt\n\nDefault: $default" 10 60 "$default" 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| eval "$var=\"${input:-$default}\"" | |
| } | |
| detect_latest_stable() { | |
| local ver | |
| ver=$(curl -sSf "https://downloads.openwrt.org/" | | |
| grep -oP '(?<=OpenWrt )\d+\.\d+\.\d+(?=\s|</strong>|Released)' | | |
| head -1) | |
| if [ -z "$ver" ]; then | |
| ver=$(curl -sSf "https://downloads.openwrt.org/releases/" | | |
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | | |
| grep -vE '-(rc|beta|alpha|test)' | | |
| sort -V | tail -1) | |
| fi | |
| [ -z "$ver" ] && ver="24.10.5" | |
| echo "$ver" | |
| } | |
| detect_newest_available() { | |
| local ver | |
| ver=$(curl -sSf "https://downloads.openwrt.org/releases/" | | |
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?' | | |
| sort -V | tail -1) | |
| [ -z "$ver" ] && ver="$(detect_latest_stable)" | |
| echo "$ver" | |
| } | |
| select_storage() { | |
| local -a menu | |
| while read -r line || [ -n "$line" ]; do | |
| local tag=$(echo "$line" | awk '{print $1}') | |
| local type=$(echo "$line" | awk '{printf "%-10s", $2}') | |
| local free=$(echo "$line" | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf "%9sB", $6}') | |
| menu+=("$tag" "Type: $type Free: $free" "OFF") | |
| done < <(pvesm status -content rootdir | awk 'NR>1') | |
| [ ${#menu[@]} -eq 0 ] && exit_script 1 "No storage pools found" | |
| [ $((${#menu[@]} / 3)) -eq 1 ] && echo "${menu[0]}" && return | |
| whiptail_radiolist "Storage Pools" "Select storage for container:" 16 80 "${menu[@]}" | |
| } | |
| detect_network_options() { | |
| BRIDGE_LIST=($(ip link | grep -o 'vmbr[0-9]\+' | sort -u)) | |
| BRIDGE_COUNT=${#BRIDGE_LIST[@]} | |
| local all_devs=$(ip link show | grep -oE '^[0-9]+: ([^:]+):' | awk '{print $2}' | cut -d':' -f1 | grep -vE '^(lo|vmbr|veth|tap|fwbr|fwpr|fwln)') | |
| readarray -t ALL_DEVICES <<<"$all_devs" | |
| local bridged_devs=$(bridge link show | cut -d ":" -f2 | cut -d " " -f2) | |
| readarray -t BRIDGED_DEVICES <<<"$bridged_devs" | |
| UNBRIDGED_DEVICES=() | |
| for dev in "${ALL_DEVICES[@]}"; do | |
| local bridged=false | |
| for bdev in "${BRIDGED_DEVICES[@]}"; do [ "$dev" = "$bdev" ] && bridged=true && break; done | |
| [ "$bridged" = false ] && UNBRIDGED_DEVICES+=("$dev") | |
| done | |
| UNBRIDGED_COUNT=${#UNBRIDGED_DEVICES[@]} | |
| } | |
| select_network_option() { | |
| local type="$1" eth="$2" | |
| local -a menu=("None" "No network assigned" "OFF") | |
| for b in "${BRIDGE_LIST[@]}"; do menu+=("bridge:$b" "Bridge $b" "OFF"); done | |
| for d in "${UNBRIDGED_DEVICES[@]}"; do menu+=("device:$d" "Device $d" "OFF"); done | |
| whiptail_radiolist "$type Network" "Select for $type ($eth) or None:" 16 60 "${menu[@]}" | |
| } | |
| detect_next_ctid() { | |
| local id=$(pvesh get /cluster/nextid 2>/dev/null) | |
| echo "${id:-100}" | |
| } | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main logic | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| echo -e "${GREEN}Fetching OpenWrt version info...${NC}" | |
| STABLE_VER=$(detect_latest_stable) | |
| echo -e "${GREEN}Latest stable: $STABLE_VER${NC}" | |
| NEWEST_VER=$(detect_newest_available) | |
| if [ "$NEWEST_VER" != "$STABLE_VER" ] && [[ "$NEWEST_VER" == *"-rc"* ]]; then | |
| if whiptail --title "Newer RC Available" --yesno \ | |
| "Newer release candidate found:\n $NEWEST_VER\nStable: $STABLE_VER\n\nUse RC?" \ | |
| 12 70 3>&1 1>&2 2>&3; then | |
| VER="$NEWEST_VER" | |
| echo -e "${GREEN}Using RC: $VER${NC}" | |
| else | |
| VER="$STABLE_VER" | |
| echo -e "${GREEN}Using stable: $VER${NC}" | |
| fi | |
| else | |
| VER="$STABLE_VER" | |
| echo -e "${GREEN}Using: $VER (no newer RC)${NC}" | |
| fi | |
| RELEASE_TYPE=$(whiptail --title "Release Type" --radiolist \ | |
| "Choose type (Stable allows manual/RC override):" 10 65 2 \ | |
| "Stable" "Stable or RC (current: $VER)" "ON" \ | |
| "Snapshot" "Latest snapshot" "OFF" 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| if [ "$RELEASE_TYPE" = "Stable" ]; then | |
| whiptail_input "OpenWrt Version" "Enter version (stable or RC)" "$VER" VER \ | |
| || exit_script 1 "Aborted by user" | |
| DOWNLOAD_URL="https://downloads.openwrt.org/releases/$VER/targets/x86/64/openwrt-${VER}-x86-64-rootfs.tar.gz" | |
| TEMPLATE_FILE="openwrt-${VER}-${ARCH}.tar.gz" | |
| else | |
| VER="snapshot" | |
| DOWNLOAD_URL="https://downloads.openwrt.org/snapshots/targets/x86/64/openwrt-x86-64-rootfs.tar.gz" | |
| TEMPLATE_FILE="openwrt-snapshot-${ARCH}.tar.gz" | |
| if whiptail --title "LuCI" --yesno "Install LuCI automatically (snapshot)?" 10 60; then | |
| INSTALL_LUCI=1 | |
| else | |
| INSTALL_LUCI=0 | |
| fi || exit_script 1 "Aborted by user" | |
| fi | |
| NEXT_CTID=$(detect_next_ctid) | |
| whiptail_input "Container ID" "Enter ID" "$NEXT_CTID" CTID || exit_script 1 "Aborted" | |
| whiptail_input "Container Name" "Enter name" "openwrt-$CTID" CTNAME || exit_script 1 "Aborted" | |
| # Password | |
| while true; do | |
| PASSWORD=$(whiptail --title "Root Password" --passwordbox "Enter password (blank = skip)" 10 50 3>&1 1>&2 2>&3) | |
| ret=$? | |
| [ $ret -ne 0 ] && { PASSWORD=""; break; } | |
| PASSWORD_CONFIRM=$(whiptail --title "Confirm" --passwordbox "Confirm password" 10 50 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| if [ -z "$PASSWORD" ] && [ -z "$PASSWORD_CONFIRM" ]; then | |
| echo -e "${GREEN}Password skipped.${NC}" | |
| break | |
| elif [ "$PASSWORD" = "$PASSWORD_CONFIRM" ]; then | |
| break | |
| else | |
| whiptail --title "Error" --msgbox "Passwords do not match." 8 50 | |
| fi | |
| done | |
| DISABLE_SYNTPD=$(whiptail --title "sysntpd" --radiolist \ | |
| "Disable sysntpd (recommended for containers)?" 12 60 2 \ | |
| "Yes" "Disable (default)" "ON" \ | |
| "No" "Keep enabled" "OFF" 3>&1 1>&2 2>&3) \ | |
| || exit_script 1 "Aborted by user" | |
| whiptail_input "Memory (MB)" "Memory size" "$DEFAULT_MEMORY" MEMORY || exit_script 1 "Aborted" | |
| whiptail_input "CPU Cores" "Number of cores" "$DEFAULT_CORES" CORES || exit_script 1 "Aborted" | |
| whiptail_input "Storage (GB)" "Storage limit" "$DEFAULT_STORAGE" STORAGE_SIZE || exit_script 1 "Aborted" | |
| whiptail_input "LAN Subnet" "e.g. 10.23.45.1/24" "$DEFAULT_SUBNET" SUBNET || exit_script 1 "Aborted" | |
| # Basic validation | |
| [[ "$CTID" =~ ^[0-9]+$ && "$CTID" -ge 100 ]] || exit_script 1 "ID must be >= 100" | |
| pct list | awk '{print $1}' | grep -q "^$CTID$" && exit_script 1 "ID $CTID in use" | |
| [[ "$MEMORY" =~ ^[0-9]+$ && "$MEMORY" -ge 64 ]] || exit_script 1 "Memory >= 64" | |
| [[ "$CORES" =~ ^[0-9]+$ && "$CORES" -ge 1 ]] || exit_script 1 "Cores >= 1" | |
| [[ "$STORAGE_SIZE" =~ ^[0-9]*\.?[0-9]+$ && $(echo "$STORAGE_SIZE > 0" | bc) -eq 1 ]] || exit_script 1 "Storage > 0 required" | |
| LAN_IP=$(echo "$SUBNET" | cut -d'/' -f1) | |
| LAN_PREFIX=$(echo "$SUBNET" | cut -d'/' -f2) | |
| case "$LAN_PREFIX" in | |
| 24) LAN_NETMASK="255.255.255.0" ;; | |
| 23) LAN_NETMASK="255.255.254.0" ;; | |
| 22) LAN_NETMASK="255.255.252.0" ;; | |
| 16) LAN_NETMASK="255.255.0.0" ;; | |
| *) exit_script 1 "Unsupported prefix /$LAN_PREFIX" ;; | |
| esac | |
| STORAGE=$(select_storage) | |
| detect_network_options | |
| [ "$BRIDGE_COUNT" -eq 0 ] && [ "$UNBRIDGED_COUNT" -eq 0 ] && echo -e "${RED}Warning: No network devices found${NC}" | |
| WAN_OPTION=$(select_network_option "WAN" "eth0") || exit_script 1 "Aborted" | |
| LAN_OPTION=$(select_network_option "LAN" "eth1") || exit_script 1 "Aborted" | |
| WAN_BRIDGE=""; WAN_DEVICE="" | |
| [[ "$WAN_OPTION" == bridge:* ]] && WAN_BRIDGE="${WAN_OPTION#bridge:}" | |
| [[ "$WAN_OPTION" == device:* ]] && WAN_DEVICE="${WAN_OPTION#device:}" | |
| LAN_BRIDGE=""; LAN_DEVICE="" | |
| [[ "$LAN_OPTION" == bridge:* ]] && LAN_BRIDGE="${LAN_OPTION#bridge:}" | |
| [[ "$LAN_OPTION" == device:* ]] && LAN_DEVICE="${LAN_OPTION#device:}" | |
| # Summary | |
| SUMMARY="Summary:\n" | |
| SUMMARY+=" Version: $VER\n" | |
| SUMMARY+=" ID/Name: $CTID / $CTNAME\n" | |
| SUMMARY+=" Password: $( [ -n "$PASSWORD" ] && echo Set || echo Skipped )\n" | |
| SUMMARY+=" sysntpd: $( [ "$DISABLE_SYNTPD" = "Yes" ] && echo DISABLED || echo Enabled )\n" | |
| SUMMARY+=" Memory/Cores/Storage: $MEMORY MB / $CORES / $STORAGE_SIZE GB on $STORAGE\n" | |
| SUMMARY+=" LAN: $SUBNET\n" | |
| SUMMARY+=" WAN: ${WAN_BRIDGE:-${WAN_DEVICE:-None}} (eth0)\n" | |
| SUMMARY+=" LAN: ${LAN_BRIDGE:-${LAN_DEVICE:-None}} (eth1)\n" | |
| [ "$RELEASE_TYPE" = "Snapshot" ] && [ "${INSTALL_LUCI:-0}" -eq 1 ] && SUMMARY+=" LuCI: auto-install\n" | |
| whiptail --title "Confirm" --yesno "$SUMMARY\n\nCreate container?" 22 75 \ | |
| || exit_script 0 "Aborted by user" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Template handling | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEMPLATE_PATH="$TEMPLATE_DIR/$TEMPLATE_FILE" | |
| download_needed=1 | |
| if [ -f "$TEMPLATE_PATH" ]; then | |
| FILE_SIZE=$(stat -c %s "$TEMPLATE_PATH" 2>/dev/null || echo 0) | |
| if [ "$FILE_SIZE" -eq 0 ]; then | |
| echo -e "${RED}Existing file empty β redownload${NC}" | |
| rm -f "$TEMPLATE_PATH" | |
| elif ! tar tzf "$TEMPLATE_PATH" >/dev/null 2>&1; then | |
| echo -e "${RED}Corrupt template detected${NC}" | |
| if whiptail --title "Corrupt File" --yesno "Redownload?" 10 60; then | |
| rm -f "$TEMPLATE_PATH" | |
| else | |
| exit_script 1 "Aborted - corrupt file kept" | |
| fi | |
| else | |
| SIZE_H=$(numfmt --to=iec --format %.2f "$FILE_SIZE") | |
| if whiptail --title "Reuse Template?" --yesno \ | |
| "Found:\n $TEMPLATE_FILE\n Size: $SIZE_H\n\nReuse? (No = redownload)" 12 70; then | |
| echo -e "${GREEN}Reusing existing template${NC}" | |
| download_needed=0 | |
| else | |
| rm -f "$TEMPLATE_PATH" | |
| fi | |
| fi | |
| fi | |
| if [ "$RELEASE_TYPE" = "Snapshot" ] && [ "$download_needed" -eq 0 ]; then | |
| FILE_AGE=$(($(date +%s) - $(stat -c %Y "$TEMPLATE_PATH" 2>/dev/null || echo 0))) | |
| if [ "$FILE_AGE" -gt 86400 ]; then | |
| echo -e "${GREEN}Snapshot >1 day old β refreshing${NC}" | |
| rm -f "$TEMPLATE_PATH" | |
| download_needed=1 | |
| fi | |
| fi | |
| if [ "$download_needed" -eq 1 ]; then | |
| echo -e "${GREEN}Downloading $VER rootfs...${NC}" | |
| wget --show-progress "$DOWNLOAD_URL" -O "$TEMPLATE_PATH.part" || { | |
| rm -f "$TEMPLATE_PATH.part" | |
| exit_script 1 "Download failed" | |
| } | |
| mv "$TEMPLATE_PATH.part" "$TEMPLATE_PATH" | |
| if ! tar tzf "$TEMPLATE_PATH" >/dev/null 2>&1; then | |
| rm -f "$TEMPLATE_PATH" | |
| exit_script 1 "Downloaded file corrupt" | |
| fi | |
| echo -e "${GREEN}Download verified${NC}" | |
| fi | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Container creation | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| echo -e "${GREEN}Creating container $CTID...${NC}" | |
| NET_OPTS=() | |
| [ -n "$WAN_BRIDGE" ] && NET_OPTS+=("--net0" "name=eth0,bridge=$WAN_BRIDGE") | |
| [ -n "$WAN_DEVICE" ] && NET_OPTS+=("--net0" "name=eth0,hwaddr=$(ip link show "$WAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2)") | |
| [ -n "$LAN_BRIDGE" ] && NET_OPTS+=("--net1" "name=eth1,bridge=$LAN_BRIDGE") | |
| [ -n "$LAN_DEVICE" ] && NET_OPTS+=("--net1" "name=eth1,hwaddr=$(ip link show "$LAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2)") | |
| pct create "$CTID" "$TEMPLATE_PATH" \ | |
| --arch amd64 \ | |
| --hostname "$CTNAME" \ | |
| --rootfs "$STORAGE:$STORAGE_SIZE" \ | |
| --memory "$MEMORY" \ | |
| --cores "$CORES" \ | |
| --unprivileged 1 \ | |
| --features nesting=1 \ | |
| --ostype unmanaged \ | |
| "${NET_OPTS[@]}" || exit_script 1 "pct create failed" | |
| pct start "$CTID" || exit_script 1 "pct start failed" | |
| sleep 5 | |
| pct exec "$CTID" -- sh -c "sed -i 's!procd_add_jail!: procd_add_jail!g' /etc/init.d/dnsmasq" 2>/dev/null | |
| [ "$DISABLE_SYNTPD" = "Yes" ] && { | |
| echo -e "${GREEN}Disabling sysntpd...${NC}" | |
| pct exec "$CTID" -- sh -c "rm -f /etc/rc.d/*sysntpd" 2>/dev/null | |
| } | |
| echo -e "${GREEN}Configuring network...${NC}" | |
| pct exec "$CTID" -- sh -c " | |
| uci set network.wan=interface; uci set network.wan.proto='dhcp'; uci set network.wan.device='eth0' | |
| uci set network.wan6=interface; uci set network.wan6.proto='dhcpv6'; uci set network.wan6.device='eth0' | |
| uci set network.lan=interface; uci set network.lan.proto='static'; uci set network.@device[0].ports='eth1' | |
| uci set network.lan.ipaddr='$LAN_IP'; uci set network.lan.netmask='$LAN_NETMASK' | |
| uci commit network; /etc/init.d/network restart | |
| " || echo -e "${RED}Network config warning${NC}" | |
| [ "$RELEASE_TYPE" = "Snapshot" ] && [ "${INSTALL_LUCI:-0}" -eq 1 ] && { | |
| echo -e "${GREEN}Installing LuCI...${NC}" | |
| sleep 15 | |
| pct exec "$CTID" -- sh -c "apk update && apk add luci" || echo -e "${RED}LuCI install failed${NC}" | |
| } | |
| [ -n "$PASSWORD" ] && { | |
| echo -e "${GREEN}Setting password...${NC}" | |
| echo -e "$PASSWORD\n$PASSWORD" | pct exec "$CTID" -- passwd || echo -e "${RED}Password set failed${NC}" | |
| } | |
| echo -e "${GREEN}Done! Container $CTID ($CTNAME) ready.${NC}" | |
| echo "Next:" | |
| echo " pct exec $CTID /bin/sh" | |
| echo " uci show network" | |
| [ "$DISABLE_SYNTPD" = "Yes" ] && { echo " sysntpd disabled"; NEXT=4; } || NEXT=3 | |
| if [ "$RELEASE_TYPE" = "Stable" ]; then | |
| echo " $NEXT. LuCI: http://$LAN_IP (if LAN up)" | |
| [ -z "$PASSWORD" ] && echo " $((NEXT+1)). Set password: pct exec $CTID passwd" | |
| else | |
| if [ "${INSTALL_LUCI:-0}" -eq 1 ]; then | |
| echo " $NEXT. LuCI β http://$LAN_IP" | |
| [ -z "$PASSWORD" ] && echo " $((NEXT+1)). Set password: pct exec $CTID passwd" | |
| else | |
| echo " $NEXT. apk update" | |
| echo " $((NEXT+1)). apk add luci" | |
| echo " $((NEXT+2)). http://$LAN_IP (after LAN config)" | |
| [ -z "$PASSWORD" ] && echo " $((NEXT+3)). Set password: pct exec $CTID passwd" | |
| fi | |
| fi | |
| exit 0 |
I have no way of testing an ARM setup... But if you know what the Arch you are needing, you can modify the script to download that...
I did run the script through Grok, and asked it to adapt the script to also work with ARM... So it may or may not work.
#!/bin/bash
# Script to create an OpenWrt LXC container in Proxmox
# Downloads from openwrt.org with latest stable or snapshot version, detects bridges/devices, IDs, configures network, sets optional password
# Pre-configures WAN/LAN in UCI, includes summary and confirmation, optional LuCI install for snapshots with apk
# Modified to support ARM (aarch64 and armv7) alongside x86_64, using correct armsr/armv8 and armsr/armv7 targets
# Default resource values
DEFAULT_MEMORY="256" # MB
DEFAULT_CORES="2" # CPU cores
DEFAULT_STORAGE="0.5" # GB
DEFAULT_SUBNET="10.23.45.1/24" # LAN subnet
ARCH="x86_64" # Default architecture
TEMPLATE_DIR="/var/lib/vz/template/cache" # Default template location
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
# Exit handler for cleanup and messages
exit_script() {
local code=$1
local msg=$2
[ -n "$msg" ] && echo -e "${RED}$msg${NC}"
exit "$code"
}
# Check if running as root
[ "$EUID" -ne 0 ] && exit_script 1 "Error: This script must be run as root"
# Check required tools
for cmd in wget pct pvesm ip curl whiptail pvesh bridge stat; do
command -v "$cmd" &>/dev/null || exit_script 1 "Error: $cmd is not installed. Please install it first."
done
# Generic whiptail radiolist function
whiptail_radiolist() {
local title="$1" prompt="$2" height="$3" width="$4" items=("${@:5}")
local selection
selection=$(whiptail --title "$title" --radiolist "$prompt" "$height" "$width" "$((${#items[@]} / 3))" "${items[@]}" 3>&1 1>&2 2>&3) || \
exit_script 1 "Error: $title selection aborted"
echo "$selection"
}
# Detect latest stable OpenWrt version (silent)
detect_latest_version() {
local ver
ver=$(curl -sSf "https://downloads.openwrt.org/releases/" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -1)
[ -z "$ver" ] && ver="24.10.0" # Default to 24.10.0 if detection fails
echo "$ver"
}
# Select storage
select_storage() {
local content='rootdir' label='Container'
local -a menu
while read -r line || [ -n "$line" ]; do
local tag=$(echo "$line" | awk '{print $1}')
local type=$(echo "$line" | awk '{printf "%-10s", $2}')
local free=$(echo "$line" | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf "%9sB", $6}')
menu+=("$tag" "Type: $type Free: $free" "OFF")
done < <(pvesm status -content "$content" | awk 'NR>1')
[ ${#menu[@]} -eq 0 ] && exit_script 1 "Error: No storage pools found for $label"
[ $((${#menu[@]} / 3)) -eq 1 ] && echo "${menu[0]}" && return
whiptail_radiolist "Storage Pools" "Which storage pool for the ${label,,}?\nUse Spacebar to select." 16 $(( $(echo "${menu[*]}" | wc -L) + 23 )) "${menu[@]}"
}
# Detect network options (bridges and unbridged devices)
detect_network_options() {
BRIDGE_LIST=($(ip link | grep -o 'vmbr[0-9]\+' | sort -u))
BRIDGE_COUNT=${#BRIDGE_LIST[@]}
local all_devs
all_devs=$(ip link show | grep -oE '^[0-9]+: ([^:]+):' | awk '{print $2}' | cut -d':' -f1 | grep -vE '^(lo|vmbr|veth|tap|fwbrdg|fwpr|fwln)')
readarray -t ALL_DEVICES <<<"$all_devs"
local bridged_devs
bridged_devs=$(bridge link show | cut -d ":" -f2 | cut -d " " -f2)
readarray -t BRIDGED_DEVICES <<<"$bridged_devs"
UNBRIDGED_DEVICES=()
for dev in "${ALL_DEVICES[@]}"; do
bridged=false
for bridged_dev in "${BRIDGED_DEVICES[@]}"; do
[ "$dev" = "$bridged_dev" ] && bridged=true && break
done
[ "$bridged" = false ] && UNBRIDGED_DEVICES+=("$dev")
done
UNBRIDGED_COUNT=${#UNBRIDGED_DEVICES[@]}
}
# Select network option
select_network_option() {
local type="$1" eth="$2"
local -a menu=("None" "No network assigned" "OFF")
for bridge in "${BRIDGE_LIST[@]}"; do
menu+=("bridge:$bridge" "Bridge $bridge" "OFF")
done
for device in "${UNBRIDGED_DEVICES[@]}"; do
menu+=("device:$device" "Device $device" "OFF")
done
whiptail_radiolist "$type Network Selection" "Select a bridge or device for $type ($eth) or 'None':\nUse Spacebar to select." 16 60 "${menu[@]}"
}
# Detect next available Container ID
detect_next_ctid() {
local id
id=$(pvesh get /cluster/nextid)
echo "${id:-100}"
}
# Prompt with default value
prompt_with_default() {
local prompt="$1" default="$2" var="$3"
read -e -p "$prompt (default: $default): " -i "$default" input
eval "$var=\"${input:-$default}\""
}
# Main execution
echo -e "${GREEN}Fetching latest stable OpenWrt version...${NC}"
STABLE_VER=$(detect_latest_version)
echo -e "${GREEN}Detected latest stable version: $STABLE_VER${NC}"
# Select architecture (x86_64, aarch64, or armv7)
ARCH=$(whiptail --title "Architecture Selection" --radiolist \
"Choose the architecture for the OpenWrt container:\nUse Spacebar to select." 12 60 3 \
"x86_64" "64-bit Intel/AMD (default)" "ON" \
"aarch64" "64-bit ARM (armv8)" "OFF" \
"armv7" "32-bit ARM" "OFF" 3>&1 1>&2 2>&3) || exit_script 1 "Error: Architecture selection aborted"
echo -e "${GREEN}Selected architecture: $ARCH${NC}"
# Select OpenWrt release type
RELEASE_TYPE=$(whiptail --title "OpenWrt Release Type" --radiolist \
"Choose the OpenWrt release type (Stable allows manual version input):\nUse Spacebar to select." 10 60 2 \
"Stable" "Stable version (e.g., $STABLE_VER)" "ON" \
"Snapshot" "Latest daily snapshot" "OFF" 3>&1 1>&2 2>&3) || exit_script 1 "Error: Release type selection aborted"
# Set architecture-specific target and download URL
if [ "$ARCH" = "x86_64" ]; then
TARGET="x86/64"
PCT_ARCH="amd64"
elif [ "$ARCH" = "aarch64" ]; then
TARGET="armsr/armv8"
PCT_ARCH="arm64"
elif [ "$ARCH" = "armv7" ]; then
TARGET="armsr/armv7"
PCT_ARCH="arm"
else
exit_script 1 "Error: Unsupported architecture $ARCH"
fi
if [ "$RELEASE_TYPE" = "Stable" ]; then
prompt_with_default "Enter OpenWrt stable version" "$STABLE_VER" VER
DOWNLOAD_URL="https://downloads.openwrt.org/releases/$VER/targets/$TARGET/openwrt-$VER-$TARGET-rootfs.tar.gz"
# Replace '/' with '-' in TARGET for filename
FILENAME_TARGET=$(echo "$TARGET" | tr '/' '-')
TEMPLATE_FILE="openwrt-$VER-$FILENAME_TARGET.tar.gz"
else
VER="snapshot"
DOWNLOAD_URL="https://downloads.openwrt.org/snapshots/targets/$TARGET/openwrt-$TARGET-rootfs.tar.gz"
# Replace '/' with '-' in TARGET for filename
FILENAME_TARGET=$(echo "$TARGET" | tr '/' '-')
TEMPLATE_FILE="openwrt-snapshot-$FILENAME_TARGET.tar.gz"
# Prompt for LuCI installation
if whiptail --title "Install LuCI" --yesno "Would you like to automatically install LuCI (graphical web interface) for the snapshot?" 10 60 3>&1 1>&2 2>&3; then
INSTALL_LUCI=1
else
INSTALL_LUCI=0
fi
fi
NEXT_CTID=$(detect_next_ctid)
prompt_with_default "Enter Container ID" "$NEXT_CTID" CTID
prompt_with_default "Enter Container Name" "openwrt-$CTID" CTNAME
while true; do
read -s -p "Enter root password (leave blank to skip): " PASSWORD; echo
read -s -p "Confirm root password: " PASSWORD_CONFIRM; echo
if [ -z "$PASSWORD" ] && [ -z "$PASSWORD_CONFIRM" ]; then
echo -e "${GREEN}Root password skipped.${NC}"
break
elif [ "$PASSWORD" = "$PASSWORD_CONFIRM" ]; then
break
else
echo -e "${RED}Passwords do not match. Please try again.${NC}"
fi
done
prompt_with_default "Enter memory size in MB" "$DEFAULT_MEMORY" MEMORY
prompt_with_default "Enter number of CPU cores" "$DEFAULT_CORES" CORES
prompt_with_default "Enter storage limit in GB" "$DEFAULT_STORAGE" STORAGE_SIZE
prompt_with_default "Enter LAN subnet" "$DEFAULT_SUBNET" SUBNET
# Validate inputs
[[ "$CTID" =~ ^[0-9]+$ && "$CTID" -ge 100 ]] || exit_script 1 "Error: Container ID must be a number >= 100"
pct list | awk '{print $1}' | grep -q "^$CTID$" && exit_script 1 "Error: Container ID $CTID is already in use"
[[ "$MEMORY" =~ ^[0-9]+$ && "$MEMORY" -ge 64 ]] || exit_script 1 "Error: Memory size must be a number >= 64 MB"
[[ "$CORES" =~ ^[0-9]+$ && "$CORES" -ge 1 ]] || exit_script 1 "Error: Core count must be a number >= 1"
[[ "$STORAGE_SIZE" =~ ^[0-9]*\.?[0-9]+$ && "$(echo "$STORAGE_SIZE > 0" | bc)" -eq 1 ]] || exit_script 1 "Error: Storage limit must be a positive number"
# Parse subnet
LAN_IP=$(echo "$SUBNET" | cut -d'/' -f1)
LAN_PREFIX=$(echo "$SUBNET" | cut -d'/' -f2)
case "$LAN_PREFIX" in
24) LAN_NETMASK="255.255.255.0" ;;
23) LAN_NETMASK="255.255.254.0" ;;
22) LAN_NETMASK="255.255.252.0" ;;
16) LAN_NETMASK="255.255.0.0" ;;
*) exit_script 1 "Error: Unsupported subnet prefix /$LAN_PREFIX. Use /16, /22, /23, or /24" ;;
esac
STORAGE=$(select_storage container)
detect_network_options
[ "$BRIDGE_COUNT" -eq 0 ] && [ "$UNBRIDGED_COUNT" -eq 0 ] && echo -e "${RED}Warning: No network options found. Selecting 'None' for WAN/LAN.${NC}"
WAN_OPTION=$(select_network_option "WAN" "eth0")
LAN_OPTION=$(select_network_option "LAN" "eth1")
WAN_BRIDGE=""; WAN_DEVICE=""
if [[ "$WAN_OPTION" == bridge:* ]]; then
WAN_BRIDGE="${WAN_OPTION#bridge:}"
elif [[ "$WAN_OPTION" == device:* ]]; then
WAN_DEVICE="${WAN_OPTION#device:}"
fi
LAN_BRIDGE=""; LAN_DEVICE=""
if [[ "$LAN_OPTION" == bridge:* ]]; then
LAN_BRIDGE="${LAN_OPTION#bridge:}"
elif [[ "$LAN_OPTION" == device:* ]]; then
LAN_DEVICE="${LAN_OPTION#device:}"
fi
# Summary and confirmation
SUMMARY="Container Configuration Summary:\n"
SUMMARY+=" OpenWrt Version: $VER\n"
SUMMARY+=" Architecture: $ARCH\n"
SUMMARY+=" Container ID: $CTID\n"
SUMMARY+=" Container Name: $CTNAME\n"
SUMMARY+=" Root Password: $( [ -n "$PASSWORD" ] && echo "Set" || echo "Not set" )\n"
SUMMARY+=" Memory: $MEMORY MB\n"
SUMMARY+=" CPU Cores: $CORES\n"
SUMMARY+=" Storage: $STORAGE_SIZE GB on $STORAGE\n"
SUMMARY+=" LAN Subnet: $SUBNET\n"
SUMMARY+=" WAN Interface: ${WAN_BRIDGE:-${WAN_DEVICE:-None}} (eth0, DHCP/DHCPv6)\n"
SUMMARY+=" LAN Interface: ${LAN_BRIDGE:-${LAN_DEVICE:-None}} (eth1, static)\n"
[ "$RELEASE_TYPE" = "Snapshot" ] && [ "$INSTALL_LUCI" -eq 1 ] && SUMMARY+=" LuCI: Will be installed automatically\n"
whiptail --title "Confirm Container Creation" --yesno "$SUMMARY\nProceed with container creation?" 20 60 || exit_script 0 "Container creation aborted by user"
# Download template with snapshot age check
if [ ! -f "$TEMPLATE_DIR/$TEMPLATE_FILE" ]; then
echo -e "${GREEN}Downloading OpenWrt $VER rootfs for $ARCH...${NC}"
wget -q "$DOWNLOAD_URL" -O "$TEMPLATE_DIR/$TEMPLATE_FILE" || exit_script 1 "Error: Failed to download OpenWrt $VER image for $ARCH"
else
if [ "$RELEASE_TYPE" = "Snapshot" ]; then
# Check if snapshot file is older than 1 day (86400 seconds)
FILE_AGE=$(($(date +%s) - $(stat -c %Y "$TEMPLATE_DIR/$TEMPLATE_FILE")))
if [ "$FILE_AGE" -gt 86400 ]; then
echo -e "${GREEN}Snapshot is older than 1 day, refreshing...${NC}"
rm -f "$TEMPLATE_DIR/$TEMPLATE_FILE"
wget -q "$DOWNLOAD_URL" -O "$TEMPLATE_DIR/$TEMPLATE_FILE" || exit_script 1 "Error: Failed to download OpenWrt snapshot for $ARCH"
else
echo -e "${GREEN}Using existing OpenWrt snapshot: $TEMPLATE_FILE${NC}"
fi
else
echo -e "${GREEN}Using existing OpenWrt image: $TEMPLATE_FILE${NC}"
fi
fi
# Build pct create command with corrected network options
echo -e "${GREEN}Creating LXC container $CTID...${NC}"
NET_OPTS=()
[ -n "$WAN_BRIDGE" ] && NET_OPTS+=("--net0" "name=eth0,bridge=$WAN_BRIDGE")
[ -n "$WAN_DEVICE" ] && NET_OPTS+=("--net0" "name=eth0,hwaddr=$(ip link show "$WAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2)")
[ -n "$LAN_BRIDGE" ] && NET_OPTS+=("--net1" "name=eth1,bridge=$LAN_BRIDGE")
[ -n "$LAN_DEVICE" ] && NET_OPTS+=("--net1" "name=eth1,hwaddr=$(ip link show "$LAN_DEVICE" | grep -o 'ether [0-9a-f:]\+' | cut -d' ' -f2)")
pct create "$CTID" "$TEMPLATE_DIR/$TEMPLATE_FILE" \
--arch "$PCT_ARCH" \
--hostname "$CTNAME" \
--rootfs "$STORAGE:$STORAGE_SIZE" \
--memory "$MEMORY" \
--cores "$CORES" \
--unprivileged 1 \
--features nesting=1 \
--ostype unmanaged \
"${NET_OPTS[@]}" || exit_script 1 "Error: Failed to create container"
echo -e "${GREEN}Starting container $CTID...${NC}"
pct start "$CTID" || exit_script 1 "Error: Failed to start container"
pct exec "$CTID" -- sh -c "sed -i 's!procd_add_jail!: procd_add_jail!g' /etc/init.d/dnsmasq"
sleep 10
echo -e "${GREEN}Configuring network...${NC}"
pct exec "$CTID" -- sh -c "
# Configure WAN (eth0) with DHCP and DHCPv6
uci set network.wan=interface
uci set network.wan.proto='dhcp'
uci set network.wan.device='eth0'
uci set network.wan6=interface
uci set network.wan6.proto='dhcpv6'
uci set network.wan6.device='eth0'
# Configure LAN (eth1) with static IP
uci set network.lan=interface
uci set network.lan.proto='static'
uci set network.@device[0].ports='eth1'
uci set network.lan.ipaddr='$LAN_IP'
uci set network.lan.netmask='$LAN_NETMASK'
# Commit changes and restart network
uci commit network
/etc/init.d/network restart" || echo -e "${RED}Warning: Network configuration failed${NC}"
if [ "$RELEASE_TYPE" = "Snapshot" ] && [ "$INSTALL_LUCI" -eq 1 ]; then
echo -e "${GREEN}Waiting 15 seconds for internet connectivity...${NC}"
sleep 15
echo -e "${GREEN}Installing LuCI...${NC}"
pct exec "$CTID" -- sh -c "apk update; apk add luci" || echo -e "${RED}Warning: LuCI installation failed${NC}"
fi
[ -n "$PASSWORD" ] && {
echo -e "${GREEN}Setting root password...${NC}"
echo -e "$PASSWORD\n$PASSWORD" | pct exec "$CTID" -- passwd || echo -e "${RED}Warning: Failed to set root password${NC}"
} || echo -e "${GREEN}Root password not set (left blank).${NC}"
echo -e "${GREEN}Container $CTID ($CTNAME) created and started!${NC}"
echo "Next steps:"
echo "1. Access: pct exec $CTID /bin/sh"
echo "2. Verify network: uci show network"
if [ "$RELEASE_TYPE" = "Snapshot" ]; then
echo "3. Update: apk update"
if [ "$INSTALL_LUCI" -eq 1 ]; then
echo "4. LuCI installed: Access at http://$LAN_IP (if LAN configured)"
else
echo "4. Install LuCI: apk add luci"
[ -n "$LAN_BRIDGE" ] || [ -n "$LAN_DEVICE" ] && echo "5. LuCI: http://$LAN_IP" || echo "5. Add eth1 to activate LAN: http://$LAN_IP"
fi
else
echo "3. Update: opkg update"
echo "4. Install LuCI: opkg install luci"
[ -n "$LAN_BRIDGE" ] || [ -n "$LAN_DEVICE" ] && echo "5. LuCI: http://$LAN_IP" || echo "5. Add eth1 to activate LAN: http://$LAN_IP"
fi
[ -z "$PASSWORD" ] && echo "6. Set password if needed: pct exec $CTID passwd"Thank you @jaminmc for this! My context is the same as @brightplastik above... Wanting to install proxmox on the RK3588 (on the CM3588 NAS board) with OpenWRT as a VM alongside Ubuntu.
I'm very new to proxmox. Would I run the ARM-adapted script in proxmox the same way I would run a script in ubuntu - copy into a directory, and then run it from proxmox terminal?
@brightplastik , did you have any luck with the ARM-adapted script above?
Thanks again!
Since I do not have an ARM system to test it on, I don't know.
Hello @7thgenerationdesign, I tried hard, even pasting all commands in shell to see if it worked, but there's some problem with wget the image on the board. It fails. I eventually managed to install owrt as a privileged CT, but with a seriously convoluted procedure I might not be able to tell you in depth. I hadto do it manually though, despite the faith I had toward this script! All I can tell you is that it is doable, spending nights to address very weird errors. π¬
Thank you for the script! Btw, AI said that the script is not malicious, well structured and directory agnostic :)
The script worked for me after fixing one issue: storage selection was being cut off (off screen) so I changed one line to hard-code width to 78:
whiptail_radiolist "Storage Pools" "Which storage pool for the ${label,,}?\nUse Spacebar to select." 16 78 "${menu[@]}"
Thanks for your script !
Is your luci/admin/status/realtime/connections working ?
That doesn't look like it's working, I believe there is a kernel module mismatch between proxmox kernel modules and some openwrt expects, namely proc netfilter.
Nice, compared to the linuxcontainers.org builds this has some clear config advantages regarding the network defaults - but I think sysntpd service removal should be implemented here also to avoid conflicts between container and host?
i.e., rm -f /etc/rc.d/*sysntpd
Nice, compared to the linuxcontainers.org builds this has some clear config advantages regarding the network defaults - but I think sysntpd service removal should be implemented here also to avoid conflicts between container and host? i.e.,
rm -f /etc/rc.d/*sysntpd
Good idea! I added a prompt to disable it during the setup. It could still be useful if the OpenWrt is a NTP server, so I made it an option. Additionally, I simplified the installation process by using whiptail for all the prompts to maintain consistency in the user interface.
Is anybody else also having the issue that the lists or pages in luci/admin/status/realtime/connections are empty?
I'll be trying this soon and finding out for myself I guess, but how much (if at all) did this break with the ProxMox 9.1 update (November 19th). Also, any thoughts on OpenWRT's release candidate instead of stable? By that I just mean is the RC reasonably reliable and does it run on a more similar linux kernal version to ProxMox 9.1 or not?
OpenWrt LXC Creator Script β Changelog
- Added automatic detection of latest stable + newer release candidates (RCs)
- Prompt user if a newer RC exists β option to use it
- When choosing RC, manual version input now defaults to the selected RC version
- Robust template handling: check size + tar integrity, prompt to reuse/redownload, clean up partial/corrupt files
- Force snapshot refresh if >1 day old
- Exit cleanly on Esc/Cancel in all whiptail dialogs
- Improved messages, prompt clarity, and summary display
Now safer, smarter about versions, and friendlier to cancel out of. π
Hello Jam, I wonder if this script is useful in my case as well...I have a rk3588 Arm SBC. I managed to install proxmox fork (8.3.3) on it, and I'd be excited to use it as home lab, with a openwrt CT for networking and another CT for dockerized services.
Did you write the script to be compatible with ARM platforms, by any chance?