Mac Update Management project

DHarrison1979
New Contributor II

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.  

DHarrison1979_0-1629299838266.png

Software Updates is set to check from Apple's Software Update server.  (Current updates are blocked while I vet this process.)

DHarrison1979_1-1629299997846.png

Then I add a command in Files and Processes to call the script, "sudo sh "/private/tmp/Updates/Mac_Updates.sh."

DHarrison1979_2-1629300095000.png

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.

DHarrison1979_3-1629300422760.png

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.

DHarrison1979_6-1629300798911.png

 

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

 

11 REPLIES 11

dcgagne
Contributor

I'm looking forward to trying this. Big Sur has been an issue for mandating updates and the thought process was to have a script prompt a user with a notification and deferrals and then execute the MDM command Download and Install updates.

This looks like it will do the job with a little less complication and aligns with the process we have for Windows devices where there is a countdown to when the device restarts and installs. 

mm2270
Legendary Contributor III

Just wanted to chime in to say thanks for posting all this. This looks interesting. Like others, pushing and mandating Apple software updates has been a monumental pain the rear since Apple's recent changes. I don't know why they insist on making things so complicated and not enterprise friendly, but that's another discussion.

Also, I had literally no idea that Applescript was capable of showing any type of progress bar. What a great bit of info! I can see using this for a variety of purposes. I see it needs to run from a compiled applet, so it means it may not be possible to customize the display on the fly, but I have some ideas I'll try around that.

Anyway, kudos for the process you've outlined. I'll be taking a closer look at this very soon.

Lastly, @dcgagne not sure what your experience has been, but for me the whole "MDM command Download and Install updates" process has been mostly a bust. It barely works, or just works when it feels like it. Given there is basically no feedback anywhere on if it's even doing anything, I'm really hoping we all don't have to end up using such a broken and unfriendly process to push updates. Again, Apple man.. you're killing us!

I have been finding the MDM command on M1's seems to work consistently on the few test M1's I have access to, but it tends to throw up errors half the time on Intel Macs. I haven't spent much time investigating it but it's still on my radar because it's the "supported" method of updating going forward. I'm also having some trouble finding the documented API command for computers so it's hard to test as a scripted deployment.

A small trip report in testing this deployment:

  1. The Intel MBA I tested it on seemed to work when I ran it as a policy event with verbose logging. The download took over 2 hours over a gigabit connection, so I'm not sure where the bottleneck was. But the important thing is it appropriately updated Big Sur to 11.5.2
  2. The M1 Macs are a little less successful. I was prompted for my admin credentials in the same scenario and also saw a prompt for a password in the terminal. The reboot timer did pop up, but hitting stop froze it so there is likely a prompt during the scripted event. I'm about to restart it to see if it actually installed or if it sees the same failure.

One thing I wanted to ask about. I noticed the script doesn't have a status check if the device is on AC or battery. Will the script gracefully quit if the update requires AC or will it soldier through the update on battery?

 

*edit* The M1 reboot failed pretty spectacularly. The reboot timer had to be force quit to allow the restart and it didn't apply the update, confirming that skipping the password prompt prevented it from being installed. 

DHarrison1979
New Contributor II

Unfortunately, all I have access to are Intel based Macs.  We have put in an order for some M1's, but it may be a challenge for me to get my hands on one right away.  

What I might suggest since you have one is to open a terminal command prompt, and run the command, "softwareupdate -i -a -R," manually to see what it does.  I'm not sure what's going on with the permissions unless there is some kind of KEXT extension I need to add.

Feel free to take the script framework that I have and modify it to suit your needs.  I proposed it as a way that helped me to come up with a patch framework like what we are doing on our Windows workstations.   It's still a work in progress (no thanks to Macs corporate inflexibility) but has been helpful in our testing.  I'm expanding scope to a few more users in our environment to get feedback.  

As far as concerns on updates with A/C and battery life, I didn't really think about adding the logic, and that's mainly due to Windows workstations getting gigs of packages and content from SCCM while on battery mode.  It would be preferable if users left their laptops always plugged in, but sometimes that’s not practical during patching.  

If you want to set up conditions within JAMF or add an additional condition to the bash script to check if plugged in and exit if not, you can absolutely do that and share your results here.  

This scripting was both a labor of love and hate.  I spent weeks trying to overcome some of the deficiencies that Macs are lacking when workflows like rebooting is required post installation of applications and updates and wanted to publish what I have here to help other folks save time and grief.  

I would definitely like to hear the results of further testing on M1's as I'm lacking those and will need to push harder to get one M1 assigned to our test benches.  

The problem is M1's handle updates in a different way that requires administrative input if it is not done via MDM, even when executing as an escalated account like the jamf deployment user. . It's kind of why a lot of the current point update workflows have been scuttled thanks to Apple effectively splitting how the updates are applied. 

 

This would certainly be very beneficial for an Intel Mac fleet but mixed environments will have to maintain 2 different update flows. 

We did get some M1's in recently, so I'm going to be testing this week.  

I wanted to respond back and let you know our organization ended up using this to quickly mitigate the forcedentry zero-day for our Intel Macs. The M1's are still a bit of a problem, but when we can quickly get 90%+ of our Big Sur devices rapidly upgraded it's a good day. 

Thanks again, this worked out really well. 

user-NThAucXRnm
New Contributor II

Hello,

I am setting up a update policy using your script but when it triggers a error shows up. Any insight on what the issue might be?

Result of command:
/private/tmp/Updates/Mac_Updates.sh: line 1: {rtf1ansiansicpg1252cocoartf2580: command not found /private/tmp/Updates/Mac_Updates.sh: line 2: syntax error near unexpected token `}' /private/tmp/Updates/Mac_Updates.sh: line 2: `\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 Menlo-Regular;\f1\fnil\fcharset0 Monaco;}'

 

userNThAucXRnm_0-1631810217897.png

After realizing the text editor was part of the issue i got:

userNThAucXRnm_0-1631814853333.png

 

update:

After messing with these scripts for awhile it seems it was notepad++ causing issues and they worked fine when doing them directly in the Apple editors 

Hello

Were you able to get the timer running? everything work but the Reboot Timer 

line 1: --: command not found

line 3: try: command not found

line 4: syntax error near unexpected token `"03:00:00",'

I was able to get it running but i did so by opening it with the native apple script program and saving it there as a .app, for whatever reason the other way i was doing it was breaking the script

that was the issue. It's working now. thank you!