277 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
#!/usr/local/bin/bash
 | 
						|
#
 | 
						|
set -euo pipefail
 | 
						|
IFS=$'\n\t'
 | 
						|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 | 
						|
 | 
						|
# Share a host directory to the guest via 9pfs.
 | 
						|
#
 | 
						|
# Inside the VM run:
 | 
						|
#   mount -t virtfs -o trans=virtio sharename /some/vm/path
 | 
						|
#   mount -t 9p -o cache=mmap -o msize=512000 sharename /mnt/9p
 | 
						|
#   mount -t 9p -o trans=virtio,cache=mmap,msize=512000 sharename /path/to/mountpoint
 | 
						|
# bhyve_options="-s 28,virtio-9p,sharename=/"
 | 
						|
 | 
						|
# Enable Sound
 | 
						|
# bhyve_options="-s 16,hda,play=/dev/dsp,rec=/dev/dsp"
 | 
						|
 | 
						|
# Example usage:
 | 
						|
#
 | 
						|
#   doas bhyve_netgraph_bridge create-disk zdata/vm/poudriere /vm/poudriere 10
 | 
						|
#   doas bhyve_netgraph_bridge start poudriere zdata/vm/poudriere /vm/poudriere /vm/iso/FreeBSD-13.2-RELEASE-amd64-bootonly.iso
 | 
						|
#   doas bhyve_netgraph_bridge start poudriere zdata/vm/poudriere /vm/poudriere
 | 
						|
 | 
						|
: ${VERBOSE:="NO"} # or YES
 | 
						|
: ${CPU_CORES:="1"}
 | 
						|
: ${MEMORY:="1G"}
 | 
						|
: ${NETWORK:="NAT"} # or RAW or BOTH
 | 
						|
: ${IP_RANGE:="10.215.1.1/24"} # Ignored for RAW networks
 | 
						|
: ${INTERFACE_NAME:="jail_nat"} # or the external interface like lagg0 for RAW networks
 | 
						|
: ${BRIDGE_NAME:="bridge_$INTERFACE_NAME"} # or bridge_raw for RAW networks
 | 
						|
: ${VNC_ENABLE:="NO"}
 | 
						|
: ${VNC_LISTEN:="127.0.0.1:5900"}
 | 
						|
: ${VNC_WIDTH:="1920"}
 | 
						|
: ${VNC_HEIGHT:="1080"}
 | 
						|
 | 
						|
if [ "$VERBOSE" = "YES" ]; then
 | 
						|
    set -x
 | 
						|
fi
 | 
						|
 | 
						|
############## Setup #########################
 | 
						|
 | 
						|
function cleanup {
 | 
						|
    for vm in "${vms[@]}"; do
 | 
						|
        log "Destroying bhyve vm $vm"
 | 
						|
        bhyvectl "--vm=$vm" --destroy
 | 
						|
        log "Destroyed bhyve vm $vm"
 | 
						|
    done
 | 
						|
}
 | 
						|
vms=()
 | 
						|
for sig in EXIT; do
 | 
						|
  trap "set +e; sleep 10; cleanup" "$sig"
 | 
						|
done
 | 
						|
 | 
						|
function die {
 | 
						|
    local status_code="$1"
 | 
						|
    shift
 | 
						|
    (>&2 echo "${@}")
 | 
						|
    exit "$status_code"
 | 
						|
}
 | 
						|
 | 
						|
function log {
 | 
						|
    (>&2 echo "${@}")
 | 
						|
}
 | 
						|
 | 
						|
############## Program #########################
 | 
						|
 | 
						|
function main {
 | 
						|
    local cmd="$1"
 | 
						|
    shift 1
 | 
						|
    if [ "$cmd" = "create-disk" ]; then
 | 
						|
        create_disk "${@}"
 | 
						|
    elif [ "$cmd" = "start" ]; then
 | 
						|
        start_vm "${@}"
 | 
						|
    else
 | 
						|
        die 1 "Unrecognized command $cmd"
 | 
						|
    fi
 | 
						|
}
 | 
						|
 | 
						|
function create_disk {
 | 
						|
    local zfs_path="$1"
 | 
						|
    local mount_path="$2"
 | 
						|
    local gigabytes="$3"
 | 
						|
    zfs create -o "mountpoint=$mount_path" "$zfs_path"
 | 
						|
    cp /usr/local/share/edk2-bhyve/BHYVE_UEFI_VARS.fd "${mount_path}/"
 | 
						|
    tee "${mount_path}/settings" <<EOF
 | 
						|
CPU_CORES="$CPU_CORES"
 | 
						|
MEMORY="$MEMORY"
 | 
						|
NETWORK="$NETWORK"
 | 
						|
IP_RANGE="$IP_RANGE"
 | 
						|
BRIDGE_NAME="$BRIDGE_NAME"
 | 
						|
INTERFACE_NAME="$INTERFACE_NAME"
 | 
						|
EOF
 | 
						|
    zfs create -s "-V${gigabytes}G" -o volmode=dev -o primarycache=metadata -o secondarycache=none "$zfs_path/disk0"
 | 
						|
}
 | 
						|
 | 
						|
