diff --git a/scripts/vm/openwrt-vm.sh b/scripts/vm/openwrt-vm.sh deleted file mode 100644 index da50011..0000000 --- a/scripts/vm/openwrt-vm.sh +++ /dev/null @@ -1,651 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2025 tteck -# Author: tteck (tteckster) -# Jon Spriggs (jontheniceguy) -# License: MIT -# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Based on work from https://i12bretro.github.io/tutorials/0405.html - -source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) - -function header_info { - clear - cat <<"EOF" - ____ _ __ __ - / __ \____ ___ ____| | / /____/ /_ - / / / / __ \/ _ \/ __ \ | /| / / ___/ __/ -/ /_/ / /_/ / __/ / / / |/ |/ / / / /_ -\____/ .___/\___/_/ /_/|__/|__/_/ \__/ - /_/ W I R E L E S S F R E E D O M - -EOF -} -header_info -echo -e "\n Loading..." -RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" -METHOD="" -NSAPP="openwrt-vm" -var_os="openwrt" -var_version=" " -DISK_SIZE="1G" -GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//') -GEN_MAC_LAN=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//') - -YW=$(echo "\033[33m") -BL=$(echo "\033[36m") -HA=$(echo "\033[1;34m") -RD=$(echo "\033[01;31m") -BGN=$(echo "\033[4;92m") -GN=$(echo "\033[1;92m") -DGN=$(echo "\033[32m") -CL=$(echo "\033[m") - -BOLD=$(echo "\033[1m") -BFR="\\r\\033[K" -HOLD=" " -TAB=" " - -CM="${TAB}✔️${TAB}${CL}" -CROSS="${TAB}✖️${TAB}${CL}" -INFO="${TAB}💡${TAB}${CL}" -OS="${TAB}🖥️${TAB}${CL}" -CONTAINERTYPE="${TAB}📦${TAB}${CL}" -DISKSIZE="${TAB}💾${TAB}${CL}" -CPUCORE="${TAB}🧠${TAB}${CL}" -RAMSIZE="${TAB}🛠️${TAB}${CL}" -CONTAINERID="${TAB}🆔${TAB}${CL}" -HOSTNAME="${TAB}🏠${TAB}${CL}" -BRIDGE="${TAB}🌉${TAB}${CL}" -GATEWAY="${TAB}🌐${TAB}${CL}" -DEFAULT="${TAB}⚙️${TAB}${CL}" -MACADDRESS="${TAB}🔗${TAB}${CL}" -VLANTAG="${TAB}🏷️${TAB}${CL}" -CREATING="${TAB}🚀${TAB}${CL}" -ADVANCED="${TAB}🧩${TAB}${CL}" -CLOUD="${TAB}☁️${TAB}${CL}" - -set -Eeo pipefail -trap 'error_handler $LINENO "$BASH_COMMAND"' ERR -trap cleanup EXIT -trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT -trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM -function error_handler() { - local exit_code="$?" - local line_number="$1" - local command="$2" - post_update_to_api "failed" "$command" - local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" - echo -e "\n$error_message\n" - cleanup_vmid -} - -function get_valid_nextid() { - local try_id - try_id=$(pvesh get /cluster/nextid) - while true; do - if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then - try_id=$((try_id + 1)) - continue - fi - if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then - try_id=$((try_id + 1)) - continue - fi - break - done - echo "$try_id" -} - -function cleanup_vmid() { - if qm status $VMID &>/dev/null; then - qm stop $VMID &>/dev/null - qm destroy $VMID &>/dev/null - fi -} - -function cleanup() { - popd >/dev/null - rm -rf $TEMP_DIR -} - -TEMP_DIR=$(mktemp -d) -pushd $TEMP_DIR >/dev/null -function send_line_to_vm() { - echo -e "${DGN}Sending line: ${YW}$1${CL}" - for ((i = 0; i < ${#1}; i++)); do - character=${1:i:1} - case $character in - " ") character="spc" ;; - "-") character="minus" ;; - "=") character="equal" ;; - ",") character="comma" ;; - ".") character="dot" ;; - "/") character="slash" ;; - "'") character="apostrophe" ;; - ";") character="semicolon" ;; - '\') character="backslash" ;; - '`') character="grave_accent" ;; - "[") character="bracket_left" ;; - "]") character="bracket_right" ;; - "_") character="shift-minus" ;; - "+") character="shift-equal" ;; - "?") character="shift-slash" ;; - "<") character="shift-comma" ;; - ">") character="shift-dot" ;; - '"') character="shift-apostrophe" ;; - ":") character="shift-semicolon" ;; - "|") character="shift-backslash" ;; - "~") character="shift-grave_accent" ;; - "{") character="shift-bracket_left" ;; - "}") character="shift-bracket_right" ;; - "A") character="shift-a" ;; - "B") character="shift-b" ;; - "C") character="shift-c" ;; - "D") character="shift-d" ;; - "E") character="shift-e" ;; - "F") character="shift-f" ;; - "G") character="shift-g" ;; - "H") character="shift-h" ;; - "I") character="shift-i" ;; - "J") character="shift-j" ;; - "K") character="shift-k" ;; - "L") character="shift-l" ;; - "M") character="shift-m" ;; - "N") character="shift-n" ;; - "O") character="shift-o" ;; - "P") character="shift-p" ;; - "Q") character="shift-q" ;; - "R") character="shift-r" ;; - "S") character="shift-s" ;; - "T") character="shift-t" ;; - "U") character="shift-u" ;; - "V") character="shift-v" ;; - "W") character="shift-w" ;; - "X") character="shift=x" ;; - "Y") character="shift-y" ;; - "Z") character="shift-z" ;; - "!") character="shift-1" ;; - "@") character="shift-2" ;; - "#") character="shift-3" ;; - '$') character="shift-4" ;; - "%") character="shift-5" ;; - "^") character="shift-6" ;; - "&") character="shift-7" ;; - "*") character="shift-8" ;; - "(") character="shift-9" ;; - ")") character="shift-0" ;; - esac - qm sendkey $VMID "$character" - done - qm sendkey $VMID ret -} - -TEMP_DIR=$(mktemp -d) -pushd $TEMP_DIR >/dev/null - -if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "OpenWrt VM" --yesno "This will create a New OpenWrt VM. Proceed?" 10 58); then - : -else - header_info && echo -e "⚠ User exited script \n" && exit -fi - -function msg_info() { - local msg="$1" - echo -ne " ${HOLD} ${YW}${msg}..." -} - -function msg_ok() { - local msg="$1" - echo -e "${BFR} ${CM} ${GN}${msg}${CL}" -} - -function msg_error() { - local msg="$1" - echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}" -} - -# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported. -# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 (NOT 9.1+) -pve_check() { - local PVE_VER - PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" - - # Check for Proxmox VE 8.x: allow 8.0–8.9 - if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then - local MINOR="${BASH_REMATCH[1]}" - if ((MINOR < 0 || MINOR > 9)); then - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported: Proxmox VE version 8.0 – 8.9" - exit 1 - fi - return 0 - fi - - # Check for Proxmox VE 9.x: allow ONLY 9.0 - if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then - local MINOR="${BASH_REMATCH[1]}" - if ((MINOR != 0)); then - msg_error "This version of Proxmox VE is not yet supported." - msg_error "Supported: Proxmox VE version 9.0" - exit 1 - fi - return 0 - fi - - # All other unsupported versions - msg_error "This version of Proxmox VE is not supported." - msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0" - exit 1 -} - -function arch_check() { - if [ "$(dpkg --print-architecture)" != "amd64" ]; then - echo -e "\n ${CROSS} This script will not work with PiMox! \n" - echo -e "Exiting..." - sleep 2 - exit - fi -} - -function ssh_check() { - if command -v pveversion >/dev/null 2>&1; then - if [ -n "${SSH_CLIENT:+x}" ]; then - if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then - echo "you've been warned" - else - clear - exit - fi - fi - fi -} - -function exit-script() { - clear - echo -e "⚠ User exited script \n" - exit -} - -function default_settings() { - VMID=$(get_valid_nextid) - HN="openwrt" - CORE_COUNT="1" - RAM_SIZE="256" - BRG="vmbr0" - LAN_BRG="vmbr0" - MAC=$GEN_MAC - LAN_MAC=$GEN_MAC_LAN - VLAN="" - LAN_VLAN=",tag=999" - LAN_IP_ADDR="192.168.1.1" - LAN_NETMASK="255.255.255.0" - MTU="" - START_VM="yes" - METHOD="default" - DISK_SIZE="1G" - echo -e "${CONTAINERID}${BOLD}${DGN}VMID: ${BGN}${VMID}${CL}" - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM: ${BGN}${RAM_SIZE}${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}" - echo -e "${BRIDGE}${BOLD}${DGN}WAN Bridge: ${BGN}${BRG}${CL}" - echo -e "${BRIDGE}${BOLD}${DGN}LAN Bridge: ${BGN}${LAN_BRG}${CL}" - echo -e "${MACADDRESS}${BOLD}${DGN}WAN MAC: ${BGN}${MAC}${CL}" - echo -e "${MACADDRESS}${BOLD}${DGN}LAN MAC: ${BGN}${LAN_MAC}${CL}" -} - -function advanced_settings() { - METHOD="advanced" - [ -z "${VMID:-}" ] && VMID=$(get_valid_nextid) - while true; do - if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z "$VMID" ]; then - VMID=$(get_valid_nextid) - fi - if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then - echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" - sleep 2 - continue - fi - echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" - break - else - exit-script - fi - done - - if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 openwrt --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $VM_NAME ]; then - HN="openwrt" - else - HN=$(echo ${VM_NAME,,} | tr -d ' ') - fi - echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" - else - exit-script - fi - - if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 1 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $CORE_COUNT ]; then - CORE_COUNT="1" - fi - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" - else - exit-script - fi - - if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 256 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $RAM_SIZE ]; then - RAM_SIZE="256" - fi - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" - else - exit-script - fi - - if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Set Disk Size in GiB (e.g., 1, 2, 4)" 8 58 "1" \ - --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then - DISK_SIZE="${DISK_SIZE}G" - fi - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}" - else - exit-script - fi - - if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Bridge" 8 58 vmbr0 --title "WAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $BRG ]; then - BRG="vmbr0" - fi - echo -e "${DGN}Using WAN Bridge: ${BGN}$BRG${CL}" - else - exit-script - fi - - if LAN_BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Bridge" 8 58 vmbr0 --title "LAN BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $LAN_BRG ]; then - LAN_BRG="vmbr0" - fi - echo -e "${DGN}Using LAN Bridge: ${BGN}$LAN_BRG${CL}" - else - exit-script - fi - - if LAN_IP_ADDR=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a router IP" 8 58 $LAN_IP_ADDR --title "LAN IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $LAN_IP_ADDR ]; then - LAN_IP_ADDR="192.168.1.1" - fi - echo -e "${DGN}Using LAN IP ADDRESS: ${BGN}$LAN_IP_ADDR${CL}" - else - exit-script - fi - - if LAN_NETMASK=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a router netmask" 8 58 $LAN_NETMASK --title "LAN NETMASK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $LAN_NETMASK ]; then - LAN_NETMASK="255.255.255.0" - fi - echo -e "${DGN}Using LAN NETMASK: ${BGN}$LAN_NETMASK${CL}" - else - exit-script - fi - - if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN MAC Address" 8 58 $GEN_MAC --title "WAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $MAC1 ]; then - MAC="$GEN_MAC" - else - MAC="$MAC1" - fi - echo -e "${DGN}Using WAN MAC Address: ${BGN}$MAC${CL}" - else - exit-script - fi - - if MAC2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN MAC Address" 8 58 $GEN_MAC_LAN --title "LAN MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $MAC2 ]; then - LAN_MAC="$GEN_MAC_LAN" - else - LAN_MAC="$MAC2" - fi - echo -e "${DGN}Using LAN MAC Address: ${BGN}$LAN_MAC${CL}" - else - exit-script - fi - - if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a WAN Vlan (leave blank for default)" 8 58 --title "WAN VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $VLAN1 ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${DGN}Using WAN Vlan: ${BGN}$VLAN1${CL}" - else - exit-script - fi - - if VLAN2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a LAN Vlan" 8 58 999 --title "LAN VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $VLAN2 ]; then - VLAN2="999" - LAN_VLAN=",tag=$VLAN2" - else - LAN_VLAN=",tag=$VLAN2" - fi - echo -e "${DGN}Using LAN Vlan: ${BGN}$VLAN2${CL}" - else - exit-script - fi - - if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then - if [ -z $MTU1 ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit-script - fi - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then - START_VM="yes" - else - START_VM="no" - fi - echo -e "${DGN}Start VM when completed: ${BGN}$START_VM${CL}" - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create OpenWrt VM?" --no-button Do-Over 10 58); then - echo -e "${RD}Creating a OpenWrt VM using the above advanced settings${CL}" - else - header_info - echo -e "${RD}Using Advanced Settings${CL}" - advanced_settings - fi -} - -function start_script() { - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then - header_info - echo -e "${BL}Using Default Settings${CL}" - default_settings - else - header_info - echo -e "${RD}Using Advanced Settings${CL}" - advanced_settings - fi -} - -arch_check -pve_check -ssh_check -start_script -post_to_api_vm - -msg_info "Validating Storage" -while read -r line; do - TAG=$(echo $line | awk '{print $1}') - TYPE=$(echo $line | awk '{printf "%-10s", $2}') - FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}') - ITEM=" Type: $TYPE Free: $FREE " - OFFSET=2 - if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then - MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET)) - fi - STORAGE_MENU+=("$TAG" "$ITEM" "OFF") -done < <(pvesm status -content images | awk 'NR>1') -VALID=$(pvesm status -content images | awk 'NR>1') -if [ -z "$VALID" ]; then - echo -e "\n${RD}⚠ Unable to detect a valid storage location.${CL}" - echo -e "Exiting..." - exit -elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then - STORAGE=${STORAGE_MENU[0]} -else - while [ -z "${STORAGE:+x}" ]; do - STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \ - "Which storage pool would you like to use for the OpenWrt VM?\n\n" \ - 16 $(($MSG_MAX_LENGTH + 23)) 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) - done -fi -msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." -msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}." -msg_info "Getting URL for OpenWrt Disk Image" - -response=$(curl -fsSL https://openwrt.org) -stableversion=$(echo "$response" | sed -n 's/.*Current stable release - OpenWrt \([0-9.]\+\).*/\1/p' | head -n 1) -URL="https://downloads.openwrt.org/releases/$stableversion/targets/x86/64/openwrt-$stableversion-x86-64-generic-ext4-combined.img.gz" - -msg_ok "${CL}${BL}${URL}${CL}" -curl -f#SL -o "$(basename "$URL")" "$URL" -FILE=$(basename "$URL") -msg_ok "Downloaded ${CL}${BL}$FILE${CL}" - -gunzip -f "$FILE" >/dev/null 2>&1 || true -FILE="${FILE%.*}" -msg_ok "Extracted OpenWrt Disk Image ${CL}${BL}$FILE${CL}" - -msg_info "Creating OpenWrt VM" -qm create "$VMID" -cores "$CORE_COUNT" -memory "$RAM_SIZE" -name "$HN" \ - -onboot 1 -ostype l26 -scsihw virtio-scsi-pci --tablet 0 -if [[ "$(pvesm status | awk -v s=$STORAGE '$1==s {print $2}')" == "dir" ]]; then - qm set "$VMID" -efidisk0 "${STORAGE}:0,efitype=4m,size=4M" -else - pvesm alloc "$STORAGE" "$VMID" "vm-$VMID-disk-0" 4M >/dev/null - qm set "$VMID" -efidisk0 "${STORAGE}:vm-$VMID-disk-0,efitype=4m,size=4M" -fi - -IMPORT_OUT="$(qm importdisk "$VMID" "$FILE" "$STORAGE" --format raw 2>&1 || true)" -DISK_REF="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p")" - -if [[ -z "$DISK_REF" ]]; then - DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$1 ~ ("vm-"id"-disk-") {print $1}' | sort | tail -n1)" -fi - -if [[ -z "$DISK_REF" ]]; then - msg_error "Unable to determine imported disk reference." - echo "$IMPORT_OUT" - exit 1 -fi - -qm set "$VMID" \ - -efidisk0 "${STORAGE}:0,efitype=4m,size=4M" \ - -scsi0 "${DISK_REF},size=${DISK_SIZE}" \ - -boot order=scsi0 \ - -tags community-script >/dev/null -msg_ok "Attached disk (${DISK_SIZE})" - -DESCRIPTION=$( - cat < - - Logo - - -

