#!/bin/sh
# SPDX-License-Identifier: BSD-2-Clause
# SPDX-FileCopyrightText: 2016 Matt Churchyard <churchers@gmail.com>

# 'vm _run'
# run a virtual machine
# this is the background process that does all the work
# in most cases this should not be run directly
#
# @param string _name the name of the guest to run
# @param optional string _iso the iso file for an install
#
vm::run(){
    local _name _iso _iso_dev
    local _cpu _memory _bootdisk _bootdisk_dev _guest _wiredmem
    local _guest_support _uefi _uuid _debug _hostbridge _fwcfg _loader
    local _opts _devices _slot _bus=0 _maxslots=31 _install_slot _func=0 _taplist _exit _passdev
    local _com _comports _comstring _logpath="/dev/null" _run=1
    local _bhyve_options _action

    cmd::parse_args "$@"
    shift $?
    _name="$1"
    _iso="$2"

    # try to load datstore details
    datastore::get_guest "${_name}" || exit 5

    # bail out immediately if guest running
    vm::confirm_stopped "${_name}" "1" || exit 10

    if [ -d "${VM_DS_PATH}/${_name}/.cloud-init" ] && [ ! -f "${VM_DS_PATH}/${_name}/seed.iso" ]; then
      makefs -t cd9660 -o R,L=cidata "${VM_DS_PATH}/${_name}/seed.iso" "${VM_DS_PATH}/${_name}/.cloud-init" || util::err "Can't write seed.iso for cloud-init"
      util::log "guest" "${_name}" "created ${VM_DS_PATH}/${_name}/seed.iso for cloud-init"
    fi

    config::load "${VM_DS_PATH}/${_name}/${_name}.conf"
    config::get "_memory" "memory"
    config::get "_loader" "loader"
    config::get "_bootdisk" "disk0_name"
    config::get "_bootdisk_dev" "disk0_dev" "file"
    config::get "_hostbridge" "hostbridge" "standard"
    config::get "_fwcfg" "fwcfg"
    config::get "_comports" "comports" "com1"
    config::get "_uuid" "uuid"
    config::get "_debug" "debug" "no"
    config::get "_bhyve_options" "bhyve_options"
    config::get "_slot" "start_slot" "4"
    config::get "_install_slot" "install_slot" "3"

    # generate a uuid if we don't have one already
    if [ -z "${_uuid}" ]; then
        _uuid=$(uuidgen)
        config::set "${_name}" "uuid" "${_uuid}"
    fi

    # get cpu topology
    vm::__cpu "_cpu"

    util::log_rotate "guest" "${_name}"
    util::log "guest" "${_name}" \
      "initialising" \
      " [loader: ${_loader}]" \
      " [cpu: ${_cpu}]" \
      " [memory: ${_memory}]" \
      " [hostbridge: ${_hostbridge}]" \
      " [com ports: ${_comports}]" \
      " [uuid: ${_uuid}]" \
      " [debug mode: ${_debug}]" \
      " [primary disk: ${_bootdisk}]" \
      " [primary disk dev: ${_bootdisk_dev}]"

    # check basic settings
    if [ -z "${_loader}" -o -z "${_cpu}" -o -z "${_memory}" ]; then
        util::log "guest" "${_name}" "fatal; unable to start - missing required configuration"
        exit 15
    fi

    # check ug
    if [ -n "${VM_NO_UG}" ]; then

        # only FreeBSD guests. these start direct in 64bit mode and don't need UG
        if [ "${_loader}" != "bhyveload" ]; then
            util::log "guest" "${_name}" "fatal; unable to start - no unrestricted guest support"
            exit 15
        fi

        # only 1 vcpu
        if [ "${_cpu}" != "1" ]; then
            _cpu=1
            util::log "guest" "${_name}" "warning; no unrestricted guest support. reducing vcpu count to 1"
        fi
    fi

    # default bhyve options
    _opts="-AHPw"

    # ignore access to unimplemented Model Specific Registers?
    config::yesno "ignore_msr" && _opts="${_opts}w"

    # if uefi, make sure we have bootrom, then update options for uefi support
    if [ "${_loader%-*}" = "uefi" ]; then
        vm::uefi
    fi

    # add any custom bhyve options
    [ -n "${_bhyve_options}" ] && _opts="${_opts} ${_bhyve_options}"

    # if we have passthru, check vt-d or amdvi support now and exit
    config::get "_passdev" "passthru0"

    if [ -n "${_passdev}" ] && ! util::check_bhyve_iommu; then
        util::log "guest" "${_name}" "fatal; pci passthrough not supported on this system (no VT-d or amdvi)"
        exit 15
    fi

    # wired memory?
    if config::yesno "wired_memory"; then
        _wiredmem="1"
        _opts="${_opts} -S"
    fi

    # set cpu/memory and uuid in opts
    _opts="-c ${_cpu} -m ${_memory} ${_opts}"
    [ -n "${_uuid}" ] && _opts="${_opts} -U ${_uuid}"

    # set utc time in opts if requested
    if config::yesno "utctime" yes; then
        if [ ${VERSION_BSD} -ge 1002000 ]; then
            _opts="${_opts} -u"
        else
            util::log "guest" "${_name}" "warning; utc time requested but not available pre FreeBSD 10.2"
        fi
    fi

    # send bhyve output to bhyve.log if debug=yes
    util::yesno "${_debug}" && _logpath="${VM_DS_PATH}/${_name}/bhyve.log"

    # complete the boot disk path
    [ -n "${_bootdisk}" ] && vm::get_disk_path "_bootdisk" "${_name}" "${_bootdisk}" "${_bootdisk_dev}"

    # build bhyve device string
    vm::bhyve_device_comports
    vm::bhyve_device_basic
    vm::bhyve_device_disks
    vm::bhyve_device_networking
    vm::bhyve_device_rand
    vm::bhyve_device_passthru
    vm::bhyve_device_fbuf
    vm::bhyve_device_mouse
    vm::bhyve_device_sound
    vm::bhyve_device_console

    vm::lock
    util::log "guest" "${_name}" "booting"
    cd /

    while true; do

        # destroy existing vmm
        # freebsd seems happy to run a bhyveload/bhyve loop
        # grub-bhyve doesn't seem to like it for a lot of users
        # Peter says don't destroy in Windows instructions, so don't if in UEFI mode
        if [ -e "/dev/vmm/${_name}" -a -z "${_uefi}" ]; then
            bhyvectl --vm="${_name}" --destroy >/dev/null 2>&1
            if [ $? -ne 0 ]; then
                util::log "guest" "${_name}" "fatal; failed to destroy existing vmm device"
                _exit=15
                break
            fi
            sleep 1
        fi

        # run any prestart script while guest is fully down
        vm::prestart

        # add install iso or disk image
        if [ -n "${_iso}" ]; then
            if echo "${_iso}" | grep -iqs '.iso$'; then
                _iso_dev="-s ${_install_slot}:0,ahci-cd,${_iso},ro,bootindex=1"
            else
                _iso_dev="-s ${_install_slot}:0,ahci-hd,${_iso},ro"
            fi
        fi

        # use null.iso if not an install and uefi firmware
        # old instructions but some windows versions apparently needed this present
        [ -z "${_iso}" -a "${_loader}" = "uefi" ] && config::yesno "nulliso_fix" && \
            _iso_dev="-s ${_install_slot}:0,ahci-cd,${vm_dir}/.config/null.iso"

        # reasonably ugly hack to remove wait option after first run
        [ "${_run}" -eq "2" ] && vm::bhyve_device_fbuf_clear_wait

        # load guest
        if [ -z "${_uefi}" ]; then

            guest::load "${_iso}"
            _exit=$?

            # check no errors
            if [ ${_exit} -ne 0 ]; then
                util::log "guest" "${_name}" "fatal; loader returned error ${_exit}"
                break
            fi
        fi

	if [ ${_bus} -ge 1 ]; then
		_opts="${_opts} -Y"
	fi

        #
        # from this line onward, no more changes in _opts
        #

        # resume if saved vm state found
        set -- ${_opts} # convert _opts to a positional parameter
        if vm::has_saved_state "${_name}"; then
            if util::check_bhyve_suspend; then
                util::log "guest" "${_name}" "saved vm state found, resuming"
                # there is no safe way to store a file path containing spaces in a string variable
                # the shell treats the quotes (" or ') in below examples as literal chatacters
                #   _opts="${_opts} -r \"${VM_DS_PATH}/${_name}/${_name}.save\""
                #   _opts="${_opts} -r '${VM_DS_PATH}/${_name}/${_name}.save'"

                # using positional parameters to handle paths that may contain spaces
                set -- "$@" -r "${VM_DS_PATH}/${_name}/${_name}.save"
            else
                util::log "guest" "${_name}" \
                    "saved vm state found but suspend/resume is unavailable or disabled, attempting to cold boot"
            fi
        fi

        util::log "guest" "${_name}" " [bhyve options: $*]" \
          " [bhyve devices: ${_devices}]" \
          " [bhyve console: ${_comstring}]"
        [ -n "${_iso_dev}" ] && util::log "guest" "${_name}" " [bhyve iso device: ${_iso_dev}]"
        util::log "guest" "${_name}" "starting bhyve (run ${_run})"

        # call rctl now as next line will block until bhyve exits
        rctl::set &

        # actually run bhyve!
        # we're already in the background so we just wait for it to exit
        bhyve $@ \
              ${_devices} \
              ${_iso_dev} \
              ${_comstring} \
              ${_name} 2> "${_logpath}"

        # get bhyve exit code
        _exit=$?
        util::log "guest" "${_name}" "bhyve exited with status ${_exit}"

        # remove any console sockets
        rm ${VM_DS_PATH}/${_name}/vtcon.* >/dev/null 2>&1

        # suspend
        [ -f "${VM_DS_PATH}/${_name}/suspending.lock" ] && break

        # decide what to do with exit code
        vm::handle_exit "_action" ${_exit}
        [ "${_action}" != "restart" ] && break

        util::log "guest" "${_name}" "restarting"

        # remove install iso so guest reboots from disk
        # after install non-uefi guests will still get install cd until a full shutdown+restart
        # as we don't reset _iso_dev
        _iso=""
        _run=$((_run + 1))
    done

    # destroy taps
    for _devices in ${_taplist}; do
        util::log "guest" "${_name}" "destroying network device ${_devices}"
        ifconfig "${_devices}" destroy
    done

    if [ -e "${VM_DS_PATH}/${_name}/suspending.lock" ]; then
        util::log "guest" "${_name}" "suspended"
    else
        util::log "guest" "${_name}" "stopped"
    fi
    [ -e "/dev/vmm/${_name}" ] && bhyvectl --destroy --vm=${_name} >/dev/null 2>&1

    vm::unlock
    exit ${_exit}
}

