#!/usr/bin/env bash
# vjunos-evo-lab.sh - Spin up a 3-router vJunosEvolved lab on KVM
#
# Usage: sudo ./vjunos-evo-lab.sh {setup|start|stop|destroy|status|console N}
#
# vJunosEvolved is a single (non-nested) VM running Junos OS Evolved as a
# virtualized PTX10001-36MR. Works fine on AMD because there's no inner KVM.
#
# First time:
#   sudo ./vjunos-evo-lab.sh setup    (one-time: bridges, disks, configs)
#   sudo ./vjunos-evo-lab.sh start    (boot all 3 routers)
#   sudo ./vjunos-evo-lab.sh console 1   (connect to rt1)
#
# Subsequent times: just 'start' / 'stop'.
#
# Topology: triangle
#   rt1:et-0/0/0  <->  rt2:et-0/0/0   (vjlink-12)
#   rt2:et-0/0/1  <->  rt3:et-0/0/0   (vjlink-23)
#   rt1:et-0/0/1  <->  rt3:et-0/0/1   (vjlink-13)
#   All re0:mgmt-0 share vjevo-mgmt bridge.

set -euo pipefail

# =================== CONFIG ===================
LAB_DIR="${LAB_DIR:-/var/lib/libvirt/images/vjunos-evo-lab}"
BASE_IMAGE="${BASE_IMAGE:-/home/jack/Downloads/ISO/Juniper/vJunosEvolved-25.4R1.13-EVO.qcow2}"
NUM_ROUTERS=3
RAM_MB=8192             # vJunosEvolved minimum
VCPUS=4
MGMT_BRIDGE="vjevo-mgmt"
CONSOLE_BASE_PORT=8700  # different from vjunos-switch ports to avoid collisions
ROOT_PASSWORD="Juniper123"

# Inter-router links: "rtA:portA rtB:portB bridge-name"
# port numbers are 0-indexed and map to et-0/0/<n>
LINKS=(
    "1:0 2:0 vjlink-12"
    "2:1 3:0 vjlink-23"
    "1:1 3:1 vjlink-13"
)
# ==============================================

usage() {
    cat <<EOF
Usage: sudo $0 {setup|start|stop|destroy|status|console N}

  setup       Create bridges, copy disks, generate configs, define VMs
  start       Start all routers (idempotent)
  stop        Gracefully shut down all routers
  destroy     Stop, undefine, and delete ALL lab files (asks for confirm)
  status      Show running state, console ports, topology
  console N   Telnet to router N's console (Ctrl-] then 'quit' to exit)

Base image: $BASE_IMAGE
Lab dir:    $LAB_DIR
EOF
}

log() { echo "[$(date +%H:%M:%S)] $*"; }
err() { echo "ERROR: $*" >&2; exit 1; }

require_root() { [[ $EUID -eq 0 ]] || err "needs root: sudo $0 $*"; }

