#!/bin/sh
#-------------------------------------------------------------------------+
# Copyright (C) 2015 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.

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

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

# 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
        [ $? -ne 0 ] && util::err "unable to load ${_mod}.ko!"
    fi
}

# check if system have bhyve support
# we look for POPCNT feature which is required
# and should be listed on both Intel & AMD systems
# 
# @modifies VM_NO_UG
#
util::check_bhyve_support(){
    local _mesg _mesg1 _mesg2 _result

    # basic checks
    [ `id -u` -ne 0 ] && util::err "virtual machines can only be managed by root"
    [ ${VERSION_BSD} -lt 1000000 ] && util::err "please upgrade to FreeBSD 10 or newer for bhyve support"

    # get features2 line which should include popcnt
    _mesg=$(grep -E '^[ ]+Features2' /var/run/dmesg.boot | tail -n 1)

    # only check if we found it
    if [ -n "${_mesg}" ]; then

        # look for pop cnt
        _result=$(echo "${_mesg}" |grep "POPCNT")
        [ -z "${_result}" ] && util::err "it doesn't look like your cpu supports bhyve (missing POPCNT)"
    fi

    # check ept for intel
    _mesg1=$(grep -E '^[ ]+VT-x' /var/run/dmesg.boot | tail -n 1)
    _mesg2=$(grep -E '^[ ]+Secondary Processor Controls' /var/run/dmesg.boot | tail -n 1)
    _mesg="${_mesg1}${_mesg2}"

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

        # look for ept
        _result=$(echo "${_mesg}" |grep "EPT")
        [ -z "${_result}" ] && util::err "it doesn't look like your cpu supports bhyve (missing EPT)"

        # look for unrestricted guest
        _result=$(echo "${_mesg}" |grep ",UG")
        [ -z "${_result}" ] && VM_NO_UG="1"
    fi
}

# check for vt-d support
# following neel@ wiki we search for DMAR acpi table
#
# @return success if host has vt-d
#
util::check_bhyve_iommu(){
    local _mesg

    _mesg=$(acpidump -t |grep DMAR)
    [ -z "${_mesg}" ] && return 1

    return 0
}

# 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} (build ${VERSION_INT})"
}

# show version & usage information
# we exit after running this
#
util::usage(){
    util::version
    cat << EOT
Usage: vm ...
    version
    init
    switch list
    switch info [name]
    switch create <name>
    switch import <name> <bridge>
    switch vlan <name> <vlan|0>
    switch nat <name> <on|off>
    switch add <name> <interface>
    switch remove <name> <interface>
    switch destroy <name>
    datastore list
    datastore add <name> <spec>
    datastore remove <name>
    list
    info [name]
    create [-d datastore] [-t template] [-s size] <name>
    [-f] install <name> <iso>
    [-f] start <name>
    stop <name> [...]
    console <name> [com1|com2]
    rename <name> <new-name>
    add [-d device] [-t type] [-s size|switch] <name>
    startall
    stopall
    reset <name>
    poweroff <name>
    configure <name>
    destroy <name>
    passthru
    clone <name[@snapshot]> <new-name>
    snapshot [-f] <name[@snapshot]>
    rollback [-r] <name@snapshot>
    iso [url]
    image list
    image create [-d description] [-u] <name>
    image destroy <uuid>
    image provision [-d datastore] <uuid> <newname>
EOT
    exit 1
}

# error
# display an error message and exit immediately
#
# @param string - the message to display
#
util::err(){
    echo "${0}: ERROR: $1" >&2
    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 "${_file}" | cut -d' ' -f8)

        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"
            _message="$3"
            _file="${VM_DS_PATH}/${_guest}/${_lf}"
            ;;
        system)
            _message="$2"
            _file="${vm_dir}/${_lf}"
            ;;
    esac

    echo "$(date +'%b %d %T'): ${_message}" >> "${_file}"
}

# 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::checkyesno(){
    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
}