# creates options to use a uefi bootrom
#
# @modifies _opts _uefi
#
vm::uefi(){
    local _bootrom

    if [ ${VERSION_BSD} -lt 1002509 ]; then
        util::log "guest" "${_name}" "fatal; uefi guests can only be run on FreeBSD 10.3 or newer"
        exit 15
    fi

    case "${_loader}" in
        uefi-devel)
            _bootrom="/usr/local/share/uefi-firmware/BHYVE_UEFI_CODE-devel.fd"
            ;;
        uefi-csm)
            _bootrom="/usr/local/share/uefi-firmware/BHYVE_UEFI_CSM.fd"
            ;;
        uefi-custom)
            _bootrom="${VM_DS_PATH}/.config/BHYVE_UEFI.fd"
           ;;
        *)
           _bootrom="/usr/local/share/uefi-firmware/BHYVE_UEFI.fd"
           ;;
    esac

    if [ ! -e "${_bootrom}" ]; then
        util::log "guest" "${_name}" "fatal; unable to locate firmware ${_bootrom}"
        exit 15
    fi

    # should we store uefi vars?
    if config::yesno "uefi_vars"; then

        # do we already have a storage file for this guest?
        if [ -e "${VM_DS_PATH}/${_name}/uefi-vars.fd" ]; then
            :
        elif [ -e "/usr/local/share/uefi-firmware/BHYVE_UEFI_VARS.fd" ]; then
            # create a copy and use
            cp "/usr/local/share/uefi-firmware/BHYVE_UEFI_VARS.fd" "${VM_DS_PATH}/${_name}/uefi-vars.fd"
        else
            util::log "guest" "${_name}" "fatal; unable to locate UEFI vars database or template"
            exit 15
        fi

        _bootrom="${_bootrom},${VM_DS_PATH}/${_name}/uefi-vars.fd"
    fi

    _opts="${_opts} -l bootrom,${_bootrom}"
    _uefi="yes"
}