OpenWrt VM

- -

- - spend Coffee - -

- - - - GitHub - - - - Discussions - - - - Issues - - -EOF -) -qm set "$VMID" -description "$DESCRIPTION" >/dev/null - -msg_ok "Created OpenWrt VM ${CL}${BL}(${HN})" -msg_info "OpenWrt is being started in order to configure the network interfaces." -qm start $VMID -sleep 15 -msg_info "Waiting for OpenWrt to boot..." -for i in {1..30}; do - if qm status "$VMID" | grep -q "running"; then - sleep 5 - msg_ok "OpenWrt is running" - break - fi - sleep 1 -done - -msg_ok "Network interfaces are being configured as OpenWrt initiates." - -if qm status "$VMID" | grep -q "running"; then - send_line_to_vm "" - send_line_to_vm "uci delete network.@device[0]" - send_line_to_vm "uci set network.wan=interface" - send_line_to_vm "uci set network.wan.device=eth1" - send_line_to_vm "uci set network.wan.proto=dhcp" - send_line_to_vm "uci delete network.lan" - send_line_to_vm "uci set network.lan=interface" - send_line_to_vm "uci set network.lan.device=eth0" - send_line_to_vm "uci set network.lan.proto=static" - send_line_to_vm "uci set network.lan.ipaddr=${LAN_IP_ADDR}" - send_line_to_vm "uci set network.lan.netmask=${LAN_NETMASK}" - send_line_to_vm "uci commit" - send_line_to_vm "halt" - msg_ok "Network interfaces configured in OpenWrt" -else - msg_error "VM is not running" - exit 1 -fi - -msg_info "Waiting for OpenWrt to shut down..." -until qm status "$VMID" | grep -q "stopped"; do - sleep 2 -done -msg_ok "OpenWrt has shut down" - -msg_info "Adding bridge interfaces on Proxmox side" -qm set "$VMID" \ - -net0 virtio,bridge="${LAN_BRG}",macaddr="${LAN_MAC}${LAN_VLAN}${MTU}" \ - -net1 virtio,bridge="${BRG}",macaddr="${MAC}${VLAN}${MTU}" >/dev/null -msg_ok "Bridge interfaces added" - -if [ "$START_VM" = "yes" ]; then - msg_info "Starting OpenWrt VM" - qm start "$VMID" - msg_ok "Started OpenWrt VM" -fi - -VLAN_FINISH="" -if [ -z "$VLAN" ] && [ "$VLAN2" != "999" ]; then - VLAN_FINISH=" Please remember to adjust the VLAN tags to suit your network." -fi -post_update_to_api "done" "none" -msg_ok "Completed Successfully!${VLAN_FINISH:+\n$VLAN_FINISH}" diff --git a/server.js b/server.js index 4daf897..ab89406 100644 --- a/server.js +++ b/server.js @@ -51,6 +51,7 @@ const handle = app.getRequestHandler(); * @property {string} [mode] * @property {ServerInfo} [server] * @property {boolean} [isUpdate] + * @property {boolean} [isShell] * @property {string} [containerId] */ @@ -207,13 +208,15 @@ class ScriptExecutionHandler { * @param {WebSocketMessage} message */ async handleMessage(ws, message) { - const { action, scriptPath, executionId, input, mode, server, isUpdate, containerId } = message; + const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, containerId } = message; switch (action) { case 'start': if (scriptPath && executionId) { if (isUpdate && containerId) { await this.startUpdateExecution(ws, containerId, executionId, mode, server); + } else if (isShell && containerId) { + await this.startShellExecution(ws, containerId, executionId, mode, server); } else { await this.startScriptExecution(ws, scriptPath, executionId, mode, server); } @@ -709,6 +712,145 @@ class ScriptExecutionHandler { }); } } + + /** + * Start shell execution + * @param {ExtendedWebSocket} ws + * @param {string} containerId + * @param {string} executionId + * @param {string} mode + * @param {ServerInfo|null} server + */ + async startShellExecution(ws, containerId, executionId, mode = 'local', server = null) { + try { + + // Send start message + this.sendMessage(ws, { + type: 'start', + data: `Starting shell session for container ${containerId}...`, + timestamp: Date.now() + }); + + if (mode === 'ssh' && server) { + await this.startSSHShellExecution(ws, containerId, executionId, server); + } else { + await this.startLocalShellExecution(ws, containerId, executionId); + } + + } catch (error) { + this.sendMessage(ws, { + type: 'error', + data: `Failed to start shell: ${error instanceof Error ? error.message : String(error)}`, + timestamp: Date.now() + }); + } + } + + /** + * Start local shell execution + * @param {ExtendedWebSocket} ws + * @param {string} containerId + * @param {string} executionId + */ + async startLocalShellExecution(ws, containerId, executionId) { + const { spawn } = await import('node-pty'); + + // Create a shell process that will run pct enter + const childProcess = spawn('bash', ['-c', `pct enter ${containerId}`], { + name: 'xterm-color', + cols: 80, + rows: 24, + cwd: process.cwd(), + env: process.env + }); + + // Store the execution + this.activeExecutions.set(executionId, { + process: childProcess, + ws + }); + + // Handle pty data + childProcess.onData((data) => { + this.sendMessage(ws, { + type: 'output', + data: data.toString(), + timestamp: Date.now() + }); + }); + + // Note: No automatic command is sent - user can type commands interactively + + // Handle process exit + childProcess.onExit((e) => { + this.sendMessage(ws, { + type: 'end', + data: `Shell session ended with exit code: ${e.exitCode}`, + timestamp: Date.now() + }); + + this.activeExecutions.delete(executionId); + }); + } + + /** + * Start SSH shell execution + * @param {ExtendedWebSocket} ws + * @param {string} containerId + * @param {string} executionId + * @param {ServerInfo} server + */ + async startSSHShellExecution(ws, containerId, executionId, server) { + const sshService = getSSHExecutionService(); + + try { + const execution = await sshService.executeCommand( + server, + `pct enter ${containerId}`, + /** @param {string} data */ + (data) => { + this.sendMessage(ws, { + type: 'output', + data: data, + timestamp: Date.now() + }); + }, + /** @param {string} error */ + (error) => { + this.sendMessage(ws, { + type: 'error', + data: error, + timestamp: Date.now() + }); + }, + /** @param {number} code */ + (code) => { + this.sendMessage(ws, { + type: 'end', + data: `Shell session ended with exit code: ${code}`, + timestamp: Date.now() + }); + + this.activeExecutions.delete(executionId); + } + ); + + // Store the execution + this.activeExecutions.set(executionId, { + process: /** @type {any} */ (execution).process, + ws + }); + + // Note: No automatic command is sent - user can type commands interactively + + } catch (error) { + this.sendMessage(ws, { + type: 'error', + data: `SSH shell execution failed: ${error instanceof Error ? error.message : String(error)}`, + timestamp: Date.now() + }); + } + } } // TerminalHandler removed - not used by current application diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index 6387e3d..12f1477 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -20,6 +20,10 @@ interface InstalledScript { server_ip: string | null; server_user: string | null; server_password: string | null; + server_auth_type: string | null; + server_ssh_key: string | null; + server_ssh_key_passphrase: string | null; + server_ssh_port: number | null; server_color: string | null; installation_date: string; status: 'in_progress' | 'success' | 'failed'; @@ -35,6 +39,7 @@ export function InstalledScriptsTab() { const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('server_name'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null); + const [openingShell, setOpeningShell] = useState<{ id: number; containerId: string; server?: any } | null>(null); const [editingScriptId, setEditingScriptId] = useState(null); const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' }); const [showAddForm, setShowAddForm] = useState(false); @@ -340,7 +345,7 @@ export function InstalledScriptsTab() { containerStatusMutation.mutate({ serverIds }); } }, 500); - }, []); // Remove containerStatusMutation from dependencies to prevent loops + }, [containerStatusMutation]); // Run cleanup when component mounts and scripts are loaded (only once) useEffect(() => { @@ -356,7 +361,7 @@ export function InstalledScriptsTab() { console.log('Status check triggered - scripts length:', scripts.length); fetchContainerStatuses(); } - }, [scripts.length]); // Remove fetchContainerStatuses from dependencies + }, [scripts.length, fetchContainerStatuses]); // Cleanup timeout on unmount useEffect(() => { @@ -526,13 +531,17 @@ export function InstalledScriptsTab() { onConfirm: () => { // Get server info if it's SSH mode let server = null; - if (script.server_id && script.server_user && script.server_password) { + if (script.server_id && script.server_user) { server = { id: script.server_id, name: script.server_name, ip: script.server_ip, user: script.server_user, - password: script.server_password + password: script.server_password, + auth_type: script.server_auth_type ?? 'password', + ssh_key: script.server_ssh_key, + ssh_key_passphrase: script.server_ssh_key_passphrase, + ssh_port: script.server_ssh_port ?? 22 }; } @@ -550,6 +559,91 @@ export function InstalledScriptsTab() { setUpdatingScript(null); }; + const handleOpenShell = (script: InstalledScript) => { + if (!script.container_id) { + setErrorModal({ + isOpen: true, + title: 'Shell Access Failed', + message: 'No Container ID available for this script', + details: 'This script does not have a valid container ID and cannot be accessed via shell.' + }); + return; + } + + // Get server info if it's SSH mode + let server = null; + if (script.server_id && script.server_user) { + server = { + id: script.server_id, + name: script.server_name, + ip: script.server_ip, + user: script.server_user, + password: script.server_password, + auth_type: script.server_auth_type ?? 'password', + ssh_key: script.server_ssh_key, + ssh_key_passphrase: script.server_ssh_key_passphrase, + ssh_port: script.server_ssh_port ?? 22 + }; + } + + setOpeningShell({ + id: script.id, + containerId: script.container_id, + server: server + }); + }; + + const handleCloseShellTerminal = () => { + setOpeningShell(null); + }; + + // Auto-scroll to terminals when they open + useEffect(() => { + if (openingShell) { + // Small delay to ensure the terminal is rendered + setTimeout(() => { + const terminalElement = document.querySelector('[data-terminal="shell"]'); + if (terminalElement) { + // Scroll to the terminal with smooth animation + terminalElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest' + }); + + // Add a subtle highlight effect + terminalElement.classList.add('animate-pulse'); + setTimeout(() => { + terminalElement.classList.remove('animate-pulse'); + }, 2000); + } + }, 200); + } + }, [openingShell]); + + useEffect(() => { + if (updatingScript) { + // Small delay to ensure the terminal is rendered + setTimeout(() => { + const terminalElement = document.querySelector('[data-terminal="update"]'); + if (terminalElement) { + // Scroll to the terminal with smooth animation + terminalElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest' + }); + + // Add a subtle highlight effect + terminalElement.classList.add('animate-pulse'); + setTimeout(() => { + terminalElement.classList.remove('animate-pulse'); + }, 2000); + } + }, 200); + } + }, [updatingScript]); + const handleEditScript = (script: InstalledScript) => { setEditingScriptId(script.id); setEditFormData({ @@ -662,7 +756,7 @@ export function InstalledScriptsTab() {
{/* Update Terminal */} {updatingScript && ( -
+
)} + {/* Shell Terminal */} + {openingShell && ( +
+ +
+ )} + {/* Header with Stats */}

Installed Scripts

@@ -995,6 +1103,7 @@ export function InstalledScriptsTab() { onSave={handleSaveEdit} onCancel={handleCancelEdit} onUpdate={() => handleUpdateScript(script)} + onShell={() => handleOpenShell(script)} onDelete={() => handleDeleteScript(Number(script.id))} isUpdating={updateScriptMutation.isPending} isDeleting={deleteScriptMutation.isPending} @@ -1203,6 +1312,17 @@ export function InstalledScriptsTab() { Update )} + {/* Shell button - only show for SSH scripts with container_id */} + {script.container_id && script.execution_mode === 'ssh' && ( + + )} {/* Container Control Buttons - only show for SSH scripts with container_id */} {script.container_id && script.execution_mode === 'ssh' && ( <> diff --git a/src/app/_components/SSHKeyInput.tsx b/src/app/_components/SSHKeyInput.tsx index bd2f74a..93bd595 100644 --- a/src/app/_components/SSHKeyInput.tsx +++ b/src/app/_components/SSHKeyInput.tsx @@ -104,9 +104,6 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK keyType = 'ECDSA'; } else if (keyLine.includes('OPENSSH PRIVATE KEY')) { // For OpenSSH format keys, try to detect type from the key content - // Look for common patterns in the base64 content - const base64Content = keyContent.replace(/-----BEGIN.*?-----/, '').replace(/-----END.*?-----/, '').replace(/\s/g, ''); - // This is a heuristic - OpenSSH ED25519 keys typically start with specific patterns // We'll default to "OpenSSH" for now since we can't reliably detect the type keyType = 'OpenSSH'; diff --git a/src/app/_components/ScriptInstallationCard.tsx b/src/app/_components/ScriptInstallationCard.tsx index 8c49e94..37350d5 100644 --- a/src/app/_components/ScriptInstallationCard.tsx +++ b/src/app/_components/ScriptInstallationCard.tsx @@ -14,6 +14,10 @@ interface InstalledScript { server_ip: string | null; server_user: string | null; server_password: string | null; + server_auth_type: string | null; + server_ssh_key: string | null; + server_ssh_key_passphrase: string | null; + server_ssh_port: number | null; server_color: string | null; installation_date: string; status: 'in_progress' | 'success' | 'failed'; @@ -31,6 +35,7 @@ interface ScriptInstallationCardProps { onSave: () => void; onCancel: () => void; onUpdate: () => void; + onShell: () => void; onDelete: () => void; isUpdating: boolean; isDeleting: boolean; @@ -50,6 +55,7 @@ export function ScriptInstallationCard({ onSave, onCancel, onUpdate, + onShell, onDelete, isUpdating, isDeleting, @@ -203,6 +209,18 @@ export function ScriptInstallationCard({ Update )} + {/* Shell button - only show for SSH scripts with container_id */} + {script.container_id && script.execution_mode === 'ssh' && ( + + )} {/* Container Control Buttons - only show for SSH scripts with container_id */} {script.container_id && script.execution_mode === 'ssh' && ( <> diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index db1d855..b18e108 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -11,6 +11,7 @@ interface TerminalProps { mode?: 'local' | 'ssh'; server?: any; isUpdate?: boolean; + isShell?: boolean; containerId?: string; } @@ -20,7 +21,7 @@ interface TerminalMessage { timestamp: number; } -export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, containerId }: TerminalProps) { +export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, isShell = false, containerId }: TerminalProps) { const [isConnected, setIsConnected] = useState(false); const [isRunning, setIsRunning] = useState(false); const [isClient, setIsClient] = useState(false); @@ -332,6 +333,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate mode, server, isUpdate, + isShell, containerId }; ws.send(JSON.stringify(message)); @@ -372,7 +374,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate wsRef.current.close(); } }; - }, [scriptPath, mode, server, isUpdate, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps + }, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps const startScript = () => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) { @@ -388,6 +390,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate mode, server, isUpdate, + isShell, containerId })); } diff --git a/src/server/database.js b/src/server/database.js index cf100d2..bca0659 100644 --- a/src/server/database.js +++ b/src/server/database.js @@ -180,6 +180,10 @@ class DatabaseService { s.ip as server_ip, s.user as server_user, s.password as server_password, + s.auth_type as server_auth_type, + s.ssh_key as server_ssh_key, + s.ssh_key_passphrase as server_ssh_key_passphrase, + s.ssh_port as server_ssh_port, s.color as server_color FROM installed_scripts inst LEFT JOIN servers s ON inst.server_id = s.id