SUSE/saptune

View on GitHub
ospackage/bin/saptune_check

Summary

Maintainability
Test Coverage
#!/bin/bash
# ------------------------------------------------------------------------------
# Copyright (c) 2019 SUSE LLC
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of version 3 of the GNU General Public License as published by the
# Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, contact SUSE Linux GmbH.
#
# ------------------------------------------------------------------------------
# Author: Sören Schmidt <soeren.schmidt@suse.com>
#
# This tool checks if saptune is set up correctly. 
# It will not dig deeper to check if the tuning itself is working.
#
# exit codes:       0   All checks ok. Saptune have been set up correctly.
#                   1   Some warnings occurred. Saptune should work, but better check manually.
#                   2   Some errors occurred. Saptune will not work.
#                   3   Wrong parameters given to the tool on commandline.
#                   5   Error in JSON string (bug in program!)
#
# Changelog:
#
# 08.01.2021  v0.1      First release. (Split of sapconf_saptune_check v1.2.1)
# 19.08.2021  v0.2      supports (only) saptune v3
#                       tests system status and lists failed services
# 26.08.2021  v0.2.1    added missing os_release to global arrays
# 09.11.2021  v0.2.2    degraded system is no longer considered an error
# 26.09.2022  v0.2.3    degraded systemd state gets explained in more detail (TEAM-6584)
# 26.09.2022  v0.3      reactivate unused function compile_filelists() as file_check() and add detection of sapconf remains (TEAM-6275)
# 03.04.2023  v0.3.1    removed error regarding tuned sapconf profile (TEAM-7529)
# 28.08.2024  v0.4      a little rework and added JSON support (TEAM-8959)
# 31.10.2024  v0.4.1    added --force-color to support forced colored output by saptune

declare -r VERSION="0.4.1"

# We use these global arrays through out the program:
#
# package_version     -  contains package version (string)
# os_version          -  contains os version and service pack
# system_status       -  contains system status and failed units 
# unit_state_active   -  contains systemd unit state (systemctl is-active) 
# unit_state_enabled  -  contains systemd unit state (systemctl is-enabled) 
# tool_profile        -  contains actual profile (string) for each tool
declare -A package_version os_version system_status unit_state_active unit_state_enabled tool_profile

color=1     # we like it colorful

