Force a Computer Restart to Install macOS Updates

bwoods
Valued Contributor

In light of the new "Force a Computer Restart to Install macOS Updates" feature in Jamf Pro 10.38.0, I've decided to create a bash function that should make life easier for admins that want to force updates on M1 machines.

Please refer to the screenshot below for more information on this feature.

bwoods_0-1652729152198.png

New to bearer tokens? Don't worry about it, I've already done the work for you. Simply fill in your api account data and let the function take care of the rest. 

 

#!/bin/bash

# Server connection information
URL="https://url.jamfcloud.com"
username="apiusername"
password="apipassword"

# Determine Serial Number
serialNumber=$(system_profiler SPHardwareDataType | awk '/Serial Number/{print $4}')

initializeSoftwareUpdate(){
	# create base64-encoded credentials
	encodedCredentials=$( printf "${username}:${password}" | /usr/bin/iconv -t ISO-8859-1 | /usr/bin/base64 -i - )
	
	# Generate new auth token
	authToken=$( curl -X POST "${URL}/api/v1/auth/token" -H "accept: application/json" -H "Authorization: Basic ${encodedCredentials}" )
	
	# parse authToken for token, omit expiration
	token=$(/usr/bin/awk -F \" 'NR==2{print $4}' <<< "$authToken" | /usr/bin/xargs)
	
	echo ${token}
	
	# Determine Jamf Pro device id
	deviceID=$(curl -s -H "Accept: text/xml" -H "Authorization: Bearer ${token}" ${URL}/JSSResource/computers/serialnumber/"$serialNumber" | xmllint --xpath '/computer/general/id/text()' -)
	
	echo ${deviceID}
	
	# Execute software update	
	curl -X POST "${URL}/api/v1/macos-managed-software-updates/send-updates" -H "accept: application/json" -H "Authorization: Bearer ${token}" -H "Content-Type: application/json" -d "{\"deviceIds\":[\"${deviceID}\"],\"maxDeferrals\":0,\"version\":\"12.3.1\",\"skipVersionVerification\":true,\"applyMajorUpdate\":true,\"updateAction\":\"DOWNLOAD_AND_INSTALL\",\"forceRestart\":true}"

	# Invalidate existing token and generate new token
	curl -X POST "${URL}/api/v1/auth/keep-alive" -H "accept: application/json" -H "Authorization: Bearer ${token}"
}

initializeSoftwareUpdate 

 

Upload this script in combination with a user interaction / jamfhelper dialog policy to start forcing updates again!!!

 

Lessons Learned 06/01/2022: 

1. The update can take an extremely long time to kick off. I'm talking 1-2 hours +

2. While the Jamf Pro GUI can do full OS upgrades, it doesn't seem to be supported in the API.

 

Lessons Learned 06/23/2022:

1. I cannot recommend putting this into production. While my jamf helper script does guide the user through the update, The targeted device does not restart in a reasonable time.

2. At this time, the best options for Monterey updates and upgrades seems to be using Nudge or the startosinstall executable that comes packaged with macOS installers: Solved: Re: macOS installer script not working for Apple S... - Jamf Nation Community - 249859

 

105 REPLIES 105

Hey @bwoods would you be able to share your jamfhelper script you wrote?

bwoods
Valued Contributor

@mlooney , Below is the first part of my jamf helper process. It is a launch daemon that runs a custom policy. The deferral interval can be changed to what ever you want with the built in parameter.

 

#!/bin/bash
deferInterval="$4"

createLaunchDaemon(){
	echo "<?xml version="1.0" encoding="UTF-8"?>
	<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
	<plist version="1.0">
	<dict>
		<key>Label</key>
		<string>com.os.update</string>
		<key>UserName</key>
		<string>root</string>
		<key>ProgramArguments</key>
		<array>
			<string>/usr/local/jamf/bin/jamf</string>
			<string>policy</string>
			<string>-event</string>
			<string>updateOS</string>
		</array>
		<key>RunAtLoad</key>
        <true/>
		<key>StartInterval</key>
		<integer>${deferInterval}</integer>
	</dict>
	</plist>" > /tmp/osUpdate.plist
	
	sudo chown root:wheel /tmp/osUpdate.plist
	sudo chmod 755 /tmp/osUpdate.plist
	sudo launchctl load /tmp/osUpdate.plist
}

createLaunchDaemon 

 

 

bwoods
Valued Contributor

@mlooney Below is the second part of my jamf helper process. It's basically a set of dialogs that guides the user through the updates. logs are created to track how many times the user has deferred the prompt. Simply create a custom policy with the trigger "updateOS". The launch daemon in part A will run this at the desired time interval.

 

 

 

#!/bin/bash
# Server connection information
URL="https://url.jamfcloud.com"
username="$4"
password="$5"

# Operatitng system information
osVersion="$6"
osName="$7"
osFullName="$7 "${osVersion}""
serialNumber=$(system_profiler SPHardwareDataType | awk '/Serial Number/{print $4}')

# Log information
currentUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )
logFolder="/Users/"${currentUser}"/logs/"
deferlog="${logFolder}/deferlog.txt"
lastDeferralCount=$(cat "${deferlog}")

