Search code examples
bashfor-loopwhile-loopprintfsnmp

Buffer data retrieved in loop and print at end


I have a function to retrieve some SNMP data and print it.

The function slows down for each record printed and with a lot of routers and walks, it's very very slow. I figure it will be much more efficient to fetch all the data and print it once done, but I'm wondering how to achieve this in bash.

My question is simple, how can I make this function faster?

function primary_cpe_hsrp_snmp(){
    echo
    read -p "Do you REALLY want to check HSRP info from the CPEs? [y/N] " -n 2 -r
    if [[ $REPLY =~ ^[Yy]$ ]]; then
            snmp_community="<somecommunitystring>"
            for cpe in "${pricpes[@]}"
            do
                    hostname=$(snmpget -v 2c -c $snmp_community $cpe sysName.0 | sed -e 's/^SNMPv2-MIB::sysName.0 = STRING: //g')
                    printf "\nSome HSRP information for $hostname on IP $cpe\n"
                    printf "%-20s %-20s %-20s %-12s \n" " ___________________________________________________________________________________"
                    printf "| %-20s | %-20s | %-20s | %-12s | \n" "Virtual IP" "Active IP" "Standby IP" "State"
                    printf "| %-20s | %-20s | %-20s | %-12s | \n" "____________________" "____________________" "____________________" "____________"
                    while read -r line; do
                            ifHsrpState[$i]=$(snmpwalk -v2c -c $snmp_community $cpe 1.3.6.1.4.1.9.9.106.1.2.1.1.15.$line | awk -F ": " '{print $2}')
                            ifHsrpActiv[$i]=$(snmpwalk -v2c -c $snmp_community $cpe 1.3.6.1.4.1.9.9.106.1.2.1.1.13.$line | awk -F ": " '{print $2}')
                            ifHsrpStand[$i]=$(snmpwalk -v2c -c $snmp_community $cpe 1.3.6.1.4.1.9.9.106.1.2.1.1.14.$line | awk -F ": " '{print $2}')
                            ifHsrpVirip[$i]=$(snmpwalk -v2c -c $snmp_community $cpe 1.3.6.1.4.1.9.9.106.1.2.1.1.11.$line | awk -F ": " '{print $2}')
                            printf "| %-20s | %-20s | %-20s | %-12s |\n" "${ifHsrpVirip[$i]}" "${ifHsrpActiv[$i]}" "${ifHsrpStand[$i]}" "${ifHsrpState[$i]}"
                    done <<< "$(snmpwalk -v 2c -c $snmp_community $cpe CISCO-HSRP-MIB::cHsrpGrpAuth | sed 's/CISCO-HSRP-MIB::cHsrpGrpAuth*\.//' | awk -F" " '{print $1}')"
            printf "%-20s %-20s %-20s %-12s \n" " ___________________________________________________________________________________"
            done
    else
            :
    fi
}