function header() {
    [ ${DO_JSON} -ne 0 ] && return 
    local len=${#1}
    echo -e "\n${1}"
    printf '=%.s' $(eval "echo {1.."$((${len}))"}")
    echo
}

function print_ok() {
    [ ${DO_JSON} -ne 0 ] && return
    local col_on col_off
    [ -t 1 ] || color=0  # Disable color if we run in a pipe
    [ ${FORCE_COLOR} -eq 1 ] && color=1  # force colored output
    if [ ${color} -eq 1 ] ; then
        col_on="\033[0;32m"
        col_off="\033[0m"
    else
        col_on=""
        col_off=""
    fi
    echo -e "[ ${col_on}OK${col_off} ] ${1}"
}

function print_fail() {
    [ ${DO_JSON} -ne 0 ] && return
    local col_on col_off bold_on
    [ -t 1 ] || color=0  # Disable color if we run in a pipe
    [ ${FORCE_COLOR} -eq 1 ] && color=1  # force colored output
    if [ ${color} -eq 1 ] ; then
        col_on="\033[0;31m"
        col_off="\033[0m"
        bold_on="\033[1m"
    else
        col_on=""
        col_off=""
        bold_on=""
    fi
    echo -e "[${col_on}FAIL${col_off}] ${1}${bold_on}\t-> ${2}${col_off}"
}

function print_warn() {
    [ ${DO_JSON} -ne 0 ] && return
    local col_on col_off bold_on
    [ -t 1 ] || color=0  # Disable color if we run in a pipe
    [ ${FORCE_COLOR} -eq 1 ] && color=1  # force colored output
    if [ ${color} -eq 1 ] ; then
        col_on="\033[0;33m"
        col_off="\033[0m"
        bold_on="\033[1m"
    else
        col_on=""
        col_off=""
        bold_on=""
    fi
    echo -e "[${col_on}WARN${col_off}] ${1}${bold_on}\t-> ${2}${col_off}"
}

function print_note() {
    [ ${DO_JSON} -ne 0 ] && return
    local col_on col_off
    [ -t 1 ] || color=0  # Disable color if we run in a pipe
    [ ${FORCE_COLOR} -eq 1 ] && color=1  # force colored output
    if [ ${color} -eq 1 ] ; then
        col_on="\033[0;37m"
        col_off="\033[0m"
    else
        col_on=""
        col_off=""
    fi
    echo -e "[${col_on}NOTE${col_off}] ${1}"
}

function _json_must_be() {
    # Params:       OBJECT_TYPE...
    # Output:       -
    # Returncode:   -
    #
    # Checks if one of the given object types is the last character
    # of_JSON_NESTING. Terminates with exit code 5 an an error nmessage
    # if not, otherwise returns.
    #
    # Requires:     _JSON_NESTING

    local current_object="${_JSON_NESTING: -1:1}"
    local object_type

    for object_type in ${@} ; do
        [ "${object_type}" == "${current_object}" ] && return
    done
    echo "Current object type \"${current_object}\" is not in the allowed types: ${@}" >&2
    exit 5
}

function _json_must_be_number() {
    # Params:       VALUE
    # Output:       -
    # Returncode:   -
    #
    # Checks if one of the given parameter is a number.
    # Terminates with exit code 5 if not, otherwise returns.
    #
    # Requires:    -

    if [[ ! ${1} =~ ^[+-]?([0-9]+([.][0-9]*)?|\.[0-9]+)$ ]] ; then
        echo "JSON value is not a number: ${1}" >&2
        exit 5
    fi
}

function _json_must_be_bool() {
    # Params:       VALUE
    # Output:       -
    # Returncode:   -
    #
    # Checks if one of the given parameter is a 'true'
    # or 'false'.
    # Terminates with exit code 5 if not, otherwise returns.
    #
    # Requires:    -

    if [[ ! "${1}" =~ ^(true|false)$ ]] ; then
        echo "JSON value is no neither 'true' nor 'false: ${1}" >&2
        exit 5
    fi
}            

function _json_key_must_exist() {
    # Params:       ARG...
    # Output:       -
    # Returncode:   -
    #
    # Checks if the second parameter exists.
    # The function normaly will be called by `add2json` with
    # its arguments (`add2json REG_TYPE [KEY [VALUE]]`).
    # Terminates with exit code 5 if not, otherwise returns.
    #
    # Requires:    -

    if [ -z "${2}" ] ; then
        echo "JSON key is missing! Command: ${*}" >&2
        exit 5
    fi
}

function _json_value_must_exist() {
    # Params:       ARG...
    # Output:       -
    # Returncode:   -
    #
    # Checks if the third parameter exists.
    # The function normaly will be called by `add2json` with
    # its arguments (`add2json REG_TYPE [KEY [VALUE]]`).
    # Terminates with exit code 5 if not, otherwise returns.
    #
    # Requires:    -

    if [ -z "${3}" ] ; then
        echo "JSON value is missing! Command: ${*}" >&2
        exit 5
    fi
}

function _json_nesting_add() {
    # Params:       DATATYPE
    # Output:       -
    # Returncode:   -
    #
    # Appends the argument (typically D or L) to _JSON_NESTING.
    #
    # Requires:    _JSON_NESTING
    
    _JSON_NESTING="${_JSON_NESTING}${1}"
}

function _json_nesting_pop() {
    # Params:       -
    # Output:       -
    # Returncode:   -
    #
    # Removes the last character from _JSON_NESTING.
    #
    # Requires:    _JSON_NESTING

    _JSON_NESTING="${_JSON_NESTING:0: -1}"
}

function start_json() {
    # Params:       -
    # Output:       -
    # Returncode:   -
    #
    # Initializes JSON_STRING and _JSON_NESTING to start
    # a new JSON string.
    # JSON_STRING starts with a { for a new dict.
    # If DO_JSON is 0, the function returns immediately.
    #
    # Requires:    DO_JSON, JSON_STRING, _JSON_NESTING 

    [ ${DO_JSON} -eq 0 ] && return
    shopt -s extglob
    JSON_STRING='{'
    _JSON_NESTING='D'
}

function end_json() {
    # Params:       -
    # Output:       -
    # Returncode:   -
    #
    # Finalizes JSON_STRING and _JSON_NESTING to finish
    # a JSON string or terminates with exit code 5 if 
    # there is a problem.
    # JSON_STRING ends with a } to close the overall dict.
    # If DO_JSON is 0, the function returns immediately.
    #
    # Requires:    DO_JSON, JSON_STRING, _JSON_NESTING 

    [ ${DO_JSON} -eq 0 ] && return
    if [ "${_JSON_NESTING}" != 'D' ] ; then 
        echo "Cannot terminate JSON string with nesting: ${_JSON_NESTING}" >&2
        exit 5
    fi
    JSON_STRING="${JSON_STRING%,*( )}}"
    _JSON_NESTING='E'
}

function print_json() {
    # Params:       -
    # Output:       JSON_STRING
    # Returncode:   -
    #
    # Prints JSON_STRING to stdout or terminates with 
    # exit code 5 if there is a problem.
    # If DO_JSON is 0, the function returns immediately.
    #
    # Requires:    DO_JSON, JSON_STRING, _JSON_NESTING 

    [ ${DO_JSON} -eq 0 ] && return
    if [ "${_JSON_NESTING}" != 'E' ] ; then 
        echo "Cannot print incomplete JSON string with nesting: ${_JSON_NESTING}" >&2
        exit 5
    fi
    echo "${JSON_STRING}"
}

function add2json() {
    # Params:       REQ_TYPE [KEY [VALUE]]
    # Output:       -
    # Returncode:   -
    #
    # Extends JSON_STRING with the given entry and 
    # updates _JSON_NESTING accordingly. 
    # Terminates with exit code 5 if there is a problem.
    # If DO_JSON is 0, the function returns immediately.
    #
    # Implemented commands are:
    #   add2json dict_start NAME 
    #   add2json dict_end
    #   add2json list_start NAME 
    #   add2json list_entry_string VALUE
    #   add2json list_entry_number VALUE
    #   add2json list_entry_bool true|false
    #   add2json list_entry_null
    #   add2json list_entry_dict_start
    #   add2json list_entry_dict_end
    #   add2json list_end
    #   add2json string NAME VALUE 
    #   add2json number NAME VALUE
    #   add2json bool NAME  true|false
    #   add2json nul NAME
    #
    # Requires:    DO_JSON, JSON_STRING, _JSON_NESTING 
    
    [ ${DO_JSON} -eq 0 ] && return

    local req_type="${1}"
    local key="${2}"
    local value="${3}"

    # Escape quotes in keys and values.
    key="${key//\"/\\\"}" 
    value="${value//\"/\\\"}" 

    # Act on the request type.
    case "${req_type}" in 

        dict_start)
            _json_must_be L D
            _json_key_must_exist "$@"
            JSON_STRING="${JSON_STRING}\"${key}\":{"
            _json_nesting_add D
            ;;

        dict_end)
            _json_must_be D
            JSON_STRING="${JSON_STRING%,*( )}},"
            _json_nesting_pop
            ;;

        list_start)
            _json_must_be L D
            _json_key_must_exist "$@"
            JSON_STRING="${JSON_STRING}\"${key}\":["
            _json_nesting_add L
            ;;

        list_entry_string)
            _json_must_be L
            _json_key_must_exist "$@"   # key is really a value (list entry)
            JSON_STRING="${JSON_STRING}\"${key}\", "
            ;;

        list_entry_number)
            _json_must_be L
            _json_key_must_exist "$@"   # key is really a value (list entry)
            _json_must_be_number ${key}   # key is really a value (list entry)
            JSON_STRING="${JSON_STRING}${key}, "
            ;;

        list_entry_bool)
            _json_must_be L
            _json_key_must_exist "$@"   # key is really a value (list entry)
            _json_must_be_bool ${key}   # key is really a value (list entry)
            JSON_STRING="${JSON_STRING}${key}, "
            ;;

        list_entry_null)
            _json_must_be L
            JSON_STRING="${JSON_STRING}null, "
            ;;

        list_entry_dict_start)
            _json_must_be L
            JSON_STRING="${JSON_STRING}{"
            _json_nesting_add D
            ;;
        
        list_entry_dict_end)
            _json_must_be D
            JSON_STRING="${JSON_STRING%,*( )}},"
            _json_nesting_pop
            ;;

        list_end)
            _json_must_be L
            JSON_STRING="${JSON_STRING%,*( )}],"
            _json_nesting_pop
            ;;
        
        string)
            _json_must_be D
            _json_key_must_exist "$@"
            _json_value_must_exist "$@"
            JSON_STRING="${JSON_STRING}\"${key}\":\"${value}\", "
            ;;

        number)
            _json_must_be D
            _json_key_must_exist "$@"
            _json_value_must_exist "$@"
            _json_must_be_number ${value}
            JSON_STRING="${JSON_STRING}\"${key}\":${value}, "
            ;;

        bool)
            _json_must_be D
            _json_key_must_exist "$@"
            _json_must_be_bool ${value}
            JSON_STRING="${JSON_STRING}\"${key}\":${value}, "
            ;;

        null)
            _json_must_be D
            _json_key_must_exist "$@"
            JSON_STRING="${JSON_STRING}\"${key}\":null, "
            ;;

        *)
            echo "Unknown request type: ${req_type}" >&2
            exit 5
            ;;
    esac
}