# Jamf helper information
jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"

description1="${osFullName} is now available. Select 'Start Now' to initialize the update. Select 'Defer' to complete the update at another time. 

Once the update is initiated, it may take 25-60 minutes for the computer to restart.

Remaining Deferral Attempts: 3"

description2="${osFullName} is now available. Select 'Start Now' to initialize the update. Select 'Defer' to complete the update at another time. 

Once the update is initiated, it may take 25-60 minutes for the computer to restart.

Remaining Deferral Attempts: 2"

description3="${osFullName} is now available. Select 'Start Now' to initialize the software update. Select 'Defer' to initialize the update at another time. 

Once the update is initiated, it may take 25-60 minutes for the computer to restart.

Remaining Deferral Attempts: 1"

finalDescription="The maximum deferral limit has been reached. Select 'Start Now' to initialize the ${osFullName} update. Otherwise, use the remaining time to sync any unsaved changes to OneDrive. 

Remaining Deferral Attempts: 0"


waitDescription="Processing software update. Please wait for the computer to restart. Ensure that the power adapter is connected. This process may take awhile."

timeout="$8"

########Functions###########

initializeSoftwareUpdate(){
	# create base64-encoded credentials
	encodedCredentials=$( printf "${username}:${password}" | /usr/bin/iconv -t ISO-8859-1 | /usr/bin/base64 -i - )
	
	# Generate new auth token
	authToken=$( curl -X POST "${URL}/api/v1/auth/token" -H "accept: application/json" -H "Authorization: Basic ${encodedCredentials}" )
	
	# parse authToken for token, omit expiration
	token=$(/usr/bin/awk -F \" 'NR==2{print $4}' <<< "$authToken" | /usr/bin/xargs)
	
	echo ${token}
	
	# Determine Jamf Pro device id
	deviceID=$(curl -s -H "Accept: text/xml" -H "Authorization: Bearer ${token}" ${URL}/JSSResource/computers/serialnumber/"$serialNumber" | xmllint --xpath '/computer/general/id/text()' -)
	
	echo ${deviceID}
	
	# Execute software update	
	curl -X POST "${URL}/api/v1/macos-managed-software-updates/send-updates" -H "accept: application/json" -H "Authorization: Bearer ${token}" -H "Content-Type: application/json" -d "{\"deviceIds\":[\"${deviceID}\"],\"maxDeferrals\":0,\"version\":\"${osVersion}\",\"skipVersionVerification\":true,\"applyMajorUpdate\":true,\"updateAction\":\"DOWNLOAD_AND_INSTALL\",\"forceRestart\":true}"
	
	# Invalidate existing token and generate new token
	curl -X POST "${URL}/api/v1/auth/keep-alive" -H "accept: application/json" -H "Authorization: Bearer ${token}"
}

pleaseWait(){
	
	buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "$waitDescription" -alignDescription "Left")
	
}

firstPrompt(){
	
	# First Prompt
buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "${description1}" -alignDescription "Left" -button1 "Start Now" -button2 "Defer")
	
	echo $buttonClicked
	
	if [[ $buttonClicked == 0 ]]; then
		echo "User clicked Start Now"
		echo "Initializing Software Update"
		rm -rf "${logFolder}"
		launchctl unload /private/tmp/osUpdate.plist
		rm -rf /private/tmp/osUpdate.plist
		initializeSoftwareUpdate
        pleaseWait
	elif [[ $buttonClicked == 2 ]]; then
		
		# Declare what the user clicked
		echo "User Clicked Defer"
		
		# Set deferralCount to 0
		deferralCount="1"
		
		#Append current number of deferrals to deferlog.txt
		echo $deferralCount > $deferlog

		exit 0
	fi
}