function start_vm {
 | 
						|
    local name="$1"
 | 
						|
    local zfs_path="$2"
 | 
						|
    local mount_path="$3"
 | 
						|
    local mount_cd="${4:-}"
 | 
						|
 | 
						|
    if [ -e "${mount_path}/settings" ]; then
 | 
						|
        source "${mount_path}/settings"
 | 
						|
    fi
 | 
						|
 | 
						|
    local host_interface_name="$INTERFACE_NAME" # for raw, external interface
 | 
						|
    local bridge_name="$BRIDGE_NAME"
 | 
						|
    local ip_range="$IP_RANGE" # for raw this value does not matter
 | 
						|
 | 
						|
    local mac_address
 | 
						|
    mac_address=$(calculate_mac_address "$name")
 | 
						|
 | 
						|
    local additional_args=()
 | 
						|
 | 
						|
    if [ "$NETWORK" = "NAT" ]; then
 | 
						|
        assert_bridge "$host_interface_name" "$bridge_name" "$ip_range"
 | 
						|
        local bridge_link_name=$(detect_available_link "${bridge_name}")
 | 
						|
        additional_args+=("-s" "2:0,virtio-net,netgraph,path=${bridge_name}:,peerhook=${bridge_link_name},mac=${mac_address}")
 | 
						|
    elif [ "$NETWORK" = "RAW" ]; then
 | 
						|
        assert_raw "$host_interface_name" "$bridge_name"
 | 
						|
        local bridge_link_name=$(detect_available_link "${bridge_name}")
 | 
						|
        additional_args+=("-s" "2:0,virtio-net,netgraph,path=${bridge_name}:,peerhook=${bridge_link_name},mac=${mac_address}")
 | 
						|
    elif [ "$NETWORK" = "BOTH" ]; then
 | 
						|
        assert_bridge "jail_nat" "$bridge_name" "$ip_range"
 | 
						|
        assert_raw "$host_interface_name" "bridge_raw"
 | 
						|
        local bridge_link_name=$(detect_available_link "${bridge_name}")
 | 
						|
        local raw_bridge_link_name=$(detect_available_link "bridge_raw")
 | 
						|
        local raw_mac_address=$(calculate_mac_address "${name}_raw")
 | 
						|
        additional_args+=("-s" "2:0,virtio-net,netgraph,path=${bridge_name}:,peerhook=${bridge_link_name},mac=${mac_address}")
 | 
						|
        additional_args+=("-s" "3:0,virtio-net,netgraph,path=bridge_raw:,peerhook=${raw_bridge_link_name},mac=${raw_mac_address}")
 | 
						|
    else
 | 
						|
        die 1 "Unrecognized NETWORK type $NETWORK"
 | 
						|
    fi
 | 
						|
 | 
						|
 | 
						|
    # -H release the CPU when guest issues HLT instruction. Otherwise 100% of core will be consumed.
 | 
						|
         # -s 3,ahci-cd,/vm/.iso/archlinux-2023.04.01-x86_64.iso \
 | 
						|
             # -s 29,fbuf,tcp=0.0.0.0:5900,w=1920,h=1080,wait \
 | 
						|
            # -s 29,fbuf,tcp=0.0.0.0:5900,w=1920,h=1080 \
 | 
						|
 | 
						|
    # TODO: Look into using nmdm instead of stdio for serial console
 | 
						|
    if [ -n "$mount_cd" ]; then
 | 
						|
        additional_args+=("-s" "5,ahci-cd,$mount_cd")
 | 
						|
    fi
 | 
						|
    if [ "$VNC_ENABLE" = "YES" ]; then
 | 
						|
        additional_args+=("-s" "29,fbuf,tcp=$VNC_LISTEN,w=$VNC_WIDTH,h=$VNC_HEIGHT")
 | 
						|
    fi
 | 
						|
    vms+=("$name")
 | 
						|
    while true; do
 | 
						|
        set -x
 | 
						|
        set +e
 | 
						|
        bhyve \
 | 
						|
            -D \
 | 
						|
            -c $CPU_CORES \
 | 
						|
            -m $MEMORY \
 | 
						|
            -H \
 | 
						|
            -P \
 | 
						|
            -o 'rtc.use_localtime=false' \
 | 
						|
            -s 0,hostbridge \
 | 
						|
            -s "4,nvme,/dev/zvol/${zfs_path}/disk0" \
 | 
						|
            -s 30,xhci,tablet \
 | 
						|
            -s 31,lpc -l com1,stdio \
 | 
						|
            -l "bootrom,/usr/local/share/uefi-firmware/BHYVE_UEFI.fd,${mount_path}/BHYVE_UEFI_VARS.fd" \
 | 
						|
            "${additional_args[@]}" \
 | 
						|
            "$name"
 | 
						|
        local exit_code=$?
 | 
						|
        set -e
 | 
						|
        set +x
 | 
						|
        if [ $exit_code -eq 0 ]; then
 | 
						|
            echo "Rebooting."
 | 
						|
            sleep 5
 | 
						|
        elif [ $exit_code -eq 1 ]; then
 | 
						|
            echo "Powered off."
 | 
						|
            break
 | 
						|
        elif [ $exit_code -eq 2 ]; then
 | 
						|
            echo "Halted."
 | 
						|
            break
 | 
						|
        elif [ $exit_code -eq 3 ]; then
 | 
						|
            echo "Triple fault."
 | 
						|
            break
 | 
						|
        elif [ $exit_code -eq 4 ]; then
 | 
						|
            echo "Exited due to an error."
 | 
						|
            break
 | 
						|
        fi
 | 
						|
    done
 | 
						|
}
 | 
						|
 | 
						|