require_tools() {
    for t in virsh qemu-img mkfs.vfat mcopy mmd ip telnet; do
        command -v "$t" >/dev/null || err "missing tool: $t"
    done
    # vJunosEvolved needs UEFI firmware (OVMF) - it has a GPT/EFI partition layout
    if ! ls /usr/share/edk2/ovmf/*.fd /usr/share/OVMF/*.fd 2>/dev/null | head -1 >/dev/null; then
        err "OVMF firmware not found. Install with: sudo dnf install edk2-ovmf"
    fi
}

detect_libvirt_user() {
    if id libvirt-qemu &>/dev/null; then
        LIBVIRT_USER="libvirt-qemu"; LIBVIRT_GROUP="kvm"
    elif id qemu &>/dev/null; then
        LIBVIRT_USER="qemu"; LIBVIRT_GROUP="qemu"
    else
        LIBVIRT_USER="root"; LIBVIRT_GROUP="root"
    fi
}

# --------------- BRIDGES ---------------
create_bridge() {
    local name=$1
    if ! ip link show "$name" &>/dev/null; then
        ip link add "$name" type bridge
        log "  created bridge: $name"
    fi
    ip link set "$name" up
    echo 0 > "/sys/class/net/$name/bridge/stp_state" 2>/dev/null || true
    echo 0x4000 > "/sys/class/net/$name/bridge/group_fwd_mask" 2>/dev/null || true
}

delete_bridge() {
    local name=$1
    if ip link show "$name" &>/dev/null; then
        ip link set "$name" down
        ip link delete "$name"
        log "  deleted bridge: $name"
    fi
}

# --------------- CONFIG DRIVE ---------------
# vJunosEvolved reads /config/juniper.conf from a FAT-formatted USB disk (raw img).
make_config_drive() {
    local rt_num=$1
    local outfile=$2
    local hostname="rt${rt_num}"
    local loopback_ip="192.168.255.${rt_num}"

    local tmpconf
    tmpconf=$(mktemp)
    cat > "$tmpconf" <<EOF
system {
    host-name ${hostname};
    root-authentication {
        plain-text-password-value "${ROOT_PASSWORD}";
    }
    login {
        user admin {
            class super-user;
            authentication {
                plain-text-password-value "${ROOT_PASSWORD}";
            }
        }
    }
    services {
        ssh {
            root-login allow;
        }
        netconf {
            ssh;
        }
    }
    syslog {
        user * { any emergency; }
        file messages { any notice; authorization info; }
        file interactive-commands { interactive-commands any; }
    }
}
interfaces {
    re0:mgmt-0 {
        unit 0 {
            family inet {
                dhcp;
            }
        }
    }
    lo0 {
        unit 0 {
            family inet {
                address ${loopback_ip}/32;
            }
        }
    }
}
EOF

    # vJunosEvolved expects raw format (not qcow2) for the config disk
    truncate -s 16M "$outfile"
    mkfs.vfat -n vmm-data "$outfile" >/dev/null
    mmd -i "$outfile" ::/config
    mcopy -i "$outfile" "$tmpconf" ::/config/juniper.conf
    rm -f "$tmpconf"
    log "  config drive: $outfile (hostname=$hostname, lo0=$loopback_ip/32, root pw=$ROOT_PASSWORD)"
}

# --------------- LIBVIRT XML ---------------
generate_xml() {
    local rt_num=$1
    local console_port=$((CONSOLE_BASE_PORT + rt_num))
    local disk="${LAB_DIR}/rt${rt_num}-disk.qcow2"
    local config="${LAB_DIR}/rt${rt_num}-config.img"
    local xml="${LAB_DIR}/rt${rt_num}.xml"

    declare -A port_to_bridge=()
    for link in "${LINKS[@]}"; do
        read -r endpointA endpointB bridge <<< "$link"
        local rtA="${endpointA%:*}" portA="${endpointA#*:}"
        local rtB="${endpointB%:*}" portB="${endpointB#*:}"
        [[ "$rtA" == "$rt_num" ]] && port_to_bridge[$portA]=$bridge
        [[ "$rtB" == "$rt_num" ]] && port_to_bridge[$portB]=$bridge
    done

    # First NIC = re0:mgmt-0, then et-0/0/0, et-0/0/1, ... in port order
    local interfaces=""
    interfaces+="
    <interface type='bridge'>
      <source bridge='${MGMT_BRIDGE}'/>
      <mac address='52:54:00:0e:00:0${rt_num}'/>
      <model type='virtio'/>
    </interface>"

    for port in $(printf "%s\n" "${!port_to_bridge[@]}" | sort -n); do
        local br="${port_to_bridge[$port]}"
        interfaces+="
    <interface type='bridge'>
      <source bridge='${br}'/>
      <mac address='52:54:00:7e:0${rt_num}:0${port}'/>
      <model type='virtio'/>
    </interface>"
    done

    cat > "$xml" <<EOF
<domain type='kvm'>
  <name>vjunos-rt${rt_num}</name>
  <memory unit='MiB'>${RAM_MB}</memory>
  <currentMemory unit='MiB'>${RAM_MB}</currentMemory>
  <vcpu>${VCPUS}</vcpu>
  <cpu mode='host-passthrough' check='none'>
    <topology sockets='1' cores='${VCPUS}' threads='1'/>
  </cpu>
  <os firmware='efi'>
    <type arch='x86_64' machine='pc'>hvm</type>
    <loader secure='no'/>
    <boot dev='hd'/>
  </os>
  <features>
    <acpi/>
    <apic/>
  </features>
  <clock offset='utc'/>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>destroy</on_reboot>
  <on_crash>preserve</on_crash>
  <devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <video>
      <model type='vga' vram='16384' heads='1' primary='yes'/>
    </video>
    <graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1'/>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' cache='writeback'/>
      <source file='${disk}'/>
      <target dev='vda' bus='virtio'/>
    </disk>
    <disk type='file' device='disk'>
      <driver name='qemu' type='raw' cache='writeback'/>
      <source file='${config}'/>
      <target dev='sda' bus='usb'/>
    </disk>${interfaces}
    <serial type='tcp'>
      <source mode='bind' host='127.0.0.1' service='${console_port}'/>
      <protocol type='telnet'/>
      <target port='0'/>
    </serial>
    <console type='tcp'>
      <source mode='bind' host='127.0.0.1' service='${console_port}'/>
      <protocol type='telnet'/>
      <target type='serial' port='0'/>
    </console>
  </devices>
</domain>
EOF
    log "  XML: $xml (console port: $console_port)"
}

# --------------- SUBCOMMANDS ---------------
do_setup() {
    require_root setup
    require_tools
    detect_libvirt_user
    [[ -f "$BASE_IMAGE" ]] || err "base image not found: $BASE_IMAGE"

    log "Setting up lab in $LAB_DIR"
    mkdir -p "$LAB_DIR"

    log "Bridges:"
    create_bridge "$MGMT_BRIDGE"
    for link in "${LINKS[@]}"; do
        read -r _ _ bridge <<< "$link"
        create_bridge "$bridge"
    done

    for i in $(seq 1 $NUM_ROUTERS); do
        log "Router $i:"
        local disk="${LAB_DIR}/rt${i}-disk.qcow2"
        local config="${LAB_DIR}/rt${i}-config.img"

        if [[ ! -f "$disk" ]]; then
            cp --reflink=auto "$BASE_IMAGE" "$disk"
            log "  disk: $disk"
        else
            log "  disk: $disk (exists, skipping)"
        fi

        # Always regenerate config drive in case juniper.conf changed
        make_config_drive "$i" "$config"

        chown "$LIBVIRT_USER:$LIBVIRT_GROUP" "$disk" "$config" 2>/dev/null || true
        chmod 644 "$disk" "$config"

        generate_xml "$i"

        # Undefine first if exists, so XML changes apply
        if virsh dominfo "vjunos-rt${i}" &>/dev/null; then
            local cur_state; cur_state=$(virsh domstate "vjunos-rt${i}")
            if [[ "$cur_state" == "running" ]]; then
                log "  vjunos-rt${i} is running - shutting down before redefine"
                virsh shutdown "vjunos-rt${i}" >/dev/null
                for _ in $(seq 1 60); do
                    [[ "$(virsh domstate vjunos-rt${i} 2>/dev/null)" != "running" ]] && break
                    sleep 1
                done
            fi
            # UEFI domains have NVRAM files - need --nvram to undefine cleanly.
            # Also handle managed-save state if present. Multiple fallbacks for virsh version differences.
            virsh undefine "vjunos-rt${i}" --nvram --managed-save >/dev/null 2>&1 || \
                virsh undefine "vjunos-rt${i}" --nvram >/dev/null 2>&1 || \
                virsh undefine "vjunos-rt${i}" >/dev/null 2>&1 || true
        fi
        virsh define "${LAB_DIR}/rt${i}.xml" >/dev/null
        log "  defined: vjunos-rt${i}"
    done

    log ""
    log "Setup complete. Run 'sudo $0 start' to boot the lab."
}

do_start() {
    require_root start
    for i in $(seq 1 $NUM_ROUTERS); do
        if ! virsh dominfo "vjunos-rt${i}" &>/dev/null; then
            log "vjunos-rt${i} not defined - run setup first"; continue
        fi
        local state; state=$(virsh domstate "vjunos-rt${i}")
        if [[ "$state" == "running" ]]; then
            log "vjunos-rt${i}: already running"
        else
            virsh start "vjunos-rt${i}" >/dev/null
            log "vjunos-rt${i}: started (console: telnet localhost $((CONSOLE_BASE_PORT + i)))"
        fi
    done
    log ""
    log "Routers booting. Junos Evolved takes ~3-5 minutes to reach login prompt."
}

do_stop() {
    require_root stop
    log "Sending graceful shutdown to all routers..."
    for i in $(seq 1 $NUM_ROUTERS); do
        if virsh dominfo "vjunos-rt${i}" &>/dev/null; then
            local state; state=$(virsh domstate "vjunos-rt${i}")
            if [[ "$state" == "running" ]]; then
                virsh shutdown "vjunos-rt${i}" >/dev/null
                log "  vjunos-rt${i}: shutdown sent"
            fi
        fi
    done
    log "Waiting up to 90s for graceful shutdown..."
    for sec in $(seq 1 90); do
        local running=0
        for i in $(seq 1 $NUM_ROUTERS); do
            [[ "$(virsh domstate vjunos-rt${i} 2>/dev/null)" == "running" ]] && running=1
        done
        [[ $running -eq 0 ]] && { log "All stopped after ${sec}s."; return; }
        sleep 1
    done
    log "WARNING: some routers didn't shut down cleanly."
}

do_destroy() {
    require_root destroy
    read -p "This will WIPE all routers and disks. Type YES to continue: " confirm
    [[ "$confirm" == "YES" ]] || { log "aborted"; exit 0; }

    do_stop
    for i in $(seq 1 $NUM_ROUTERS); do
        virsh undefine "vjunos-rt${i}" --nvram --managed-save 2>/dev/null || \
            virsh undefine "vjunos-rt${i}" --nvram 2>/dev/null || \
            virsh undefine "vjunos-rt${i}" 2>/dev/null || true
    done
    rm -f "${LAB_DIR}"/rt*-disk.qcow2 "${LAB_DIR}"/rt*-config.img "${LAB_DIR}"/rt*.xml
    delete_bridge "$MGMT_BRIDGE"
    for link in "${LINKS[@]}"; do
        read -r _ _ bridge <<< "$link"
        delete_bridge "$bridge"
    done
    log "Lab destroyed. Base image preserved: $BASE_IMAGE"
}

do_status() {
    echo "=== Routers ==="
    for i in $(seq 1 $NUM_ROUTERS); do
        if virsh dominfo "vjunos-rt${i}" &>/dev/null; then
            local state; state=$(virsh domstate "vjunos-rt${i}")
            printf "  vjunos-rt%d  %-10s  console: telnet localhost %d   lo0: 192.168.255.%d\n" \
                "$i" "$state" "$((CONSOLE_BASE_PORT + i))" "$i"
        else
            printf "  vjunos-rt%d  %-10s\n" "$i" "not-defined"
        fi
    done
    echo ""
    echo "=== Bridges ==="
    ip -br link show type bridge 2>/dev/null | grep -E "vjevo-mgmt|vjlink-" || echo "  (none)"
    echo ""
    echo "=== Topology ==="
    for link in "${LINKS[@]}"; do
        read -r a b br <<< "$link"
        echo "  rt${a%:*}:et-0/0/${a#*:}  <==[$br]==>  rt${b%:*}:et-0/0/${b#*:}"
    done
}

do_console() {
    local n="${1:-}"
    [[ -z "$n" ]] && err "Usage: $0 console <1|2|3>"
    [[ "$n" =~ ^[0-9]+$ && "$n" -ge 1 && "$n" -le $NUM_ROUTERS ]] || err "router number must be 1-$NUM_ROUTERS"
    local port=$((CONSOLE_BASE_PORT + n))
    log "rt${n} console (port $port). Disconnect: Ctrl-] then 'quit'"
    telnet localhost "$port"
}

# --------------- MAIN ---------------
case "${1:-}" in
    setup)   do_setup ;;
    start)   do_start ;;
    stop)    do_stop ;;
    destroy) do_destroy ;;
    status)  do_status ;;
    console) shift; do_console "$@" ;;
    -h|--help|"") usage ;;
    *)       usage; exit 1 ;;
esac