# MARKED FOR REMOVAL
function add_msg_json() {
    # Params:       -
    # Output:       -
    # Returncode:   -
    #
    # Adds a note, warning or fail message to the arrays
    # JSON_NOTE_MESSAGES, JSON_WARN_MESSAGES, JSON_FAIL_MESSAGES
    # or JSON_REMEDIATION_MESSAGES which are added to JSON_STRING
    # by update_messages_json().
    # Terminates with exit code 5 if there is a problem.
    # If DO_JSON is 0, the function returns immediately.
    #
    # Requires:    JSON_NOTE_MESSAGES JSON_WARN_MESSAGES 
    #              JSON_FAIL_MESSAGES JSON_REMEDIATION_MESSAGES

    [ ${DO_JSON} -eq 0 ] && return

    case "${1}" in 
        note)
            JSON_NOTE_MESSAGES+=("${2}")
            ;;
        warn)
            JSON_WARN_MESSAGES+=("${2}")
            ;;
        fail)
            JSON_FAIL_MESSAGES+=("${2}")
            ;;
        remediation)
            JSON_REMEDIATION_MESSAGES+=("${2}")
            ;;  
        *)
            echo "Unknown JSON message type: ${1}" >&2
            exit 5
            ;;
    esac
}

# MARKED FOR REMOVAL
function update_messages_json() {
    # Params:       -
    # Output:       -
    # Returncode:   -
    #
    # Adds a 'messages' dictionary with lists for 'notes'
    # (JSON_NOTE_MESSAGES), 'warnings' (JSON_WARN_MESSAGES)
    # 'remediations' (JSON_REMEDIATION_MESSAGES) and 'errors'
    # (JSON_FAIL_MESSAGES) or terminates with exit code 5 if
    # there is a problem.
    # If DO_JSON is 0, the function returns immediately.
    #
    # Requires:    DO_JSON, JSON_STRING, _JSON_NESTING 
    #              JSON_NOTE_MESSAGES JSON_WARN_MESSAGES 
    #              JSON_FAIL_MESSAGES JSON_REMEDIATION_MESSAGES

    [ ${DO_JSON} -eq 0 ] && return

    add2json dict_start 'messages'
    add2json list_start 'notes'
    for msg in "${JSON_NOTE_MESSAGES[@]}" ; do
        add2json list_entry_string "${msg}"
    done 
    add2json list_end
    add2json list_start 'warnings'
    for msg in "${JSON_WARN_MESSAGES[@]}" ; do
        add2json list_entry_string "${msg}"
    done 
    add2json list_end
    add2json list_start 'errors'
    for msg in "${JSON_FAIL_MESSAGES[@]}" ; do
        add2json list_entry_string "${msg}"
    done 
    add2json list_end
    add2json list_start 'remediations'
    for msg in "${JSON_REMEDIATION_MESSAGES[@]}" ; do
        add2json list_entry_string "${msg}"
    done 
    add2json list_end
    add2json dict_end
}

