Posted on 04-26-2017 02:07 PM
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.
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.)
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!)
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
}
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
Posted on 04-26-2017 10:37 PM
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.
Posted on 04-27-2017 01:36 AM
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.
Posted on 04-27-2017 07:32 AM
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.
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
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
Posted on 07-21-2022 06:10 AM
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.)
Posted on 05-15-2024 09:18 AM
Is it possible to adjust this to run after x amount of hours instead of days?