#!/bin/sh #-------------------------------------------------------------------------+ # Copyright (C) 2016 Matt Churchyard (churchers@gmail.com) # All rights reserved # # Redistribution and use in source and binary forms, with or without # modification, are permitted providing that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # '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 _loader local _opts _devices _slot _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 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 "_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="-AHP" # 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" 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 util::log "guest" "${_name}" " [bhyve options: ${_opts}]" \ " [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 ${_opts} \ ${_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 # 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 util::log "guest" "${_name}" "stopped" [ -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="-Hwl 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}/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" } # 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 # 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 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 ${_slot}:${_func},ahci${_ahci}" _ahci="" _ahci_num=0 _add=1 fi ;; esac _atype="" else _devices="${_devices} -s ${_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 _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 } # 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 _taplist # vm::bhyve_device_networking(){ local _type _switch _mac _custom_tap _tap _sid _mtu local _num=0 _member_type _sw_type _iname while true; do config::get "_type" "network${_num}_type" [ -z "${_type}" ] && break config::get "_switch" "network${_num}_switch" config::get "_mac" "network${_num}_mac" config::get "_custom_tap" "network${_num}_device" config::get "_iname" "network${_num}_name" # set a static mac if we don't have one [ -z "${_mac}" ] && vm::generate_static_mac # move slot if we've hit function 8 if [ ${_func} -ge 8 ]; then _func=0 _slot=$((_slot + 1)) fi # get type of switch switch::type "_sw_type" "${_switch}" if [ "${_sw_type}" = "vale" ]; then # create a vale port id switch::vale::id "_tap" "${_switch}" "${_mac}" util::log "guest" "${_name}" "adding vale interface ${_tap} (${_switch})" _devices="${_devices} -s ${_slot}:${_func},${_type},${_tap}" [ -n "${_mac}" ] && _devices="${_devices},mac=${_mac}" _func=$((_func + 1)) else # create interface if [ -n "${_custom_tap}" ]; then _tap="${_custom_tap}" elif [ -n "${_iname}" ]; then _tap=$(ifconfig tap create name "${_iname}") else _tap=$(ifconfig tap create) fi # should this be a span member? _member_type="addm" config::yesno "network${_num}_span" && _member_type="span" if [ -n "${_tap}" ]; then util::log "guest" "${_name}" "initialising network device ${_tap}" ifconfig "${_tap}" descr "vmnet/${_name}/${_num}/${_switch:-custom}" group vm-port >/dev/null 2>&1 if [ -n "${_switch}" ]; then switch::id "_sid" "${_switch}" if [ -n "${_sid}" ]; then _mtu=$(ifconfig "${_sid}" | head -n1 | awk '{print $NF}') if [ "${_mtu}" != "1500" ]; then util::log "guest" "${_name}" "setting mtu of ${_tap} to ${_mtu}" ifconfig "${_tap}" mtu "${_mtu}" >/dev/null 2>&1 fi util::log "guest" "${_name}" "adding ${_tap} -> ${_sid} (${_switch} ${_member_type})" ifconfig "${_sid}" "${_member_type}" "${_tap}" >/dev/null 2>&1 || util::log "guest" "${_name}" "failed to add ${_tap} to ${_sid}" util::log "guest" "${_name}" "bring up ${_tap} -> ${_sid} (${_switch} ${_member_type})" ifconfig "${_tap}" up >/dev/null 2>&1 || util::log "guest" "${_name}" "failed to bring up ${_tap} in ${_sid}" # set private if configured switch::is_private "${_switch}" && ifconfig "${_sid}" "private" "${_tap}" >/dev/null 2>&1 else util::log "guest" "${_name}" "failed to find virtual switch '${_switch}'" fi fi _devices="${_devices} -s ${_slot}:${_func},${_type},${_tap}" [ -n "${_mac}" ] && _devices="${_devices},mac=${_mac}" _func=$((_func + 1)) [ -z "${_custom_tap}" ] && _taplist="${_taplist}${_taplist:+ }${_tap}" fi fi _num=$((_num + 1)) done if [ ${_num} -ge 1 ]; then _slot=$((_slot + 1)) _func=0 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 ${_slot}:0,virtio-rnd" _slot=$((_slot + 1)) 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 ${_slot}:0,fbuf,${_fbuf_conf}" _slot=$((_slot + 1)) } # 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 ${_slot}:0,xhci,tablet" _slot=$((_slot + 1)) fi } vm::bhyve_device_sound(){ local _play config::get "_play" "sound_play" "/dev/dsp0" if config::yesno "sound"; then _devices="${_devices} -s ${_slot}:0,hda,play=${_play}" _slot=$((_slot + 1)) 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 ${_slot}:0,virtio-console${_dev_str}" _slot=$((_slot + 1)) } # 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 _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 ${_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 _devices="${_devices} -s ${_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 } # 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 setvar "${_var}" "Stopped" 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 } # 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}" }