# decide how to handle bhyve exit code
#
# @param string _var variable to put action into
# @param int _code bhyve exit code
#
vm::handle_exit(){
    local _var="$1"
    local _code="$2"

    # check exit code
    # get relevant action from config, defaulting to the behaviour
    # we'd normally expect. we don't currently allow overriding shutdown
    # as it makes it impossible to actually stop a guest cleanly
    #
    case "${_code}" in
        0) config::get "${_var}" "on_restart" "restart" ;;
        1) ;&
        2) if [ -e "${VM_DS_PATH}/${_name}/restart" ]; then
               setvar "${_var}" "restart"
               unlink "${VM_DS_PATH}/${_name}/restart" >/dev/null 2>&1
           else
               setvar "${_var}" "shutdown"
           fi
           ;;
        *) config::get "${_var}" "on_fault" "shutdown" ;;
    esac
}

# lock a vm
# stop another instance being started on this or another host
# we write hostname so vm-bhyve can inform user which host locked a vm
#
# @param string - the name of the guest to lock
#
vm::lock(){
    hostname > "${VM_DS_PATH}/${_name}/run.lock"
}

# unlock a vm
#
# @param string - the name of the guest to unlock
#
vm::unlock(){
    unlink "${VM_DS_PATH}/${_name}/run.lock" >/dev/null 2>&1
    unlink "${VM_DS_PATH}/${_name}/suspending.lock" >/dev/null 2>&1
    unlink "${VM_DS_PATH}/${_name}/console" >/dev/null 2>&1
}

