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

# make sure we have the right environment
#
util::setup(){
    util::load_module "nmdm"
    util::load_module "if_bridge"

    # tap(4) & tun(4) were unified in r347241, this is closest ABI bump
    if [ `uname -K` -ge 1300029 ]; then
        util::load_module "if_tuntap"
    else
        util::load_module "if_tap"
    fi

    sysctl net.link.tap.up_on_open=1 >/dev/null 2>&1

    # do we have the default template example, but no default in our .templates?
    # if so, get a copy, this at least allows a simple "vm create" to work out of the box
    if [ -e "/usr/local/share/examples/vm-bhyve/default.conf" -a ! -e "${vm_dir}/.templates/default.conf" ]; then
        cp "/usr/local/share/examples/vm-bhyve/default.conf" "${vm_dir}/.templates/" >/dev/null 2>&1
    fi
}

# load a kernel module
#
# @param string _mod the module name
#
util::load_module(){
    local _mod="$1"
    kldstat -qm ${_mod} >/dev/null 2>&1
    if [ $? -ne 0 ]; then
        kldload ${_mod} >/dev/null 2>&1
        [ $? -eq 0 ] || util::err "unable to load ${_mod}.ko!"
    fi
}

# load vmm kernel module
#
# `kldstat -n` calls kldstat(2), `kldstat -m` calls modstat(2).
# the former checks if the module is loaded, the latter checks if
# the module is initialized (ready to use).
#
# @return 0: success
#         1: failure
#         2: not supported
util::load_vmm(){
    # nothing to do if vmm module is already initialized
    if kldstat -qm vmm; then
        return 0
    fi

    # if loaded but not initialized, it means not supported
    if kldstat -qn vmm; then
        return 2
    fi

    # if not loaded, try to load
    if kldload -q vmm; then
        if kldstat -qm vmm; then
            return 0
        else
            # loaded but failed to initialize: not supported
            return 2
        fi
    else
        # failed to load
        return 1
    fi
}

# check if system have bhyve support
#
# vmm module seems to be working if modstat(2) succeeds
#
# the vm_disable_host_checks="yes" rc settings allows bypassing all this
# if your system should be supported but these checks break.
#
# @modifies VM_NO_UG
#
util::check_bhyve_support(){
    local _ret

    # almost all our functionality requires access to things only root can do
    [ `id -u` -ne 0 ] && util::err "virtual machines can only be managed by root"

    # don't check if user wants to bypass host checks
    util::yesno "$vm_disable_host_checks" && return 0

    util::load_vmm
    _ret=$?

    case "${_ret}" in
        0) ;; # success
        1) util::err "unable to load vmm.ko!" ;;
        2) util::err "virtualization is not supported or is disabled" ;;
    esac

    # check sysctls
    # these only work for intel
    # for AMD we give up trying to check for the time being
    sysctl hw.model |grep Intel >/dev/null 2>&1

    if [ $? -eq 0 ]; then
        [ "`sysctl -n hw.vmm.vmx.initialized 2>/dev/null`" != "1" ] && util::err "kernel vmm not initialised (no VT-x / AMD SVM cpu support?)"
        [ "`sysctl -n hw.vmm.vmx.cap.unrestricted_guest 2>/dev/null`" != "1" ] && VM_NO_UG="1"
    fi
}

# check for passthru support
# following neel@ wiki we search for DMAR acpi table for vt-d
# and we check sysctl if amdvi is present and enabled
#
# @return success if host has vt-d or amdvi
#
util::check_bhyve_iommu(){
    local _vtd
    local _amdvi

    # don't check if user wants to bypass host checks
    # think this check is fairly solid but there's probably someone somewhere
    # with iommu support that our tests fail for.
    util::yesno "$vm_disable_host_checks" && return 0

    _vtd=$(acpidump -t |grep DMAR)
    _amdvi=$(sysctl hw |grep 'vmm.amdvi.enable: 1')
    [ -z "${_vtd}" -a -z "${_amdvi}" ] && return 1

    return 0
}