function detect_available_link {
 | 
						|
    local bridge_name="$1"
 | 
						|
    local linknum=1
 | 
						|
    while true; do
 | 
						|
        local link_name="link${linknum}"
 | 
						|
        if ! ng_exists "${bridge_name}:${link_name}"; then
 | 
						|
            echo "$link_name"
 | 
						|
            return
 | 
						|
        fi
 | 
						|
        linknum=$((linknum + 1))
 | 
						|
        if [ "$linknum" -gt 90 ]; then
 | 
						|
            (>&2 echo "No available links on bridge $bridge_name")
 | 
						|
            exit 1
 | 
						|
        fi
 | 
						|
    done
 | 
						|
}
 | 
						|
 | 
						|
function assert_bridge {
 | 
						|
    local host_interface_name="$1"
 | 
						|
    local bridge_name="$2"
 | 
						|
    local ip_range="$3"
 | 
						|
 | 
						|
    if ! ng_exists "${bridge_name}:"; then
 | 
						|
        ngctl -d -f - <<EOF
 | 
						|
mkpeer . eiface hook ether
 | 
						|
name .:hook $host_interface_name
 | 
						|
EOF
 | 
						|
        ngctl -d -f - <<EOF
 | 
						|
mkpeer ${host_interface_name}: bridge ether link0
 | 
						|
name ${host_interface_name}:ether $bridge_name
 | 
						|
EOF
 | 
						|
        ifconfig $(ngctl msg "${host_interface_name}:" getifname | grep Args | cut -d '"' -f 2) name "${host_interface_name}" "$ip_range" up
 | 
						|
    fi
 | 
						|
}
 | 
						|
 | 
						|
function assert_raw {
 | 
						|
    local extif="$1"
 | 
						|
    local bridge_name="$2"
 | 
						|
 | 
						|
    kldload -n ng_bridge ng_eiface ng_ether
 | 
						|
 | 
						|
    if ! ng_exists "${bridge_name}:"; then
 | 
						|
        ngctlcat <<EOF
 | 
						|
# Create a bridge.
 | 
						|
mkpeer $extif: bridge lower link0
 | 
						|
# Assign a name to the bridge.
 | 
						|
name $extif:lower ${bridge_name}
 | 
						|
# Since the host is also using $extif, we need to connect the upper hook also. Otherwise we will lose connectivity.
 | 
						|
connect $extif: ${bridge_name}: upper link1
 | 
						|
 | 
						|
# Enable promiscuous mode so the host ethernet adapter accepts packets for all addresses
 | 
						|
msg $extif: setpromisc 1
 | 
						|
 | 
						|
# Do not overwrite source address on packets
 | 
						|
msg $extif: setautosrc 0
 | 
						|
EOF
 | 
						|
    fi
 | 
						|
}
 | 
						|
 | 
						|
function ng_exists {
 | 
						|
    ngctl status "${1}" >/dev/null 2>&1
 | 
						|
}
 | 
						|
 | 
						|
function calculate_mac_address {
 | 
						|
    local name="$1"
 | 
						|
    local source
 | 
						|
    source=$(md5 -r -s "$name" | awk '{print $1}')
 | 
						|
    echo "06:${source:0:2}:${source:2:2}:${source:4:2}:${source:6:2}:${source:8:2}"
 | 
						|
}
 | 
						|
 | 
						|
function find_available_port {
 | 
						|
    local start_port="$1"
 | 
						|
    local port="$start_port"
 | 
						|
    while true; do
 | 
						|
        sockstat -P tcp -p 443
 | 
						|
        port=$((port + 1))
 | 
						|
    done
 | 
						|
}
 | 
						|
 | 
						|
function ngctlcat {
 | 
						|
    if [ "$VERBOSE" = "YES" ]; then
 | 
						|
        tee /dev/tty | ngctl -d -f -
 | 
						|
    else
 | 
						|
        ngctl -d -f -
 | 
						|
    fi
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
main "${@}"
 |