function add_message_json() {
    # Params:       TYPE TEXT REMEDIATION
    # Output:       -
    # Returncode:   -
    #
    # Adds dictionary to a list ()'messages'), which must 
    # be present, with the elements 'type' (OK|WARN|FAIL|NOTE),
    # 'text' and 'remediation' (optional) or terminates with
    #  exit code 5 if there is a problem.
    # The remediation is left out, except the type is FAIL or
    # WARN.
    # If DO_JSON is 0, the function returns immediately.
    #
    # Requires:    DO_JSON, JSON_STRING, _JSON_NESTING 

    [ ${DO_JSON} -eq 0 ] && return

    local parameter_failure=0
    [ $# -lt 2 -o $# -gt 3 ] &&  parameter_failure=1
    [[ "${1}" =~ ^(OK|WARN|FAIL|NOTE)$ ]] || parameter_failure=2
    [ "${1}" == 'FAIL' -a $# -ne 3 ] &&  parameter_failure=3
    [ "${1}" == 'WARN' -a $# -ne 3 ] &&  parameter_failure=3
    if [ ${parameter_failure} -ne 0 ] ; then 
        echo "${FUNCNAME@Q} wrongly called (code=${parameter_failure}) with: ${@@Q} " >&2
        exit 5
    fi

    add2json list_entry_dict_start
    add2json string 'type' "${1}"
    add2json string 'text' "${2}"
    [ -n "${3:-}" ] && add2json string 'remediation' "${3}"
    add2json list_entry_dict_end

}

function is_in() {
    # Params:       NAME LIST
    # Output:       -
    # Returncode:   0 (true), 1 (false)
    #
    # Checks if NAME is in the space-separated string LIST
    # and returns 0 (true) or 1 (false).
    #
    # Requires:     -

    local name="${1}"
    local list=( ${2} )

    for elem in "${list[@]}" ; do
        if [ "${name}" == "${elem}" ] ; then
            return 1
        fi
    done
    return 1
}

function get_os_version() {
    # Params:   -
    # Output:   -
    # Exitcode: -
    #
    # Determines the OS version as string for each PACKAGE.
    #
    # The function updates the associative array "os_version".
    #
    # Requires:-

    local VERSION_ID
    
    eval "$(grep ^VERSION_ID= /etc/os-release)"
    os_version['release']="${VERSION_ID%.*}"
    os_version['servicepack']="${VERSION_ID#*.}"
}

function get_package_versions() {
    # Params:   PACKAGE...
    # Output:   -
    # Exitcode: -
    #
    # Determines package version as string for each PACKAGE.
    # Not installed packages will have an empty string as version.
    #
    # The function updates the associative array "package_version".
    #
    # Requires:-

    local package version
    for package in "${@}" ; do
        if version=$(rpm -q --qf '%{version}' "${package}" 2>&1) ; then
            package_version["${package}"]=${version}
        else
            package_version["${package}"]=''
        fi
    done
}

function get_system_status() {
    # Params:   -
    # Output:   -
    # Exitcode: -
    #
    # Collect data about system status and failed services.
    #
    # The function updates the associative arrays "system_status".
    #
    # Requires: -
    
    system_status["status"]=$(systemctl is-system-running 2> /dev/null)
    system_status["failed_units"]=$(systemctl list-units --state=failed --plain --no-legend --no-pager | cut -d ' ' -f 1 | tr '\n' ' ' 2> /dev/null)
}

function get_unit_states() {
    # Params:   UNIT...
    # Output:   -
    # Exitcode: -
    #
    # Determines the state (is-active/is-enabled) for each UNIT.
    # A missing state is reported as "missing".
    #
    # The function updates the associative arrays "unit_state_active" and "unit_state_enabled".
    #
    # Requires: -

    local unit state_active state_enabled
    for unit in "${@}" ; do
        state_active=$(systemctl is-active "${unit}" 2> /dev/null)
        state_enabled=$(systemctl is-enabled "${unit}" 2> /dev/null)
        unit_state_active["${unit}"]=${state_active:-missing}
        unit_state_enabled["${unit}"]=${state_enabled:-missing}
    done
}

function get_tool_profiles() {
    # Params:   -
    # Output:   -
    # Exitcode: -
    #
    # Determines the current profile of tuned and saptune (profile==Notes/Solution). 
    # A missing profile (file) is reported as "missing".
    #
    # The function updates the associative array "tool_profile".
    #
    # Requires: -

    local active_profile TUNE_FOR_NOTES TUNE_FOR_SOLUTIONS
    active_profile=''
    [ -e /etc/tuned/active_profile ] && active_profile=$(< /etc/tuned/active_profile)
    tool_profile['tuned']="${active_profile:-missing}"

    if [ -e /etc/sysconfig/saptune ] ; then
        eval $(grep ^TUNE_FOR_NOTES= /etc/sysconfig/saptune)
        eval $(grep ^TUNE_FOR_SOLUTIONS= /etc/sysconfig/saptune)
        if [ -z "${TUNE_FOR_NOTES}" -a -z "${TUNE_FOR_SOLUTIONS}" ] ; then
            tool_profile['saptune']='missing'    
        else
            tool_profile['saptune']="solutions: ${TUNE_FOR_SOLUTIONS:=-} notes: ${TUNE_FOR_NOTES:=-}"
        fi
    else
        tool_profile['saptune']='missing'    
    fi
}

function configured_saptune_version() {
    # Params:   -
    # Output:   -
    # Exitcode: -
    #
    # Checks the configured saptune version. 
    # A missing saptune is reported as "missing".
    #
    # The function updates the variable "configured_saptune_version".
    #
    # Requires: -

    local SAPTUNE_VERSION
    [ -e /etc/sysconfig/saptune ] && eval $(grep ^SAPTUNE_VERSION= /etc/sysconfig/saptune)
    configured_saptune_version="${SAPTUNE_VERSION:-missing}"
}

function collect_data() {
    # Params:   -
    # Output:   -
    # Exitcode: -
    #
    # Calls various functions to collect data.
    #
    # Requires: get_os_version()
    #           get_package_versions()
    #           get_system_status()
    #           get_unit_states()
    #           get_tool_profiles()
    #           configured_saptune_version()

    # Collect OS version.
    get_os_version

    # Collect data about some packages.
    get_package_versions sapconf saptune tuned

    # Collect data about system status and failed services.
    get_system_status

    # Collect data about some systemd services.
    get_unit_states sapconf.service tuned.service saptune.service

    # Collect the profiles of various tools.
    get_tool_profiles

    # Get configured saptune version.
    configured_saptune_version
}

function file_check() {
    # Params:   VERSIONTAG
    # Output:   warnings, fails and notes with print_warn(), print_fail() and print_note()
    # Exitcode: -
    #
    # Checks the existence of mandatory and invalid files for sapconf and saptune 
    # (depending on SLES release and VERSIONTAG) and prints warnings or fails.
    #
    # The following strings for VERSIONTAG are allowed: "saptune-3"
    #
    # Also for all mandatory and invalid files, we search for RPM leftovers (.rpmnew/.rpmsave). 
    #
    # IMPORTANT:
    #   When adding new files every file must be listed in either of the arrays mandatory_files"
    #   or "invalid_files" but in *each* SLES release and tag section!
    #
    # The function updates the variables "warnings" and "fails" used in saptune_check(). 
    #
    # Requires: print_warn(), print_fail(), print_note() and add_message_json()

    local VERSION_ID tag="${1}" mandatory_files invalid_files rpm_leftovers sapconf_leftovers critical_sapconf_leftovers
    declare -a mandatory_files invalid_files rpm_leftovers

    eval $(grep ^VERSION_ID= /etc/os-release)
    case ${VERSION_ID} in 
        12*)
            case ${tag} in 
                saptune-3)
                    mandatory_files=( '/etc/sysconfig/saptune' )
                    invalid_files=( '/etc/saptune/extra/SAP_ASE-SAP_Adaptive_Server_Enterprise.conf' '/etc/saptune/extra/SAP_BOBJ-SAP_Business_OBJects.conf' '/etc/sysconfig/saptune-note-1275776' '/etc/sysconfig/saptune-note-1557506' '/etc/sysconfig/saptune-note-SUSE-GUIDE-01' '/etc/sysconfig/saptune-note-SUSE-GUIDE-02' '/etc/tuned/saptune' )
                    ;;
            esac
            ;;
        15*)
            case ${tag} in 
                saptune-3) 
                    mandatory_files=( '/etc/sysconfig/saptune' )
                    invalid_files=( '/etc/saptune/extra/SAP_ASE-SAP_Adaptive_Server_Enterprise.conf' '/etc/saptune/extra/SAP_BOBJ-SAP_Business_OBJects.conf' '/etc/sysconfig/saptune-note-1275776' '/etc/sysconfig/saptune-note-1557506' '/etc/sysconfig/saptune-note-SUSE-GUIDE-01' '/etc/sysconfig/saptune-note-SUSE-GUIDE-02' '/etc/tuned/saptune' )
                    ;;
            esac
            ;;
    esac

    # Leftovers from a damaged sapconf update/removal, which do not pervent saptune from starting.    
    sapconf_leftovers=( '/var/lib/sapconf' '/run/sapconf/active' '/run/sapconf_act_profile' )

    # Critical leftovers from a damaged sapconf update/removal, which prevent saptune from starting.    
    critical_sapconf_leftovers=( '/var/lib/sapconf/act_profile' '/run/sapconf/active' )

    # Now check the existence of mandatory and invalid files and print warnings and fails.    
    for ((i=0;i<${#mandatory_files[@]};i++)) ; do
        if [ ! -e "${mandatory_files[i]}" ] ; then 
            msg="${mandatory_files[i]} is missing, but a mandatory file."
            remediation="Check your installation!"
            print_fail "${msg}" "${remediation}"
            add_message_json FAIL "${msg}" "${remediation}"
            ((fails++))
        fi
        rpm_leftovers+=("${mandatory_files[i]}.rpmsave" "${mandatory_files[i]}.rpmnew" )
    done
    for ((i=0;i<${#invalid_files[@]};i++)) ; do
        if [ -e "${invalid_files[i]}" ] ; then   
            msg="${invalid_files[i]} is not used by this version. Maybe a leftover from an update?"
            remediation="Check the content and remove it."
            print_warn "${msg}" "${remediation}"
            add_message_json WARN "${msg}" "${remediation}"
            ((warnings++))
        fi
        rpm_leftovers+=("${invalid_files[i]}.rpmsave" "${invalid_files[i]}.rpmnew" )
    done 
    
    # Print a warning if we have found RPM leftovers!
    for ((i=0;i<${#rpm_leftovers[@]};i++)) ; do
        if [ -e "${rpm_leftovers[i]}" ] ; then 
            msg="${rpm_leftovers[i]} found. This is a leftover from a package update!"
            remediation="Check the content of \"${rpm_leftovers[i]}\" and remove it."
            print_warn "${msg}" "${remediation}"
            add_message_json WARN "${msg}" "${remediation}"
            ((warnings++))
        fi
    done 

    # Print a warning and recommend a deletion, if sapconf is not intalled and we found some files.
    if [ -z ${package_version['sapconf']} ] ; then
        for ((i=0;i<${#sapconf_leftovers[@]};i++)) ; do
            if [ -e "${sapconf_leftovers[i]}" ] ; then 
                msg="${sapconf_leftovers[i]} found. This is a leftover from a sapconf package upgrade or removal!"
                remediation="Delete ${critical_sapconf_leftovers[i]}. If this happens regularly, please report a bug."
                add_message_json WARN "${msg}" "${remediation}"
                ((warnings++))
            fi
        done 
    fi

    # Print a fail and recommend a deletion, if sapconf.service is stopped and we find these files.
    if [ "${unit_state_active['sapconf.service']}" == 'inactive' ] ; then 
        for ((i=0;i<${#critical_sapconf_leftovers[@]};i++)) ; do
            if [ -e "${critical_sapconf_leftovers[i]}" ] ; then 
                msg="${critical_sapconf_leftovers[i]} found. This is an unintended leftover from sapconf!"
                remediation="Delete ${critical_sapconf_leftovers[i]}. If this happens regularly, please report a bug."
                print_fail "${msg}" "${remediation}"
                add_message_json FAIL "${msg}" "${remediation}"
                ((fails++))
            fi
        done 
    fi 

}

function check_saptune() {
    # Checks if saptune is installed correctly.

    local fails=0 warnings=0 version_tag SAPTUNE_VERSION TUNE_FOR_SOLUTIONS TUNE_FOR_NOTES

    start_json  # open new JSON document
    add2json list_start messages  # open messages dilistct

    # We can stop, if saptune is not installed.
    if [ -z "${package_version['saptune']}" ] ; then
        msg="saptune is not installed"
        remediation="Install saptune with: zypper install saptune"
        print_fail "${msg}" "${remediation}"
        add_message_json FAIL "${msg}"  "${remediation}"
        add2json list_end  # close messages list
        add2json number warnings 0
        add2json number errors 1
        end_json
        print_json
        return 2    
    fi

    case "${package_version['saptune']}" in
        3.*)
            version_tag='saptune-3'
            ;;
        *)  
            msg="The saptune version ${package_version['saptune']} is unknown to this script! Exiting."
            remediation="Install a supported saptune version."
            print_fail "${msg}" "${remediation}"
            add_message_json FAIL "${msg}"  "${remediation}"
            add2json list_end  # close messages list
            add2json number warnings 0
            add2json number errors 1
            end_json
            print_json
            return 2 
            ;;
    esac

    # Let's test.
    header "Checking saptune"
    msg="saptune package has version ${package_version['saptune']}" 
    print_note "${msg}"
    add_message_json NOTE "${msg}"

    # Check if leftover files still in place.
    file_check saptune-3

    # Checking if system is "running" and has no failed units."
    case "${system_status['status']}" in
        running)
            msg="systemd reports status \"running\""
            print_ok "${msg}"
            add_message_json OK "${msg}"
            ;;
        degraded)
            msg="systemd reports status \"${system_status['status']}\". Failed units: ${system_status['failed_units']}"
            remediation="Check the cause and reset the state with 'systemctl reset-failed'!"
            print_warn "${msg}" "${remediation}"
            add_message_json WARN "${msg}" "${remediation}"   
            msg="A degraded systemd system status means, that one or more systemd units failed. The system is still operational! Tuning might not be affected, please run 'saptune verfiy' for detailed information."
            print_note "${msg}" 
            add_message_json NOTE "${msg}"
            ((warnings++))
            ;;
        *)  msg="systemd reports status \"${system_status['status']}\"."
            remediation="Check systemd to see what is wrong!"
            print_fail "${msg}" "${remediation}"
            add_message_json FAIL "${msg}" "${remediation}"
            ((fails++))
            ;;   
    esac  

    # Checking if the correct version has been configured.
    #add2json string configured_saptune_version "${configured_saptune_version}"
    case ${configured_saptune_version} in 
        3)  msg="configured saptune version is 3"
            print_ok "${msg}"
            add_message_json OK "${msg}"
            ;; 
        *)  msg="Configured saptune version is ${configured_saptune_version}"
            remediation="Misconfiguration happened or an update went wrong! This needs to be investigated."
            print_fail "${msg}" "${remediation}"
            add_message_json FAIL "${msg}" "${remediation}" 
            ((fails++))
            ;;
    esac 

    # Checking status of sapconf.service.
    if [ -n "${package_version['sapconf']}" ] ; then 
        case "${unit_state_active['sapconf.service']}" in
            inactive)
                msg="sapconf.service is inactive"
                print_ok "${msg}"
                add_message_json OK "${msg}" 
                ;;
            *)
                msg="sapconf.service is ${unit_state_active['sapconf.service']}"
                remediation="Run 'systemctl stop sapconf.service' or 'saptune service takeover'."
                print_fail "${msg}" "${remediation}"
                add_message_json FAIL "${msg}" "${remediation}" 
                ((fails++))
                ;;
        esac
        case "${unit_state_enabled['sapconf.service']}" in
            disabled)
                msg="sapconf.service is disabled"
                print_ok "${msg}"
                add_message_json OK "${msg}" 
                ;;
             *)
                msg="sapconf.service is ${unit_state_enabled['sapconf.service']}"
                remediation="Run 'systemctl disable sapconf.service' or 'saptune service takeover'."
                print_fail "${msg}" "${remediation}"
                add_message_json FAIL "${msg}" "${remediation}"
                ((fails++))
                ;;
        esac
    fi

    # Checking if saptune.service is enabled and started.
    #add2json list_start 'saptune.service'
    #add2json list_entry_string "${unit_state_active['saptune.service']}"
    #add2json list_entry_string "${unit_state_enabled['saptune.service']}"
    #add2json list_end
    case "${unit_state_active['saptune.service']}" in
        active)
            msg="saptune.service is active"
            print_ok "${msg}"
            add_message_json OK "${msg}"
            ;;
        *)
            msg="saptune.service is ${unit_state_active['saptune.service']}"
            remediation="Run 'systemctl start saptune.service', 'saptune service start' or 'saptune service takeover'."
            print_fail "${msg}" "${remediation}"
            add_message_json FAIL "${msg}" "${remediation}" 
            ((fails++))
            ;;
    esac
    case "${unit_state_enabled['saptune.service']}" in
        enabled)
            msg="saptune.service is enabled"
            print_ok "${msg}"
            add_message_json OK "${msg}"
            ;;
        *)
            msg="saptune.service is ${unit_state_enabled['saptune.service']}"
            remediation="Run 'systemctl enable saptune.service', 'saptune service enable' or 'saptune service takeover'."
            print_fail "${msg}" "${remediation}"
            add_message_json FAIL "${msg}" "${remediation}" 
            ((fails++))
            ;;
    esac

    # Checking status of tuned.service. and the profile.
    if [ -n "${package_version['tuned']}" ] ; then 
        case "${tool_profile['tuned']}" in
            saptune)
                msg="tuned.service is ${unit_state_active['tuned.service']}/${unit_state_enabled['tuned.service']} with profile ('${tool_profile['tuned']}')"
                remediation="This profile should not exist anymore! This needs to be investigated."
                print_fail "${msg}" "${remediation}"
                add_message_json FAIL "${msg}" "${remediation}"
                ((fails++))
                ;;
            *)
                msg="tuned profile is '${tool_profile['tuned']}'"
                print_note "${msg}"
                add_message_json NOTE "${msg}"
                case "${unit_state_active['tuned.service']}" in
                    inactive)
                        msg="tuned.service is inactive"
                        print_ok "${msg}"
                        add_message_json OK "${msg}"
                        ;;
                    *)
                        msg="tuned.service is ${unit_state_active['tuned.service']}"
                        remediation="Verify that tuning does not conflict with saptune or run 'systemctl stop tuned.service'!"
                        print_warn "${msg}" "${remediation}"
                        add_message_json WARN "${msg}" "${remediation}"
                        ((warnings++))
                        ;;
                esac
                case "${unit_state_enabled['tuned.service']}" in
                    disabled)
                        msg="tuned.service is disabled"
                        print_ok "${msg}"
                        add_message_json OK "${msg}"
                        ;;
                    *) 
                        msg="tuned.service is ${unit_state_enabled['tuned.service']}"
                        remediation="Verify that tuning does not conflict with saptune or run 'systemctl disable tuned.service'!"
                        print_warn "${msg}" "${remediation}"
                        add_message_json WARN "${msg}" "${remediation}"
                        ((warnings++))
                        ;;
                esac
                ;;
        esac
    fi

    # Print JSON output.
    add2json list_end  # close messages list
    add2json number warnings ${warnings}
    add2json number errors ${fails}
    #update_messages_json
      
    end_json    # finialize JSON string
    print_json

    # Summary.
    if [ ${DO_JSON} -eq 0 ] ; then
        echo
        [ ${warnings} -gt 0 ] && echo "${warnings} warning(s) have been found."
        [ ${fails} -gt 0 ] && echo "${fails} error(s) have been found."
        if [ ${fails} -gt 0 ] ; then
            echo "Saptune will not work properly!"
        else 
            if [ ${warnings} -gt 0 ] ; then
                echo "Saptune should work properly, but better investigate!"
            else
                echo "Saptune is set up correctly."
            fi
        fi
    fi 
    [ ${fails} -gt 0 ] && return 1
    return 0    
}

