Extension Attribute Frequency

dan-snelson
Valued Contributor II

Background

While we're waiting for @NightFlight's Extention Attribute Execution Frequency feature request to be implimented, here's my two cents, which was inspired by @brad's approach for only occasionally capturing the status of a computer's Recovery HD.


Approach

As one of the first steps of an Extension Attribute script, you pass the name of the Extension Attribute and the desired execution frequency (in days) to a client-side function. A client-side plist stores the epoch and the result.

During subsequent inventory updates, if the current epoch is less than the given frequency, it just reads the previous result from the plist instead of executing the entire Extension Attribute script.

For example, I have an EA for “Model Name”; how many times do you need to run that Extension Attribute? (Once per quarter? Once per year? Certainly not every time.)


Results

Early tests show an overall inventory collection that is 1.6x faster, using the following as a gauge before and after:
time -p sudo jamf recon -verbose

(Some individual EA scripts which curl external Web sites or query the Sophos AV binary have realized a 113x increase!)


Scripts

Client-side Functions

You'll need to install the following functions client-side (i.e., when enrollment in complete) and update the path for "organizationalPlist".

#!/bin/sh
####################################################################################################
#
# ABOUT
#
#   Standard functions which are imported into other scripts
#
####################################################################################################
#
# HISTORY
#
#   Version 1.2, 26-Apr-2017, Dan K. Snelson
#       Added Extension Attribute Execution Frequency & Results
#
####################################################################################################



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
#
# LOGGING
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 


## Variables
logFile="/var/log/com.company.log"
alias now="/bin/date '+%Y-%m-%d %H:%M:%S'"


## Check for / create logFile
if [ ! -f "${logFile}" ]; then
    # logFile not found; Create logFile ...
    /usr/bin/touch "${logFile}"
    /bin/echo "`/bin/date +%Y-%m-%d %H:%M:%S`  *** Created log file via function ***" >>"${logFile}"
fi

## I/O Redirection to client-side log file
exec 3>&1 4>&2            # Save standard output (stdout) and standard error (stderr) to new file descriptors
exec 1>>"${logFile}"        # Redirect standard output, stdout, to logFile
exec 2>>"${logFile}"        # Redirect standard error, stderr, to logFile


function ScriptLog() { # Write to client-side log file ...

    /bin/echo "`/bin/date +%Y-%m-%d %H:%M:%S`  ${1}"

}