# create string for guest com ports
# this builds the '-l comX' part of the bhyve command into _comstring
# _com is used by bhyveload|grub_bhyve so we set that to the first
# com port we come across.
# The nmdm devices are written to $vm_dir/{guest}/console so we can
# read them back later for the 'vm console' command
#
# @modifies _com _comstring
#
vm::bhyve_device_comports(){
    local _port _num=1 _tmux_name
    local _port_name

    unlink "${VM_DS_PATH}/${_name}/console" >/dev/null 2>&1

    for _port in ${_comports}; do
        if [ ${_num} -eq 1 ]; then
            # if tmux mode, log this to the console data
            if [ -n "${VM_OPT_TMUX}" ]; then
                _tmux_name=$(echo "${_name}" | tr "." "~")
                echo "${_port}=tmux/${_tmux_name}" >> "${VM_DS_PATH}/${_name}/console"
            fi

            # if foreground mode, we don't configure a serial port
            if [ -n "${VM_OPT_FOREGROUND}" ]; then
                _comstring="-l ${_port},stdio"
                _num=$((_num + 1))
                continue
            fi
        fi

        # generate a port name unique to this vm and port number
        _port_name="/dev/nmdm-${_name}.${_num}"

        # use first com port for the loader
        [ ${_num} -eq 1 ] && _com="${_port_name}A"

        echo "${_port}=${_port_name}B" >> "${VM_DS_PATH}/${_name}/console"
        _comstring="${_comstring}${_comstring:+ }-l ${_port},${_port_name}A"
        _num=$((_num + 1))
    done
}

# get bhyve device string for basic devices
# hostbridge & lpc on their own slots
# windows requires slot 0 & 31, nothing else cares fortunately
#
# @modifies _devices
#
vm::bhyve_device_basic(){

    # add hostbridge
    case "$_hostbridge" in
        no*) ;;
        amd) _devices="-s 0,amd_hostbridge" ;;
        *)   _devices="-s 0,hostbridge" ;;
    esac

    # lpc
    _devices="${_devices}${_devices:+ }-s 31,lpc${_fwcfg:+,fwcfg=$_fwcfg}"
}

# get bhyve device string for disk devices
# read all disks starting at 0 and add to the _devices string
# this is done first so disks will start at slot 4. For uefi,
# we move through slots and stop at slot 6. For non-uefi we
# step through all functions and just keep going
#
# since r302459 the ahci controller supports
# up to 32 devices per controller. by default we set
# the device limit to 1 and use the original syntax, but
# this can be overridden by setting the ahci_device_limit
# guest option to an integer between 2 and 32.
#
# @modifies _devices _slot _bus
#
vm::bhyve_device_disks(){
    local _disk _type _dev _path _opts _ahci _atype
    local _ahci_num=0 _num=0 _add _ahci_multi
    local _ahci_limit=1

    # check if user has set a per-controller device limit
    config::get "_ahci_multi" "ahci_device_limit"

    if [ ${VERSION_BSD} -ge 1200000 ] || \
       [ ${VERSION_BSD} -ge 1101000 ] || \
       [ ${VERSION_BSD} -ge 1004000 ]; then

        if [ -n "${_ahci_multi}" ]; then

            # see if it's numeric
            echo "${_ahci_multi}" | egrep -iqs '^[0-9]+$'

            [ $? -eq 0 -a ${_ahci_multi} -gt 1 -a ${_ahci_multi} -le 32 ] && \
                _ahci_limit="${_ahci_multi}"
        fi
    fi

    # get all disks
    while true; do
        config::get "_disk" "disk${_num}_name"
        config::get "_type" "disk${_num}_type"
        [ -z "${_disk}" -o -z "${_type}" ] && break

        config::get "_dev" "disk${_num}_dev"
        config::get "_opts" "disk${_num}_opts"

        if [ ${_func} -ge 8 ]; then
            _func=0
            _slot=$((_slot + 1))
        fi
	if [ ${_slot} -ge ${_maxslots} ]; then
	    _slot=0
	    _bus=$((_bus + 1))
            _maxslots=32
        fi

        vm::get_disk_path "_path" "${_name}" "${_disk}" "${_dev}"

        # ahci device and multi mode?
        if [ ${_ahci_limit} -gt 1 -a "${_type%%-*}" = "ahci" ]; then

            # check device type
            case "${_type}" in
                ahci-cd)
                    _atype="cd"
                    ;&
                ahci-hd)
                    [ -z "${_atype}" ] && _atype="hd"
                    _ahci="${_ahci},${_atype}:${_path}"
                    [ -n "${_opts}" ] && _ahci="${_ahci},${_opts}"
                    _ahci_num=$((_ahci_num + 1))

                    # we need to move to another controller if we get to the limit
                    if [ ${_ahci_num} -ge ${_ahci_limit} ]; then
                        _devices="${_devices} -s ${_bus}:${_slot}:${_func},ahci${_ahci}"
                        _ahci=""
                        _ahci_num=0
                        _add=1
                    fi
                    ;;
            esac

            _atype=""
        else

            _devices="${_devices} -s ${_bus}:${_slot}:${_func},${_type},${_path}"
            [ -n "${_opts}" ] && _devices="${_devices},${_opts}"
            _add=1
        fi

        # have we just added a device?
        if [ -n "${_add}" ]; then
            if [ -n "${_uefi}" ]; then
                _slot=$((_slot + 1))

            else
                _func=$((_func + 1))
            fi

            _add=""
        fi

	if [ ${_slot} -ge ${_maxslots} ]; then
	    _slot=0
	    _bus=$((_bus + 1))
	    _maxslots=32
        fi

        _num=$((_num + 1))
    done

    # have ahci devices left?
    if [ -n "${_ahci}" ]; then
        _devices="${_devices} -s ${_slot}:${_func},ahci${_ahci}"
        [ -n "${_uefi}" ] && _slot=$((_slot + 1))
    fi

    # move to next slot if we have devices
    # unless uefi as we already inc slot in uefi mode
    if [ ${_num} -ge 1 -a -z "${_uefi}" ]; then
        _slot=$((_slot + 1))
        _func=0
    fi
    if [ ${_slot} -ge ${_maxslots} ]; then
	_slot=0
	_bus=$((_bus + 1))
	_maxslots=32
    fi
}

