At my organization, I was recently tasked to add Mac patching along with our Windows patching. Since our organization is in healthcare, we run 24/7. So while patching is mandatory, we want to make sure that our customers are getting adequate heads up that patches have been applied, and that they need to reboot their workstation.
Here's the background:
For Windows, we use Configuration Manager for patch management, and when patches become required, our customers have up to 3 hours to reboot their workstation before their workstations get rebooted for them.
I set up a similar configuration through JAMF where Macs check in monthly and when updates become available on a Mac, that Mac checks in to install the updates. We set up a reboot notification, and the toast notification comes up letting our users know that their Mac will reboot in 180 minutes. I'm not a huge fan of the toast notification, but went with the out of box standard.
I have a couple of Mac test devices including VM's running various builds from Mojave to Big Sur. The standard out of the box updates works great on my Catalina and Mojave boxes, but my Big Sur boxes would notify me that I had an update, but would not automatically install it. I opened a support ticket with JAMF and discovered there was an issue with Big Sur and out of box automatic updates from their JAMF representative. Their engineers suggested to work around this issue that I would need to run a command in the files and process, "sudo softwareupdate -i -a -R." This command searches for updates, and triggers an instant reboot once the update has been downloaded to install.
This is obviously no bueno. Picture executives during a major presentation with their investors when their Mac suddenly reboots to apply the updates when it checks in. So my dilemma was figuring out a way to apply the updates, notify the user their Mac needs to reboot, and provide an adequate window to do this. I also discovered that simply running, "sudo softwareupdate -i -a," will not apply any updates. The -R switch must be applied in order for the updates to be installed post reboot and in my experience triggering a reboot post the -i -a switches didn't apply the updates either.
This is what I wound up doing. I created two scripts. The first is a bash script that checks to see if there are any software updates by running, the, "softwareupdate -l," command. If there are no updates, the script quietly exits. If updates are pending, then it runs the -l command again and logs what those updates are, then I run the command with -d switch to download the pending updates. Once the updates have been downloaded, it triggers the second script which is an applescript that shows a progress toast notification with a live countdown until the user's mac reboots. (The script was recycled from an A/V removal policy I was working on.) The window cannot be minimized, but can be dragged around the screen. The progress toast notification also has a stop button (Can't alter the text, but note that pressing stop will reboot the Mac.). I added an if statement for the stop button that when exit code -128 is triggered, it will run the command, "softwareupdate -i -a -R." The same command will also run when the countdown runs out.
To bring the scripts into JAMF, I use composer to create a package with places the scripts into, "/private/tmp/Updates." The package is uploaded into JAMF. I don't really care about cleaning up the source folder after the fact since rebooting clears it automatically.
I create a JAMF policy and call it, "Automatic Mac Updates." The policy has 4 configuration items.
Packages which contains the package that I just build for composer.
Software Updates is set to check from Apple's Software Update server. (Current updates are blocked while I vet this process.)
Then I add a command in Files and Processes to call the script, "sudo sh "/private/tmp/Updates/Mac_Updates.sh."
Under the general configuration, I set the policy to enable and set to recurring check-in, but set the Execution Frequency for once a month to avoid multiple software update triggers. The goal is to keep this consistent with our Windows patching.
Once the policy is configure, I set up my test scope collection, and the users are greeted with this message if an update need to be applied.
Here are the scripts that I have used.
This is the bash script that I called, "Mac_Updates.sh." This script is called from Files and Process and does the detection, logging, and calling. If there is no user logged in, the softwareupdate reboot is triggered instantly.
#!/bin/bash
###Checking to see if there are updates available
OUTPUT=$(softwareupdate -l ) 2>&1
#echo $OUTPUT
shopt -s nocasematch
#If no update is present, the script wil exit.
if echo $OUTPUT | grep -iqFe "no new";
then
Echo "I have no new updates. Exiting script."
fi
#If update is present, will log it, install it, and notify the user the Mac will reboot
if echo $OUTPUT | grep -iqFe "software update found";
then
Echo "I have found software update(s). Installing now...."
#Create a log folder called Updates
if [[ ! -d "/Library/Logs/Updates" ]]
then
sudo mkdir "/Library/Logs/Updates"
fi
dateTime=$(date +%Y_%m_%d_%H%M%S)
#Logging update
softwareupdate -l 2>&1 > "/Library/Logs/Updates/Updates-$dateTime.log"
#Download and install the update
sudo softwareupdate -d
#Reboot Mac post Symantec Removal
###check to see if user is logged in
function checkUser {
status=0
for u in $(who | awk '{print $1}' | sort | uniq)
do
if [ "$u" == "$1" ]; then
return 0
fi
done
return 1
}
if [ $# -eq 0 ] ; then
echo "User Logged In"
sudo "/private/tmp/Updates/Updates Reboot Timer.app/Contents/MacOS/applet"
else
echo "User Not Logged In"
sudo softwareupdate -i -a -R
fi
fi
This script is the Applescript reboot toast notification.
-- Progress Bar - Reboot Timer
try
progress_timer("03:00:00", "Reboot Timer") -- call the progress timer with an HMS time and timer label
on error error_message number error_number
if error_number is equal to -128 then
do shell script "sudo softwareupdate -i -a -R"
end if
end try
do shell script "sudo softwareupdate -i -a -R"
return result
------------------------------------------
-- subroutines in alphabetical order --
------------------------------------------
-- getTimeConversion converts a time in HMS format (hh:mm:ss) to a time in seconds
on getTimeConversion(HMS_Time)
set HMSlist to the words of HMS_Time -- get {hh, mm, ss} from HMS time (p. 158 Rosenthal)
set theHours to item 1 of HMSlist
set theMinutes to item 2 of HMSlist
set theSeconds to item 3 of HMSlist
return round (theHours * (60 ^ 2) + theMinutes * 60 + theSeconds)
end getTimeConversion
-- progress_timer displays the elapsed time in a progress bar. For information on progress bars see: https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/DisplayProgress.html
on progress_timer(HMS_Time, timerLabel)
set theTimeSec to getTimeConversion(HMS_Time) -- convert the HMS format to seconds
set progress total steps to theTimeSec
set progress completed steps to 0
set startTime to (current date)
repeat with i from 0 to theTimeSec -- begin at i = 0 to start the timer at 00:00:00
set HMS_SoFar to TimetoText(i) -- convert the seconds so far to HMS format for display
set HMS_ToGo to TimetoText(theTimeSec - i) -- convert the seconds to go to HMS format for display
set progress completed steps to 0
set progress description to "
Your IT Department needs to make changes to your Mac.
Your workstation must be rebooted in order for these changes to take effect.
Your workstation will reboot in " & HMS_ToGo & "
Note: Press Stop if you would like to reboot your workstation immediately."
set progress additional description to ¬
""
--"Time Elapsed: " & HMS_SoFar & return & ¬
--"Counting Down: " & HMS_ToGo
set progress completed steps to i
set elapsedTime to (current date) - startTime -- get actual elapsed time for adjusting delay
set lagAdjust to elapsedTime - i -- compute lag adjustment
delay 1 - lagAdjust -- delay 1 second minus any cumulative lag that needs removing
end repeat
--set HMS_Elapsed to TimetoText(elapsedTime) -- convert elapsedTime back to HMS format for display
set dialogText to null
--set dialogText to "Elapsed Time: " & return & ¬
-- "Nominal = " & HMS_Time & return & ¬
-- "Actual = " & HMS_Elapsed
tell me to activate
--display dialog dialogText with title timerLabel & " Timer"
return dialogText
end progress_timer
-- TimetoText converts a time in seconds to a time in HMS format (hh:mm:ss)
on TimetoText(theTime)
-- parameters - TheTime [integer]: the time in seconds
-- returns [text]: the time in the format hh:mm:ss
-- Nigel Garvey points out this script is only valid for parameter values up to 86399 seconds (23:59:59) and offers a solution for longer times here: https://macscripter.net/viewtopic.php?pid=134656#p134656
if (class of theTime) as text is "integer" then
set TimeString to 1000000 + 10000 * (theTime mod days div hours) -- hours
set TimeString to TimeString + 100 * (theTime mod hours div minutes) -- minutes
set TimeString to (TimeString + (theTime mod minutes)) as text -- seconds
tell TimeString to set theTime to (text -6 thru -5) & ":" & (text -4 thru -3) & ":" & (text -2 thru -1)
end if
return theTime
end TimetoText