secondPrompt(){
	
	# Second Prompt
	buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "${description2}" -alignDescription "Left" -icon -button1 "Start Now" -button2 "Defer")
	
	echo $buttonClicked
	
	if [[ $buttonClicked == 0 ]]; then
		echo "User clicked Start Now"
		echo "Initializing Software Update"
		rm -rf "${logFolder}"
		launchctl unload /private/tmp/osUpdate.plist
		rm -rf /private/tmp/osUpdate.plist
		initializeSoftwareUpdate
        pleaseWait
	elif [[ $buttonClicked == 2 ]]; then
		echo "User Clicked Defer"
		
		# Set deferralCount to 0
		deferralCount="2"
		
		#Append current number of deferrals to deferlog.txt
		echo $deferralCount > $deferlog
		
		exit 0
	fi
}

thirdPrompt(){
	
	# Third Prompt
	buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "${description3}" -alignDescription "Left" -button1 "Start Now" -button2 "Defer")
	
	echo $buttonClicked
	
	if [[ $buttonClicked == 0 ]]; then
		echo "User clicked Start Now"
		echo "Initializing Software Update"
		rm -rf "${logFolder}"
		launchctl unload /private/tmp/osUpdate.plist
		rm -rf /private/tmp/osUpdate.plist
		initializeSoftwareUpdate
        pleaseWait
	elif [[ $buttonClicked == 2 ]]; then
		echo "User Clicked Defer"
		
		# Set deferralCount to 0
		deferralCount="3"
		
		#Append current number of deferrals to deferlog.txt
		echo $deferralCount > $deferlog
		
		exit 0
	fi
}


finalPrompt(){
	
	buttonClicked=$("$jamfHelper" -windowType hud -lockHUD -windowPosition "lr" -title "Software Update" -description "$finalDescription" -alignDescription "Left" -button1 "Start Now" -timeout $timeout -countdown -alignCountdown "Center")
	if [[ $buttonClicked == 0 ]]; then
		echo "User clicked Start Now"
		echo "Initializing Software Update"
		rm -rf "${logFolder}"
		launchctl unload /private/tmp/osUpdate.plist
		rm -rf /private/tmp/osUpdate.plist
		initializeSoftwareUpdate
        pleaseWait
	fi

}

# First Prompt
if [[ ! -f $deferlog ]]; then
	
	echo "Initializing deferral process"
	
	# Create a logs folder
	mkdir $logFolder
	
	# Create a deferlog.txt to track deferral count
	touch "$deferlog"
	
	# Set deferralCount to 0
	deferralCount="1"
	
	#Append current number of deferrals to deferlog.txt
	echo $deferralCount > $deferlog
	
	
	# Initialize First Prompt
	firstPrompt 
	
# Second Prompt
elif [[ ${lastDeferralCount} = "1" ]]; then
	echo "Initializing Second Prompt"
	secondPrompt 
	
# Third Prompt
elif [[ ${lastDeferralCount} = "2" ]]; then
	echo "Initializing Third Prompt"
	thirdPrompt  
	
elif [[ ${lastDeferralCount} = "3" ]]; then
	echo "Initializing Final Prompt"
	finalPrompt 
	
fi

exit 0		## Success
exit 1		## Failure

 

 

 

 

moussabl
New Contributor II

Hi @bwoods I have encountered an issue and I'm hoping you might be able to help me out. 

When I let the launchdaemon script from policy A runs after the specified time it will show the Jamf helper prompt, and when click "Start Now" nothing seems to happen except the script removing the log folder from the /Users. Also, no logs are showing up in deferral policy B logs that the user has selected "Start Now".  However, If I select "Defer" it works and I can see the logs showing up in policy B that the user has deferred.    

The interesting thing is if I manually run sudo jamf policy -event updaeOS on the test machine the "Start Now" from the jamf helper works fine! 

 

Any advice would be appreciated greatly. 

bwoods
Valued Contributor

@moussabl , ensure that the custom triggers are exactly the same. In your post above, see "updaeOS" instead of "updateOS". The trigger in the launch daemon must mach the trigger in the policy. Also, I designed the script to self destruct after "Start Now" is selected. The logs only work if the user clicks defer.  

For testing I suggest using Code Runner to run Part A over and over again until you're satisfied with the results.

For instance, have the code for Part A in code Runner and Part B in Jamf. Manually set the timeout in Part A to something reasonable like 10 seconds to test the code. 

moussabl
New Contributor II

Thanks heaps @bwoods! It's working for us now.

Sorry, final question. would you know if there's a way to postponed the Jamf helper notification when a user is active in a meeting on MS Teams or Zoom call?

Thank you very much.