# get bhyve device string for networking
# we dynamically create a new tap device for each interface
# if we can find the correct bridge, we then add the tap as a member
# we add each tap to __taplist from vm::run which it will
# use to desstroy them all on shutdown
#
# @modifies _devices _slot _bus _taplist _func
#
vm::bhyve_device_networking(){
    local _emulation _num=0

    while true; do
        config::get "_emulation" "network${_num}_type"
        [ -z "${_emulation}" ] && break

        # move slot if we've hit function 8
        if [ ${_func} -ge 8 ]; then
            _func=0
            _slot=$((_slot + 1))
        fi
	if [ ${_slot} -ge ${_maxslots} ]; then
	    _slot=0
	    _bus=$((_bus + 1))
	    _maxslots=32
        fi

        switch::provision
        _num=$((_num + 1))
    done

    if [ ${_num} -ge 1 ]; then
        _slot=$((_slot + 1))
        _func=0
    fi
    if [ ${_slot} -ge ${_maxslots} ]; then
        _slot=0
        _bus=$((_bus + 1))
	_maxslots=32
    fi
}

# check if user wants a virtio-rand device
#
# @modifies _devices _slot
#
vm::bhyve_device_rand(){

    if config::yesno "virt_random"; then
        _devices="${_devices} -s ${_bus}:${_slot}:0,virtio-rnd"
        _slot=$((_slot + 1))
    fi
    if [ ${_slot} -ge ${_maxslots} ]; then
        _slot=0
        _bus=$((_bus + 1))
	_maxslots=32
    fi
}

# add frame buffer output
#
vm::bhyve_device_fbuf(){
    local _port _listen _res _pass _vga _wait
    local _fbuf_conf

    # only works in uefi mode
    [ -z "${_uefi}" ] && return 0

    # check graphics enabled
    ! config::yesno "graphics" && return 0

    # only available in 11+
    if [ ${VERSION_BSD} -lt 1100000 ]; then
        util::log "guest" "${_name}" "warning; UEFI graphics is only available in FreeBSD 11 and newer"
        return 1
    fi

    config::get "_port" "graphics_port"
    config::get "_listen" "graphics_listen"
    config::get "_res" "graphics_res"
    config::get "_vga" "graphics_vga"
    config::get "_pass" "vnc_password"
    config::get "_wait" "graphics_wait" "auto"

    # check if graphics_wait is auto
    # auto will count as yes so we need to unset it if we're not in an install.
    [ "${_wait}" = "auto" -a -z "${_iso}" ] && _wait="no"

    # try to get port
    # return if we can't
    if [ -z "${_port}" ]; then
        vm::find_available_net_port "_port" "5900"

        if [ -z "${_port}" ]; then
            util::log "guest" "${_name}" "warning; unable to allocate a network port for graphics/vnc"
            return 1
        fi

        util::log "guest" "${_name}" "dynamically allocated port ${_port} for vnc connections"
    fi

    # add ip, port, resolution, wait
    _fbuf_conf="tcp=${_listen:-0.0.0.0}:${_port}"
    [ -n "${_res}" ] && _fbuf_conf="${_fbuf_conf},w=${_res%%x*},h=${_res##*x}"
    [ -n "${_vga}" ] && _fbuf_conf="${_fbuf_conf},vga=${_vga}"
    [ -n "${_pass}" ] && _fbuf_conf="${_fbuf_conf},password=${_pass}"
    util::yesno "${_wait}" && _fbuf_conf="${_fbuf_conf},wait"

    # write vnc port to console data
    echo "vnc=${_listen:-0.0.0.0}:${_port}" >> "${VM_DS_PATH}/${_name}/console"

    # add device
    _devices="${_devices} -s ${_bus}:${_slot}:0,fbuf,${_fbuf_conf}"
    _slot=$((_slot + 1))
    if [ ${_slot} -ge ${_maxslots} ]; then
        _slot=0
        _bus=$((_bus + 1))
	_maxslots=32
    fi
}