function help() {
    echo "Usage: ${0##*/} [--json] [--force-color]"
}

function intro() {
    [ ${DO_JSON} -ne 0 ] && return
    echo -e "\nThis is ${0##*/} v${VERSION}.\n"
    echo -e "It verifies if saptune is set up correctly."
    echo -e "Please keep in mind:"
    echo -e " - This tool does not check, if the tuning itself works correctly."
    echo -e " - Follow the hints from top to down to minimize side effects.\n"
}


# --- MAIN ---

declare -a JSON_NOTE_MESSAGES JSON_WARN_MESSAGES JSON_FAIL_MESSAGES JSON_REMEDIATION_MESSAGES
unset JSON_STRING _JSON_NESTING
DO_JSON=0
FORCE_COLOR=0

# Check parameters.
for param in ${@} ; do
    case "${param}" in
        --json)
            DO_JSON=1
            ;;
        --force-color)
            FORCE_COLOR=1
            ;;
        *)
            echo "Unknown parameter: ${param}" >&2
            help
            exit 3
            ;;
    esac
done

# Determine if we are running a SLES.
eval $(grep ^ID= /etc/os-release)
[ "${ID}" != "sles" ] && { echo "Only SLES is supported! Your OS ID is ${ID}! Exiting." ; exit 2 ; }

# Introduction.
intro

collect_data
check_saptune

# Bye.
exit $?