# LBNL NHC - DMI/SMBIOS Checks
#
# Michael Jennings <mej@lbl.gov>
# 08 March 2013
#

DMI_HANDLES=( )
DMI_HANDLE_LOOKUP=( )
DMI_TYPE_IDS=( )
DMI_TYPE_STRINGS=( )
DMI_TYPE_HANDLES=( )
DMI_DATA=( )
DMI_RAW_DATA=( )

# Read hardware information from DMI via "dmidecode" command.
function nhc_dmi_gather_data() {
    local HANDLE TYPE STR WS WSCOUNT LEADER
    local IFS i j IN_DATA=0
    local -a TAG_STACK TMPDATA

    if [[ ${#DMI_RAW_DATA[*]} -eq 0 ]]; then
        nhc_dmi_gather_raw_data || return 1
    fi

    # Skip to the first line that starts with Handle
    for ((i=0; i<${#DMI_RAW_DATA[*]}; i++)); do
        if mcheck_glob "${DMI_RAW_DATA[$i]}" 'Handle *' ; then
            break
        fi
    done

    # Iterate through $DMI_RAW_DATA[] array to gather process data.
    for ((; i<${#DMI_RAW_DATA[*]}; i++)); do
        if mcheck_regexp "${DMI_RAW_DATA[$i]}" '^Handle ([A-Fa-fXx0-9]+), DMI type ([0-9]+), ' ; then
            # Before processing a new handle, we need to save the data from the old one, if any.
            if [[ -n "$HANDLE" ]]; then
                if [[ ${#TMPDATA[*]} > 0 ]]; then
                    IFS=$'\n'
                    DMI_DATA[$HANDLE]="${TMPDATA[*]}"
                    IFS=$' \t\n'
                    TMPDATA=( )
                else
                    DMI_DATA[$HANDLE]=""
                fi
            fi
            HANDLE="${BASH_REMATCH[1]}"
            TYPE="${BASH_REMATCH[2]}"
            TAG_STACK=( )
            # Save the handle information
            DMI_HANDLE_LOOKUP[$HANDLE]=${#DMI_HANDLES[*]}
            DMI_HANDLES[${#DMI_HANDLES[*]}]=$HANDLE
            DMI_TYPE_IDS[$HANDLE]=$TYPE
            DMI_TYPE_HANDLES[$TYPE]="${DMI_TYPE_HANDLES[$TYPE]} $HANDLE"
        else
            if mcheck_regexp "${DMI_RAW_DATA[$i]}" '^([[:space:]]+)([^[:space:]]+.*)$' ; then
                # Lines that start with whitespace are DMI table information.  Each tab
                # character must be expanded using the DMI type string and any previous
                # section headers under which the current line is indented.
                WS="${BASH_REMATCH[1]}"
                STR="${BASH_REMATCH[2]}"
                LEADER=""
                if [[ ${#WS} < ${#TAG_STACK[*]} ]]; then
                    # If the current line is indented less than the last line,
                    # erase any extra section tags from the stack.
                    for ((j=${#TAG_STACK[*]}-1; j>= ${#WS}; j--)) ; do
                        unset TAG_STACK[$j]
                    done
                fi
                if [[ ${#TAG_STACK[*]} > 0 ]]; then
                    # If we have anything left on the tag stack, prefix the string with it.
                    LEADER="${TAG_STACK[*]} "
                fi
                # Append the new string to the array of data strings.
                TMPDATA=( "${TMPDATA[@]}" "$LEADER$STR" )
            else
                # Lines that don't start with whitespace are DMI type strings.
                STR="${DMI_RAW_DATA[$i]}"
                DMI_TYPE_STRINGS[$HANDLE]="$STR"
            fi
            if [[ "${STR%:}" = "${STR}" ]]; then
                # Add a colon to the end in case there isn't one.
                STR="${STR}:"
            fi
            TAG_STACK=( "${TAG_STACK[@]}" "$STR" )
        fi
    done
    export DMI_HANDLES DMI_HANDLE_LOOKUP DMI_TYPE_IDS DMI_TYPE_STRINGS DMI_TYPE_HANDLES DMI_DATA
}

# Alternative format for dmidecode data.  This version just grabs the raw
# output from the dmidecode command without any fancy parsing or manipulation.
function nhc_dmi_gather_raw_data() {
    # Check to make sure we can actually run the "dmidecode" command.
    if ! type -t "dmidecode" >& /dev/null ; then
        DMI_RAW_DATA[0]=""
        return 1
    fi

    # Create array $DMI_RAW_DATA[] by splitting "dmidecode" output on newlines.
    IFS=$'\n'
    DMI_RAW_DATA=( $(dmidecode) )
    IFS=$' \t\n'

    export DMI_RAW_DATA
}


# Check the DMI info table for matching data.  Syntax is:
#  check_dmi_data_match [ -! | -n | ! ] [-h handle] [-t type] <string match>
function check_dmi_data_match() {
    local HANDLE TYPE OPTION i l
    local NEG=0 MATCH
    local -a HANDLES TMPDATA

    if [[ ${#DMI_HANDLES[*]} -eq 0 ]]; then
        nhc_dmi_gather_data
    fi

    OPTIND=1
    while getopts ":!h:nt:" OPTION ; do
        case "$OPTION" in
            !|n) NEG=1 ;;
            h) HANDLES=( $OPTARG ) ;;
            t) TYPE="$OPTARG" ;;
            :) die 1 "$FUNCNAME:  Option -$OPTARG requires an argument." ; return 1 ;;
            \?) die 1 "$FUNCNAME:  Invalid option:  -$OPTARG" ; return 1 ;;
        esac
    done
    shift $((OPTIND-1))

    if [[ "$1" == '!' ]]; then
        NEG=1
        shift
    fi
    MATCH="$*"

    # If we weren't given a set of handles, look at all handles
    # which are valid for the supplied type, if any.
    if [[ ${#HANDLES[*]} == 0 ]]; then
        if [[ -n "$TYPE" ]]; then
            case "$TYPE" in
                *[^0-9]*)
                    # Find all matching type strings.
                    for ((i=0; i<${#DMI_HANDLES[*]}; i++)) ; do
                        HANDLE=${DMI_HANDLES[$i]}
                        if [[ "${DMI_TYPE_STRINGS[$HANDLE]}" == "$TYPE" ]]; then
                            HANDLES[${#HANDLES[*]}]=$HANDLE
                        fi
                    done
                    ;;
                *)
                    # This case is easy since we can track this at parse time.
                    HANDLES=( ${DMI_TYPE_HANDLES[$TYPE]} )
                    ;;
            esac
        else
            # Check all handles.
            HANDLES=( "${DMI_HANDLES[@]}" )
        fi
    fi
    if [[ ${#HANDLES[*]} == 0 ]]; then
        # No handles are valid for the given parameters
        die 1 "$FUNCNAME:  No valid DMI handles found"
        return 1
    fi

    # Search all lines to find a match
    for ((i=0; i<${#HANDLES[*]}; i++)); do
        HANDLE=${HANDLES[$i]}
        if [[ -n "$TYPE" && "$TYPE" != "${DMI_TYPE_IDS[$HANDLE]}" && "$TYPE" != "${DMI_TYPE_STRINGS[$HANDLE]}" ]]; then
            continue
        fi
        IFS=$'\n'
        TMPDATA=( ${DMI_DATA[$HANDLE]} )
        IFS=$' \t\n'
        for ((j=0; j<${#TMPDATA[*]}; j++)) ; do
            if mcheck "${TMPDATA[$j]}" "$MATCH" ; then
                # If the match is negated, we're making sure a matching line *isn't* there.
                # Otherwise, we're making sure it *is* there.
                if [[ $NEG == 1 ]]; then
                    die 1 "$FUNCNAME:  Match found for $MATCH:  ${TMPDATA[$j]}"
                    return 1
                else
                    return 0
                fi
            fi
        done
    done

    # No matching line has been found.
    if [[ $NEG == 1 ]]; then
        return 0
    else
        die 1 "$FUNCNAME:  No match found for $MATCH"
        return 1
    fi
}

# "Grep" through the raw DMI data for a match to the supplied string.
function check_dmi_raw_data_match() {
    local NEG=0 MATCH

    if [[ "$1" == '!' ]]; then
        NEG=1
        shift
    fi
    MATCH="$*"
        
    if [[ ${#DMI_RAW_DATA[*]} -eq 0 ]]; then
        nhc_dmi_gather_raw_data
    fi

    # Search all lines to find a match
    for ((i=0; i<${#DMI_RAW_DATA[*]}; i++)); do
        if mcheck "${DMI_RAW_DATA[$i]}" "$MATCH" ; then
            # If the match is negated, we're making sure a matching line *isn't* there.
            # Otherwise, we're making sure it *is* there.
            if [[ $NEG == 1 ]]; then
                die 1 "$FUNCNAME:  Match found for $MATCH:  ${DMI_RAW_DATA[$i]}"
                return 1
            else
                return 0
            fi
        fi
    done

    # No matching line has been found.
    if [[ $NEG == 1 ]]; then
        return 0
    else
        die 1 "$FUNCNAME:  No match found for $MATCH"
        return 1
    fi
}