@moussabl If the notification is a system notification, teach the users to enable Do not disturb on the computer at the beginning of a meeting (and how to disable it afterwards). This process is slightly different depending on the macOS version:

  • In macOS 10.15 Catalina and earlier, the quickest way was to go to the Notification centre (itemised hamburger menu on the right of the menu bar) and scroll up to find the Do Not Disturb switch.
  • In macOS 11 Big Sur and newer, go to the Control Centre menu and change the Focus (usually top right of the Control Centre).

bwoods
Valued Contributor

Hmmm...my first instinct would be to create a conditional statement to check if Zoom or Teams is open, but I don't know how to tell if the user is on a call. Simply checking for open apps may result in the process never starting because messaging apps are usually always open.

bwoods
Valued Contributor

@mlooney , Below is a screenshot of my parameters for the second part of my jamf helper process. You'll want to increase the timeout for production. I use 30 seconds for testing.

bwoods_0-1654005004112.png

 

@bwoods Thank you for all this! So both of these are attached to different policies? 

 

Also, does this work with Intel devices too? 

bwoods
Valued Contributor

You'll need two policies for this to work. Part A needs a policy with recurring check-in trigger to place the daemon. Part B needs a policy with a custom trigger. This also works on intel.

Thanks! I split it up into two policies a bit ago exactly the way you said above. 

 

It's been about an hour since the script ran and the software update began "processing" and it hasn't moved since then. It did prompt me for a restart (I exited the window)  but no forced restart as of yet.

I'll wait a bit longer to see if anything happens.

 

In addition, do you know if this script works fine with Intel devices? Or only with M1's?

bwoods
Valued Contributor

Yeah, It can take a long time. It's basically all up to APNS, but you can check the computer record to see if the management action is pending. Search your computer>Management Tab>You should see something like scheduleOSUpdate or availableOSUdate. It works on Intel machines too.

Just checked the computer>Management Tab and didn't see any pending/failed commands :( 

It'll only show as Pending if the command hasn't been sent yet.  Since you're running this on the machine, it likely won't have Jamf's typically 10 minute hold before the command is sent.  You should check computer > History > Management History instead.

Also note that if you haven't already run the "Download and Notify" MDM command it will likely be in the process of downloading the updates before it reboots for installing them.

Gotcha. I checked the Computer > History > Management History area and do see both "ScheduleOSUpdate" and "AvailableOSUpdates" completed about an hour and a half ago. The "Software Update" window on the machine itself is still present saying it's processing the software update.

 

I believe this machine already had the updates downloaded to it and just needed to be restarted to complete everything prior to this policy being implemented/running.

bwoods
Valued Contributor

I don't believe it takes updates that have already been downloaded into account. It just downloads them again and install from there...but yeah it can take quite a while.

Update - So I let the system sit overnight and it never actually restarted by itself. 

In the top left it says, "Restarting Your Computer. Your computer needs to restart to install updates." and gives me a button to click to restart but it never forcefully restarted. 

- Any idea why that might be?

 

Another thing, when I tested the deferral by clicking "Defer" the update window popped up immediately after saying I have 2 deferrals left. I tried to setup deferrals in the policy itself in JAMF but they didn't seem to sync together. Do you know why this is?

- Edit: Might be because I have the policy set to "Ongoing" hehe. My mistake!

 

Screen Shot 2022-06-01 at 9.48.57 AM.png

Screen Shot 2022-06-01 at 9.49.09 AM.png

A tad confused on how to setup the deferrals actually.

 

Do i need to setup deferrals in the actual policy itself too? If not, should the policy be set to ongoing or? Or just select "Defer" on the window that pops up from the script and it'll take care of the rest?

 

Apologies - not too familiar with scripting and whatnot.

bwoods
Valued Contributor

I've hardcoded the script for 3 deferrals after user interaction. You can change the deferInterval in Part A with parameter $4. You can change the time out in part B with parameter $8. You can't edit much else, this is specifically what my org requested. You'd have to tinker with the script in Part B to change the description etc...

Below I have the daemon calling the script every 30 seconds. You most likely need to extend this interval.

bwoods_0-1654100451188.png

 

Thanks for all your help and your amazing script! 

 

We got it working on our end for now!

stutz
Contributor

@bwoods I see that this script specifically calls out a macOS version to update to.  Does this script also install non-macOS point release updates (ex: Command Line Tools, Xcode etc...)?

bwoods
Valued Contributor

@stutz , I haven't tested that yet.

eargueta
New Contributor III