function jssLog() { # Write to JSS ...

    ScriptLog "${1}"              # Write to the client-side log ...

    ## I/O Redirection to JSS
    exec 1>&3 3>&- 2>&4 4>&-        # Restore standard output (stdout) and standard error (stderr)
    /bin/echo "${1}"              # Record output in the JSS

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
#
# JAMF Display Message
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 

function jamfDisplayMessage() {

    ScriptLog "${1}"
    /usr/local/jamf/bin/jamf displayMessage -message "${1}" &

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
#
# Extension Attribute Execution Frequency
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 

function eaFrequency() {

    # Validate parameters
    if [ -z "$1" ] || [ -z "$2" ] ; then                     
      ScriptLog "Error calling "eaFrequency" function: One or more parameters are blank; exiting."
      exit 1
    fi

    # Variables
    organizationalPlist="/client-side/path/to/com.company.plist"
    plistKey="$1"                             # Supplied plistKey
    frequency="$2"                                # Supplied frequency in days
    frequencyInSeconds=$((frequency * 86400))   # There are 86,400 seconds in 1 day

    # Check for / create plist ...
    if [ ! -f "${organizationalPlist}" ]; then
        ScriptLog "The plist, "${organizationalPlist}", does NOT exist; create it ..."
        /usr/bin/touch "${organizationalPlist}"
        /usr/sbin/chown root:wheel "${organizationalPlist}"
        /bin/chmod 0600 "${organizationalPlist}"
    fi

    # Query for the given plistKey; suppress any error message, if key not found.
    plistKeyTest=$( /usr/libexec/PlistBuddy -c 'print "'"${plistKey} Epoch"'"' ${organizationalPlist} 2>/dev/null )

    # Capture the exit code, which indicates success v. failure
    exitCode=$? 
    if [ "${exitCode}" != 0 ]; then
        ScriptLog "The key, "${plistKey} Epoch", does NOT exist; create it with a value of zero ..."
        /usr/bin/defaults write "${organizationalPlist}" "${plistKey} Epoch" "0"   
    fi

    # Read the last execution time ...
    lastExecutionTime=$( /usr/bin/defaults read "${organizationalPlist}" "${plistKey} Epoch" )

    # Calculate the elapsed time since last execution ...
    elapsedTimeSinceLastExecution=$(( $(date +%s) - ${lastExecutionTime} ))

    # If the elapsed time is less than the frequency, read the previous result ...
    if [ "${elapsedTimeSinceLastExecution}" -lt "${frequencyInSeconds}" ]; then
        ScriptLog "Elapsed time since last execution for "$plistKey", $elapsedTimeSinceLastExecution, is less than $frequencyInSeconds; read previous result."
        eaExecution="No"
        eaResult "${plistKey}" # Obtain the current result
    else
        # If the elapsed time is less than the frequency, read the previous result ...
        ScriptLog "Elapsed time since last execution for "$plistKey", $elapsedTimeSinceLastExecution, is greater than $frequencyInSeconds; execute the Extension Attribute."
        /usr/bin/defaults write "${organizationalPlist}" "${plistKey} Epoch" "`/bin/date +%s`"
        eaExecution="Yes"
    fi

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
#
# Extension Attribute Result
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 

function eaResult() {

    # Validate parameters
    if [ -z "$1" ] ; then
        ScriptLog "Error calling "eaResult" function: Parameter 1 is blank; exiting."
        exit 1
    fi

    # Variables
    organizationalPlist="/client-side/path/to/com.company.plist"
    plistKey="$1"
    result="$2"

    if [ -z "$2" ] ; then
        # If the function is called with a single parameter, then just read the previously recorded result
        returnedResult=$( /usr/bin/defaults read "${organizationalPlist}" "${plistKey} Result" )

    else

        # If the function is called with two parameters, then write / read the new result   
        /usr/bin/defaults write "${organizationalPlist}" "${plistKey} Result" ""${result}""
        returnedResult=$( /usr/bin/defaults read "${organizationalPlist}" "${plistKey} Result" )

    fi

}

Extension Attribute Modifications

Make the following modifications to your most time-consuming EA scripts, which output their results to a variable called results . (Also, update the source path for your environment.)

We just looked for time-consuming EAs while running the following:
time -p sudo jamf recon -verbose

#!/bin/sh
####################################################################################################
# Import client-side functions
source /client-side/path/to/functions.sh
####################################################################################################

# Variables
eaName="Friendly name for Extension Attribute"        # Name of Extension Attribute
eaDays="30"                                   # Number of days between executions


# Check for Extension Attribute execution

eaFrequency "${eaName}" "${eaDays}"

if [ ${eaExecution} == "Yes" ]; then

    #
    #
    #
    # Insert your Current Extension Attribute script here.
    #
    #
    #

    eaResult "${eaName}" "${result}"

else

    eaResult "${eaName}"

fi

jssLog "<result>${returnedResult}</result>"

exit 0
4 REPLIES 4

tlarkin
Honored Contributor

For what it is worth when I worked at Jamf I put this in as an internal Feature Request probably 3 years ago by now. I also did an entire Macbrained presentation on concepts like this around that same time. The idea is to have all your code write to a flat file locally, then extension attributes only ever have to read in that value and echo "<result>$value</result>"

I would go a step further and ask for any and all code to be able with a check box be synchronized to a jamf folder locally. That way you could import your own Python modules on top of calling the source command in bash. This would also work for other languages like Ruby and Perl.

Then allow the JSS to hold the latest version of the code base and deploy it or source it to the rest of your code. The whole infrastructure as code thing is pretty real and right now there isn't a good or clean way to recycle code. Hopefully things like this get more traction, and more people adopt things like what OP posted.

PeterClarke
Contributor II

My experience has been that EA are generally fast to compute.
I also like the idea that EA's don't contain stale data

At the same time, I am all for having fast recon times, historically slow recon was a problem, though it now seems to be less of an issue in Casper Vn 9.x, and unlike the past, we have not been observing any undue delays with recon.

Being able to specify individual frequencies for EA collection could be useful in some cases.
I actually implemented something like this in an earlier environment we had - since replaced,
where I separated 'an implementor' from 'a reader'.

I would say that in custom situations, it may be a useful technique.

dan-snelson
Valued Contributor II

Thanks, @tlarkin and @PeterClarke.

Here's a real-world example, using @jake's VMware - Virtual Machine List.

If my math is right, it's a speed improvement of 181.6x.


Traditional Extension Attribute

Execution Time:
- real 18.36
- user 2.29
- sys 6.59

#!/bin/sh

OS=`/usr/bin/sw_vers -productVersion | /usr/bin/colrm 5`

if [[ "$OS" < "10.6" ]]; then
myVMList=`find /Users -name "*.vmx"`
else
myVMList=`mdfind -name ".vmx" | grep -Ev "vmx.lck" | grep -Ev "vmxf"`
fi

IFS=$'
'
myCount=1
echo "<result>"
for myFile in $myVMList
do
myNetwork=`cat "$myFile"| grep "ethernet.*.connectionType"| awk '{print $3}'| sed 's/"//g'`
myDisplayName=`cat "$myFile"| grep "displayName"| sed 's/displayName = //g'| sed 's/"//g'`
myMemSize=`cat "$myFile"| grep "memsize"| awk '{print $3}'| sed 's/"//g'`
myUUID=`cat "$myFile"| grep "uuid.bios"| sed 's/uuid.bios = //g'| sed 's/"//g'`
myMAC=`cat "$myFile"| grep "ethernet.*.generatedAddress"| grep -v "Offset"| awk '{print $3}'| sed 's/"//g'`

echo "=-=-=-=-=-=-=-=-=-=-=-=-=-"
echo "VMWare VM #$myCount"
echo "File Name: $myFile"
echo "Display Name: $myDisplayName"
echo "Network Type: $myNetwork"
echo "MAC Address: $myMAC"
echo "Memory: $myMemSize MB"
echo "UUID: $myUUID"

let myCount=myCount+1
done
echo "</result>"

unset IFS

Updated Extension Attribute

Execution Time (after initial run):
- real 0.10
- user 0.03
- sys 0.03

#!/bin/sh
####################################################################################################
#
# Extension Attribute to determine VMWare - Virtual Machine List
#
####################################################################################################
# Import client-side functions
source /path/to/client-side/functions.sh
####################################################################################################

# Variables
eaName="VMWare - Virtual Machine List"    # Name of Extension Attribute
eaDays="90"                               # Number of days between executions


# Check for Extension Attribute execution
eaFrequency "${eaName}" "${eaDays}"

if [ ${eaExecution} == "Yes" ]; then

    OS=`/usr/bin/sw_vers -productVersion | /usr/bin/colrm 5`

    if [[ "$OS" < "10.6" ]]; then
        myVMList=`find /Users -name "*.vmx"`
    else
        myVMList=`mdfind -name ".vmx" | grep -Ev "vmx.lck" | grep -Ev "vmxf"`
    fi

    IFS=$'
'

    myCount=1

    for myFile in $myVMList; do

        myNetwork=`cat "$myFile"| grep "ethernet.*.connectionType"| awk '{print $3}'| sed 's/"//g'`
        myDisplayName=`cat "$myFile"| grep "displayName"| sed 's/displayName = //g'| sed 's/"//g'`
        myMemSize=`cat "$myFile"| grep "memsize"| awk '{print $3}'| sed 's/"//g'`
        myUUID=`cat "$myFile"| grep "uuid.bios"| sed 's/uuid.bios = //g'| sed 's/"//g'`
        myMAC=`cat "$myFile"| grep "ethernet.*.generatedAddress"| grep -v "Offset"| awk '{print $3}'| sed 's/"//g'`

        result="=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-"
        result="${result}"$'
'"VMWare VM #${myCount}"
        result="${result}"$'
'"File Name: ${myFile}"
        result="${result}"$'
'"Display Name: ${myDisplayName}"
        result="${result}"$'
'"Network Type: ${myNetwork}"
        result="${result}"$'
'"MAC Address: ${myMAC}"
        result="${result}"$'
'"Memory: ${myMemSize} MB"
        result="${result}"$'
'"UUID: ${myUUID}"

        let myCount=myCount+1

    done

    unset IFS

    eaResult "${eaName}" "${result}"

else

    eaResult "${eaName}"

fi

jssLog "<result>${returnedResult}</result>"

exit 0

gampo
New Contributor

To God be the glory, this is very helpful, Dan.  Thank you so much.  In my initial testing, line 108 where the "plistKeyTest" variable is defined, there is a space character between "${plistKey}" and "Epoch" which I needed to remove in order to have the check work properly if the plist key did exist.  (This is assuming " Epoch" with the space in the first position of the key was not intended.)