# remove wait option if we're in auto mode
# fairly ugly code that uses sed to look for ",wait{word boundary}"
# and removes it. Hopefully this will never match anything else in
# the device string...
#
# @modifies _devices
#
vm::bhyve_device_fbuf_clear_wait(){
    local _wait

    [ -z "${_uefi}" ] && return

    config::get "_wait" "graphics_wait" "auto"
    [ "${_wait}" = "auto" ] && _devices=$(echo "${_devices}" | sed 's@,wait[[:>:]]@@')
}

# add a xhci mouse device
#
vm::bhyve_device_mouse(){

    [ ${VERSION_BSD} -lt 1100000 ] && return 0

    # add a tablet device if enabled
    if config::yesno "xhci_mouse"; then
        _devices="${_devices} -s ${_bus}:${_slot}:0,xhci,tablet"
        _slot=$((_slot + 1))
    fi
    if [ ${_slot} -ge ${_maxslots} ]; then
        _slot=0
        _bus=$((_bus + 1))
	_maxslots=32
    fi
}

vm::bhyve_device_sound(){
    local _play _rec
    config::get "_play" "sound_play" "/dev/dsp0"
    config::get "_rec" "sound_rec"

    if config::yesno "sound"; then
        _devices="${_devices} -s ${_bus}:${_slot}:0,hda,play=${_play}"

        if [ -n "${_rec}" ]; then
            _devices="${_devices},rec=${_rec}"
        fi

        _slot=$((_slot + 1))
    fi
    if [ ${_slot} -ge ${_maxslots} ]; then
        _slot=0
        _bus=$((_bus + 1))
	_maxslots=32
    fi
}

# add virtio_console devices to the guest
#
# @modifies _devices _slot
#
vm::bhyve_device_console(){
    local _console _curr=0
    local _dev_str

    [ ${VERSION_BSD} -lt 1102000 ] && return 0
    config::get "_console" "virt_console0"
    [ -z "${_console}" ] && return 0

    # add ports
    while [ -n "${_console}" -a ${_curr} -lt 16 ]; do
         # if set to "yes/on/1", just use the console number as port name
         case "${_console}" in
             [Yy][Ee][Ss]|[Oo][Nn]|1) _console="${_curr}" ;;
         esac

         _dev_str="${_dev_str},${_console}=${VM_DS_PATH}/${_name}/vtcon.${_console}"

         _curr=$((_curr + 1))
         config::get "_console" "virt_console${_curr}"
    done

    _devices="${_devices} -s ${_bus}:${_slot}:0,virtio-console${_dev_str}"
    _slot=$((_slot + 1))
    if [ ${_slot} -ge ${_maxslots} ]; then
        _slot=0
        _bus=$((_bus + 1))
	_maxslots=32
    fi
}