Solution

  • Here's an attempt at refactoring the code.

    The idea is to reduce the number of snmpwalk calls to one per IP, then use awk to parse and extract the data of interest from the output.

    The shell still does the final formatting (instead of awk) because you were using bash arrays and I don't know what you want to do with the data afterwards.

    The new primary_cpe_hsrp_snmp function now takes at least two arguments: The SNMP community and the IP address(es) of the CISCO hardware to connect to. I also moved the user interaction outside of the function (it felt wrong to keep it there).

    edit: modified the OFS of awk and the IFS of read from white-space to comma.

    #!/bin/bash
    
    # function primary_cpe_hsrp_snmp SNMP_COMMINUTY [IPADDRESS ...]
    #
    primary_cpe_hsrp_snmp() {
    
        local snmp_community="$1"
        local cpe
    
        for cpe in "${@:2}"
        do
            local hostname="$(snmpget -v 2c -c "$snmp_community" "$cpe" sysName.0 | awk -F ': ' '{print $2}')"
    
            printf 'Some HSRP information for %s on IP %s\n' "$hostname" "$cpe"
            printf ' ——————————————————————————————————————————————————————————————————————————————————— \n'
            printf '| %-20s | %-20s | %-20s | %-12s |\n' "Virtual IP" "Active IP" "Standby IP" "State"
            printf '| ———————————————————— | ———————————————————— | ———————————————————— | ———————————— |\n'
    
            local ifHsrpVirip ifHsrpActiv ifHsrpStand ifHsrpState
    
            while IFS=',' read ifHsrpVirip ifHsrpActiv ifHsrpStand ifHsrpState
            do
                printf '| %-20s | %-20s | %-20s | %-12s |\n' "$ifHsrpVirip" "$ifHsrpActiv" "$ifHsrpStand" "$ifHsrpState"
            done < <(
                snmpwalk -v 2c -c "$snmp_community" "$cpe" .1.3.6.1.4.1.9.9.106.1.2.1.1 |
                awk -v OFS=',' '
                    /cHsrpGrpVirtualIpAddr/ {
                        key = substr($1,index($1,"."))
                        cHsrpGrpVirtualIpAddr[key] = $NF
                        next
                    }
                    /cHsrpGrpActiveRouter/ {
                        key = substr($1,index($1,"."))
                        cHsrpGrpActiveRouter[key] = $NF
                        next
                    }
                    /cHsrpGrpStandbyRouter/ {
                        key = substr($1,index($1,"."))
                        cHsrpGrpStandbyRouter[key] = $NF
                        next
                    }
                    /cHsrpGrpStandbyState/ {
                        key = substr($1,index($1,"."))
                        cHsrpGrpStandbyState[key] = $NF
                        next
                    }
                    END {
                        for (key in cHsrpGrpVirtualIpAddr)
                            print cHsrpGrpVirtualIpAddr[key], cHsrpGrpActiveRouter[key], cHsrpGrpStandbyRouter[key], cHsrpGrpStandbyState[key]
                    }
                '
            )
        done
    }
    
    ################################################################################
    
    pricpes=(
        someIP_1
        someIP_2
        ...
    )
    
    read -r -p "Do you REALLY want to check HSRP info from the CPEs? [y/N] " -n 1 
    echo
    if [[ $REPLY =~ ^[Yy]$ ]]
    then
        primary_cpe_hsrp_snmp '<somecommunitystring>' "${pricpes[@]}"
    fi
    

    remark: you may want to try snmpbulkwalk instead of snmpwalk; it might speed up the requests.


    Explanations

    In your code, for each pair of SNMP interface ID . HSRP group number (represented by X.Y in the table), you were querying the following OIDs/MIBs:

    OID MIB
    1.3.6.1.4.1.9.9.106.1.2.1.1.11.X.Y CISCO-HSRP-MIB::cHsrpGrpVirtualIpAddr.X.Y
    1.3.6.1.4.1.9.9.106.1.2.1.1.13.X.Y CISCO-HSRP-MIB::cHsrpGrpActiveRouter.X.Y
    1.3.6.1.4.1.9.9.106.1.2.1.1.14.X.Y CISCO-HSRP-MIB::cHsrpGrpStandbyRouter.X.Y
    1.3.6.1.4.1.9.9.106.1.2.1.1.15.X.Y CISCO-HSRP-MIB::cHsrpGrpStandbyState.X.Y

    I have to say, firing so many forks and initiating so many TCP connections will surely make the code inefficient.

    In fact, you can get the HSRP data for all pairs of SNMP interface ID . HSRP group number with a single snmpwalk call by requesting the 1.3.6.1.4.1.9.9.106.1.2.1.1 OID (aka CISCO-HSRP-MIB::cHsrpGrpEntry). The downside is that the output won't be ordered by SNMP interface ID . HSRP group number and that you'll get superfluous information, so you'll have to filter/select/order the data that you need by post-processing the output (that's what the awk does in my solution).

    Now, let's take a line "of interest" from the output of snmpwalk … 1.3.6.1.4.1.9.9.106.1.2.1.1 for explaining the awk, for example:

    CISCO-HSRP-MIB::cHsrpGrpVirtualIpAddr.2.1 = IpAddress: 2.3.4.5
    

    In the awk code, this line satisfies the condition /cHsrpGrpVirtualIpAddr/ (which is a regex match), so its corresponding block of code is executed:

    /cHsrpGrpVirtualIpAddr/ {
        key = substr($1,index($1,"."))
        cHsrpGrpVirtualIpAddr[key] = $NF
        next
    }
    
    • The key = substr($1,index($1,".")) extracts .2.1 from the first field and saves it as key

    • The cHsrpGrpVirtualIpAddr[key] = $NF stores the last field ($NF), whose value is 2.3.4.5, in the associative array cHsrpGrpVirtualIpAddr for the key .2.1.

    • The next means that we're done with this line so we can go on with the next one.

    The others condition { ... } are similar except the END one which is executed after all the input lines were processed. In it I loop over all the defined keys and output the values stored in the arrays in a format that can be easily read with the shell.