# check if bhyve support snapshot/suspend
#
# @return success if bhyve is built with snapshot/suspend support
#
util::check_bhyve_suspend(){
    local _rc
    local _suspend_enaled

    # userland
    bhyvectl 2>&1 | grep -q -- "--suspend="
    _rc=$?
    if [ $_rc -gt 0 ]; then
        util::warn "bhyve on this host does not support suspend"
        return $_rc
    fi

    # kernel
    sysctl kern.conftxt | grep -q "BHYVE_SNAPSHOT"
    _rc=$?
    if [ $_rc -gt 0 ]; then
        util::warn "bhyve on this host does not support suspend"
        return $_rc
    fi

    config::core::get "_suspend_enabled" "suspend" "no"
    if ! util::yesno "${_suspend_enabled}"; then
        util::warn "bhyve seems to support suspend but disabled in vm-bhyve config"
        return 1
    fi

    return $_rc
}

# restart a local service
# checks if service is running and either starts or restarts
#
# @param string _serv the name of the service
#
util::restart_service(){
    local _serv="$1"
    local _cmd="restart"

    # see if it's actually running
    service ${_serv} status >/dev/null 2>&1
    [ $? -ne 0 ] && _cmd="start"

    service ${_serv} ${_cmd} >/dev/null 2>&1
    [ $? -ne 0 ] && util::warn "failed to ${_cmd} service ${_serv}"
}

# show version
#
util::version(){
    echo "vm-bhyve: Bhyve virtual machine management v${VERSION}"
}

# show version & usage information
# we exit after running this
#
util::usage(){
    util::version
    cat << EOT

Available subcommands grouped by scenario:

Help / Information
  usage         Display this help
  help          Display help about any subcommand
  version       Display version information about vm-bhyve

Host and Infrastructure Management
  init          Initialize the host; should be run after each reboot
  switch        Manage virtual network switches
  datastore     Manage datastores
  get           Get all or specified global vm-bhyve configuration(s)
  set           Set global vm-bhyve configuration(s)
  passthru      List all passthru devices and their device IDs

VM Management
  list          Display list of guests
  info          Display detailed info about all or specified guest(s)
  create        Create a new guest
  rename        Rename a guest
  clone         Clone an existing guest
  destroy       Remove a guest from the host
  add           Add a new network or a disk to a guest
  configure     Open the editor to modify a guest configuration
  edit          Alias to configure
  image         Manage images created from provisioned guests
  console       Open a console to a guest
  migrate       Transfer a guest from one to another using SSH
  tags          Manage tags on a guest

VM Start/Stop Operations
  install       Start a guest from a specified ISO for installation
  start         Start specified guest(s)
  stop          Stop specified running guest(s)
  restart       Restart a running guest
  suspend       Stop specified guest(s), while retaining their state
  discard       Discard the suspended state of a guest
  startall      Start all guests
  stopall       Stop all guests
  suspendall    Suspend all guests
  reset         Reset a guest forcibly
  poweroff      Power off a guest forcibly

Snapshot Operations
  snapshot      Take a snapshot of a guest
  rollback      Roll back a guest to a snapshot

Image Management
  iso           List stored ISOs or download one
  img           List stored VM images or download one
EOT
    exit 1
}

# err
# display an error message and exit immediately
#
# @param string - the message to display
#
util::err(){
    echo "${0}: ERROR: $1" >&2
    exit 1
}

# err_inline
# display an error inline with informational output
#
# @param string - message to display
#
util::err_inline(){
    echo "  ! $1"
    exit 1
}

# warn
# display warning, but do not exit
#
# @param string - the message to display
#
util::warn(){
    echo "${0}: WARNING: $1" >&2
}

# log_rotate
# simple rotation of log files
# if we hit 1MB, which should cover a fair amount of history,
# we move existing log and and create a new one.
# one keep 1 previous file, as that should be enough
#
# @param string _type whether to rotate guest or main log
#
util::log_rotate(){
    local _type="$1"
    local _lf="vm-bhyve.log"
    local _file _size _guest

    case "${_type}" in
        guest)
            _guest="$2"
            _file="${VM_DS_PATH}/${_guest}/${_lf}"
            ;;
        system)
            _file="${vm_dir}/${_lf}"
            ;;
    esac

    if [ -e "${_file}" ]; then
        _size=$(stat -f %z "${_file}")

        if [ -n "${_size}" -a "${_size}" -ge 1048576 ]; then
            unlink "${_file}.0.gz" >/dev/null 2>&1
            mv "${_file}" "${_file}.0"
            gzip "${_file}.0"
        fi
    fi
}