# get any pci passthrough devices
# FreeBSD 11 needs wired memory so update _opts in that case if
# we have any pass through devices
#
# @modifies _devices _slot _bus _opts _wiredmem
#
vm::bhyve_device_passthru(){
    local _dev _orig_slot _func=0
    local _last_orig_slot
    local _num=0

    while true; do
        config::get "_dev" "passthru${_num}"
        [ -z "${_dev}" ] && break

        # see if there's an = sign
        # we allow A/B/C=D:E to force D:E as the guest SLOT:FUNC
        if echo "${_dev}" | grep -qs "="; then
            _devices="${_devices} -s ${_bus}:${_dev##*=},passthru,${_dev%%=*}"
        else
            _orig_slot=${_dev%%/*}

            # only move to new slot if the original device is on a different slot to the last one.
            # if user wants to passthru a device that has multiple functions which must stay together
            # on one slot, they should be together in configuration file
            if [ -n "${_last_orig_slot}" -a "${_last_orig_slot}" != "${_orig_slot}" ]; then
                _slot=$((_slot + 1))
                _func=0
            fi

        if [ ${_slot} -ge ${_maxslots} ]; then
            _slot=0
            _bus=$((_bus + 1))
	    _maxslots=32
        fi

            _devices="${_devices} -s ${_bus}:${_slot}:${_func},passthru,${_dev}"
            _last_orig_slot=${_orig_slot}
            _func=$((_func + 1))
        fi

        _num=$((_num + 1))
    done

    if [ ${_num} -ge 1 ]; then
        _slot=$((_slot + 1))

        # add wired memory for 10.3+
        [ ${VERSION_BSD} -ge 1003000 ] && _opts="${_opts} -S" && _wiredmem="1"
    fi
    if [ ${_slot} -ge ${_maxslots} ]; then
        _slot=0
        _bus=$((_bus + 1))
	_maxslots=32
    fi
}

# get the path to a disk image depending on type.
# the disk name in configuration is usually the name of the file
# or zvol, directly under the guest directory/dataset.
# if a user wants the disk image anywhere else, they can specify
# the following
# diskX_dev="custom"
# diskX_name="/full/path/to/disk/device/or/file"
#
# @param string _var variable to put disk path into
# @param string _name the name of the guest
# @param string _disk the name of the disk
# @param string _disk_dev=file|zvol|sparse-zvol|custom type of device
#
vm::get_disk_path(){
    local _var="$1"
    local _name="$2"
    local _disk="$3"
    local _disk_dev="$4"

    case "${_disk_dev}" in
        zvol)        ;&
        sparse-zvol) setvar "${_var}" "/dev/zvol/${VM_DS_ZFS_DATASET}/${_name}/${_disk}" ;;
        custom)      setvar "${_var}" "${_disk}" ;;
        iscsi)       info::__find_iscsi "${_var}" "${_disk}" ;;
        *)           setvar "${_var}" "${VM_DS_PATH}/${_name}/${_disk}" ;;
    esac
}

# looks for a prestart setting and runs it if possible
# this will execute before each start/reboot
#
# script is given the guest name and full path as arguments
#
vm::prestart(){
    local _script

    config::get "_script" "prestart" || return 0
    echo "${_script}" | grep -qs '^/'
    [ $? -ne 0 ] && _script="${VM_DS_PATH}/${_name}/${_script}"
    [ ! -x "${_script}" ] && return 1

    util::log "guest" "${_name}" "running prestart script ${_script} ${_name} ${VM_DS_ZFS_DATASET}/${_name}"

    (
      cd "${VM_DS_PATH}/${_name}"
      exec "${_script}" "${_name}" "${VM_DS_ZFS_DATASET}/${_name}"
    )
}

# get a list of all running virtual machines
# this loads a list of all running guests into global variables
# both variables are in the following format with one guest per line
# "pid guest"
#
# VM_RUN_BHYVE = all running bhyve instances
# VM_RUN_LOAD  = all loading guests (bhyveload|grub-bhyve)
#
# @modifies VM_RUN_BHYVE VM_RUN_LOAD
#
vm::running_load(){
    VM_RUN_BHYVE=$(pgrep -fl "bhyve:" | awk '{print $1,$NF}')
    VM_RUN_LOAD=$(pgrep -fl "grub-bhyve|bhyveload" | awk '{print $1,$NF}')
}

# see if a specific virtual machine is running
# we search the VM_RUN_BHYVE and VM_RUN_LOAD variables looking for the
# specified guest. If found we output "Running" or "Bootloader", along with
# the PID. If not running we output "Stopped"
#
# @param string _var variable to put result into
# @param string _name name of the guest to look for
# @return success if running
#
vm::running_check(){
    local _var="$1"
    local _var2="$2"
    local _name="$3"
    local IFS=$'\n'
    local _entry

    for _entry in ${VM_RUN_BHYVE}; do
        if [ "${_entry##* }" = "${_name}" ]; then
            setvar "${_var}" "Running (${_entry%% *})"
            setvar "${_var2}" "${_entry%% *}"
            return 0
        fi
    done

    for _entry in ${VM_RUN_LOAD}; do
        if [ "${_entry##* }" = "${_name}" ]; then
            setvar "${_var}" "Bootloader (${_entry%% *})"
            setvar "${_var2}" "${_entry%% *}"
            return 0
        fi
    done

    datastore::get_guest "${_name}"

    if vm::has_saved_state "${_name}"; then
        setvar "${_var}" "Suspended"
    else
        setvar "${_var}" "Stopped"
    fi

    return 1
}

# make sure a virtual machine is not running
# display warning if it is. up to caller to decide whether to continue.
# on unclean shutdown, lock files may not be cleared up by vm-bhyve,
# we now provide option to skip the lock file if it references this host,
# and there is no /dev/vmm/{_name}.
#
# @param string _name the guest name
# @param int _skip_lock skip local lock file if no /dev/vmm found
# @return success if not running
#
vm::confirm_stopped(){
    local _name="$1"
    local _skip_lock="$2"
    local _host _our_host

    _our_host=$(hostname)

    # check vm-bhyve lock
    # this will err even if guest is running on another node
    if [ -e "${VM_DS_PATH}/${_name}/run.lock" ]; then
        _host=$(head -n 1 "${VM_DS_PATH}/${_name}/run.lock")

        if [ "${_host}" != "${_our_host}" -o "${_skip_lock}" != "1" ]; then
            util::warn "${_name} appears to be running on ${_host} (locked)"
            return 2
        fi
    fi

    # check local machine, just in case guest run manually
    if [ -e "/dev/vmm/${_name}" ]; then
        util::warn "${_name} appears to be running locally (vmm exists)"
        return 1
    fi

    return 0
}

#
# check if the virtual machine has saved state
#
# @param string _name the guest name
# @return success if the guest has saved state
#
vm::has_saved_state(){
    local _name="$1"

    [ -e "${VM_DS_PATH}/${_name}/${_name}.save" ]
    return $?
}

# generate a static mac address for a guest.
# we now make sure all interfaces have a static mac so
# there's no worry of guest problems due to it changing.
# bhyve is allocated 58:9c:fc:0x:xx:xx, so we try and
# randomise the last 20 bits (5 hex digits).
# using guest name, interface number and time as seed,
# with an incrementing integer just in case we happen to
# get 5 zeros.
#
vm::generate_static_mac(){
    local _time=$(date +%s)
    local _key _part="0:00:00"
    local _base _int=0

    _base="${_name}:${_num}:${_time}"

    # generate the last 5 digits
    # make sure we don't get 0:00:00 (see sys/net/ieee_oui.h)
    while [ "${_part}" = "0:00:00" ]; do
        _key="${_base}:${_int}"
        _part=$(md5 -qs "${_key}" | awk 'BEGIN {FS="";OFS=":"}; {print $1,$2$3,$4$5}')
        _int=$((_int + 1))
    done

    # add base bhyve OUI
    _mac="58:9c:fc:0${_part}"

    util::log "guest" "${_name}" "generated static mac ${_mac} (based on '${_key}')"
    config::set "${_name}" "network${_num}_mac" "${_mac}"
}

# try to find an available network port to listen on
# this isn't clever enough to diffeential ports on unique ip's yet
#
# @param string _var variable to put port into
# @param int _curr the port to start searching from
# @return true if we found a port
#
vm::find_available_net_port(){
    local _var="$1"
    local _curr="$2"
    local _open _line _max _used
    local IFS=$'\n'

    # stop searching after 200 ports
    _max=$((_curr + 200))

    # get list of open ports
    _open=$(netstat -an | grep LISTEN | awk '{print $4}' | awk -F. '{print $NF}')

    while [ "${_curr}" -lt "${_max}" ]; do

        # reset used flag
        _used=""

        # see if current port is used
        for _line in ${_open}; do
            [ "${_line}" = "${_curr}" ] && _used="1"
        done

        # this port available?
        if [ -z "${_used}" ]; then
            setvar "${_var}" "${_curr}"
            return 0
        fi

        _curr=$((_curr + 1))
    done

    # not found a port
    setvar "${_var}" ""
    return 1
}

# remove any values from configuration that should be unique
# such as uuid and mac addresses. these should be generated
# automatically at runtime
#
# used for cloning or imaging existing guests
# should this remove devices like passthru that cannot be shared?
#
# @param string _name name of guest to generalise
#
vm::generalise(){
    local _name="$1"
    local _entry _num=0

    config::load "${VM_DS_PATH}/${_name}/${_name}.conf"
    config::remove "${_name}" "uuid"

    while true; do
        config::get "_entry" "network${_num}_mac"
        [ -z "${_entry}" ] && break

        config::remove "${_name}" "network${_num}_mac"
        _num=$((_num + 1))
    done
}

# get number of cpus from the configuration file, and also
# look for sockets/cores/threads options.
# returns the bhyve cpu config string
#
# @param string _var variable to put cpu configuration into
#
vm::__cpu(){
    local _var="$1"
    local _c_cpu _c_socket _c_core _c_thread
    local _config

    config::get "_c_cpu" "cpu"
    [ -n "${_c_cpu}" ] || util::err "fatal; unable to determine cpu count"
    _config="${_c_cpu}"

    config::get "_c_socket" "cpu_sockets"
    config::get "_c_core" "cpu_cores"
    config::get "_c_thread" "cpu_threads"

    [ -n "${_c_socket}" ] && _config="${_config},sockets=${_c_socket}"
    [ -n "${_c_core}" ] && _config="${_config},cores=${_c_core}"
    [ -n "${_c_thread}" ] && _config="${_config},threads=${_c_thread}"

    setvar "${_var}" "${_config}"
}