I was able to create the this policy work but it only works during the software Update have the option set to "Restart", how can you push it to make the initial call when the software Update is set to "Start". When I run this executiion it doesn't do anything at the time or start the download. Wondering in what I am missing here. 

bwoods
Valued Contributor

@eargueta, please rephrase your question. I don't understand the issue you're having.

dvasquez
Contributor III

I am seeing a hiccup here when running the script:

launchctl /private/tmp/osUpdate.plist
		rm -rf /private/tmp/osUpdate.plist

I get the following output indicating this command "launchctl" is not being used correctly. Have you run into this?

I am trying to use the script to upgrade Big Sur to Monterey M1 type.

 echo 'User clicked Start Now'

User clicked Start Now

+ echo 'Initializing Software Update'

Initializing Software Update

+ rm -rf /Users/dvasqu29/logs/

+ launchctl /private/tmp/osUpdate.plist

Unrecognized subcommand: /private/tmp/osUpdate.plist

Usage: launchctl <subcommand> ... | help [subcommand]

Many subcommands take a target specifier that refers to a domain or service

within that domain. The available specifier forms are:

 

system/[service-name]

Targets the system-wide domain or service within. Root privileges are required

to make modifications.

 

user/<uid>/[service-name]

Targets the user domain or service within. A process running as the target user

may make modifications. Root may modify any user's domain. User domains do not

exist on iOS.

 

gui/<uid>/[service-name]

Targets the GUI domain or service within. Each GUI domain is associated with a

user domain, and a process running as the owner of that user domain may make

modifications. Root may modify any GUI domain. GUI domains do not exist on iOS.

 

session/<asid>/[service-name]

Targets a session domain or service within. A process running within the target

security audit session may make modifications. Root may modify any session

domain.

 

pid/<pid>/[service-name]

Targets a process domain or service within. Only the process which owns the

domain may modify it. Even root may not do so.

 

When using a legacy subcommand which manipulates a domain, the target domain is

inferred from the current execution context. When run as root (whether it is

via a root shell or sudo(1)), the target domain is assumed to be the

system-wide domain. When run from a normal user's shell, the target is assumed

to be the per-user domain for that current user.

 

 

bwoods
Valued Contributor

Looks like you need to choose to unload or unload your daemon and/or agent.

bwoods
Valued Contributor

Ah, I see that that's a mistake in the script. I'll add the unload above.

bwoods
Valued Contributor

@dvasquez I fixed it. Copy the script again in my previous post.

Ok, I will check it out. I get the keys and I see the connection to the API calls. But the Silicon laptops will just not update. So I'll try again. Thank you this was super helpful. For the record I did add the umload to mine but for some reason the laptops will nto upgrade. Anyway thanks!

bwoods
Valued Contributor

@dvasquez when you run the command on a machine, are you seeing that the management command has run in Jamf Pro. Simply go to the management tab to check this.

I will look for that but in early testing, I did not see any progress there. 

 

I will test again. 

tegus232
New Contributor III

@bwoods How do i go about using it for macOS11 to 12. I am getting the following error "ScheduleOSUpdate Unsupported InstallAction for this ProductKey"

 

This M1 2021. it was when i used it for inline updates

bwoods
Valued Contributor

It looks like the API doesn't support full OS Upgrades yet. I can't get that feature to work like it does in the GUI.

rblaas
Contributor II

So we are struggling also with the macOS updates 

This script looks somewhat helpful. 

But, Beside it is taking a long time.. The computer will still reboot suddenly without any warning.. 

I would like to send a download only command first and fire an install command when I know the download is finished. 

 

Is this possible?

bwoods
Valued Contributor

Also, there is no option to download beforehand and install later. It's always going to download and install the update in the same APNS command.

bwoods
Valued Contributor

@rblaas, you have to use my script to warn users beforehand. Otherwise the machine will just reboot without warning.

At this time, I don't recommend putting this into production. The reboot takes too long to happen. Best options at this point are to use Nudge or deploy and update with macOS Installers.

I find it works best with computers already on 12.3+ so it can take advantage of the built-in MaxUserDeferrals key and notifications to the user:

Image 5-25-22 at 10.12 AM (2).jpg

bwoods
Valued Contributor

@markopolo , what is the behavior you're seeing once the max deferral limit is reached? Does the computer restart randomly or are you prompted to install during the evening hours?

Also, what is the average time for a machine to reboot on your end?

To be honest, it always ran the update at night in our testing so I wasn't able to observe it. It seems to pick a time when the computer is inactive to do the forced restart. I'll try to do some additional testing and let you know.