# log to file
# writes the date and a message to the specified log
# the global log is in $vm_dir/vm-bhyve.log
# guest logs are $vm_dir/{guest}/vm-bhyve.log
#
# @param string _type=guest|system log to global vm-bhyve log or guest
# @param optional string _guest if _type=guest, the guest name, otherwise do not provide at all
# @param string _message the message to log
#
util::log(){
    local _type="$1"
    local _lf="vm-bhyve.log"
    local _guest _message _file _date

    case "${_type}" in
        guest)
            _guest="$2"
            _file="${VM_DS_PATH}/${_guest}/${_lf}"
            shift 2
            ;;
        system)
            _file="${vm_dir}/${_lf}"
            shift 1
            ;;
    esac

    while [ -n "$1" ]; do
      echo "$(date -Iseconds): $1" >> "${_file}"
      shift
    done
}

# write content to a file, and log what we
# did to the guest log file
# it's useful to be able to see what files vm-bhyve is creating
# and the contents so we write that to the log.
# The file is created in $vm_dir/{guest}
#
# @param string _type=write|appnd create file or append to it
# @param string _guest the guest name
# @param string _file the file name to write to
# @param string _message the data to write
#
util::log_and_write(){
    local _type="$1"
    local _guest="$2"
    local _file="${VM_DS_PATH}/${_guest}/$3"
    local _message="$4"

    if [ "${_type}" = "write" ]; then
        util::log "guest" "${_guest}" "create file ${_file}"
        echo "${_message}" > "${_file}"
    else
        echo "${_message}" >> "${_file}"
    fi

    util::log "guest" "${_guest}" " -> ${_message}"
}

# confirm yes or no
#
# @param string _msh message to display
# @return int success if confirmed
#
util::confirm(){
    local _msg="$1"
    local _resp

    while read -p "${_msg} (y/n)? " _resp; do
        case "${_resp}" in
            y*) return 0 ;;
            n*) return 1 ;;
        esac
    done
}

# our own checkyesno copy
# doesn't warn for unsupported values
# also returns as 'yes' unless value is specifically no/off/false/0
#
# @param _value the value to test
# @return int 1 if set to "off/false/no/0", 0 otherwise
#
util::yesno(){
    local _value="$1"

    [ -z "${_value}" ] && return 1

    case "$_value" in
        [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0)
            return 1 ;;
        *)  return 0 ;;
    esac
}

# 'vm check name'
# check name of virtual machine
#
# @param _name name to check
# @param _maxlen=30(229 on 13+) maximum name length (NOTE should be 2 less than desired)
# @return int 0 if name is valid
#
util::check_name(){
    local _name="$1"
    local _maxlen="$2"

    if [ -z "${_maxlen}" ]; then
        : ${_maxlen:=229}
    fi

    echo "${_name}" | egrep -iqs "^[a-z0-9][.a-z0-9_-]{0,${_maxlen}}[a-z0-9]\$"
}

# check if the specified string is a valid core configuration
# setting that the user can change
#
# @param string the setting name to look for
#
util::valid_config_setting(){
    echo "${VM_CONFIG_USER}" | grep -iqs "${1};"
}

# __getpid
# get a process id
#
# @param string _var variable to put pid into
# @param string _proc process to look for
#
util::getpid(){
    local _var="$1"
    local _proc="$2"
    local _ret

    _ret=$(pgrep -f "${_proc}")
    [ $? -eq 0 ] || return 1
    setvar "${_var}" "${_ret}"
}

util::get_part(){
    local _var="$1"
    local _data="$2"
    local _num="$3"

    setvar "${_var}" $(echo "${_data}" |cut -w -f${_num})
}

#
# pad_spaces
# print spaces corresponding to the length of the given string
#
# @param string
#
util::pad_spaces(){
    printf "%*s" "${#1}" ""
}

#
# validate_cidr
# validate the provided CIDR notation
# if GNU ipcalc is available, perform stricter validation
# otherwise, perform a simple check using regex
#
# @param string IPv4 or IPv6 CIDR expression
#
util::validate_cidr(){
    local _has_ipcalc

    # perform stricter check if GNU ipcalc is available (optional)
    which ipcalc >/dev/null && pkg info -q gnu-ipcalc
    _has_ipcalc=$?

    # ipcalc cannot check if IP address contains netmask (/prefix)
    # so check it using case glob
    case "$1" in
        */[0-9]*)
            if [ "$_has_ipcalc" -eq 0 ]; then
                ipcalc -sc "$1"
            else
                echo "$1" | egrep -iqs \
                    -e '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$' \
                    -e '^([0-9a-f]{0,4}:){1,7}[0-9a-f]{0,4}/[0-9]{1,3}$'
            fi
            ;;
        *) # no netmask
            return 1 ;;
    esac
}
