Programmatic macOS updates for Apple Silicon

dan-snelson
Valued Contributor II

(Please pardon the piecemeal post; I'm presuming partial information is better than nothing.)


scheduleOSUpdate via the Jamf Pro API

Thanks to AppleCare pointing out that:

When running the softwareupdate command in a root shell on Apple Silicon users are being prompted for a password. This is expected behavior and the recommendation is to use the Schedule an OS Update command via MDM. This is the method to use if you want to update Apple Silicon Macs without requiring user credentials.

In other words:

if [[ "$arch" == "arm64" ]]; then
    scheduleOSUpdateViaAPI
else
    /usr/sbin/softwareupdate --install --all --include-config-data --restart --force
fi

In my limited testing, users are still prompted:
1dd859ae6a0d47459212605ed9035d25
0f66bc76c8164c1f8d4e61444960f217


Pending Feature Requests


Snippets

####################################################################################################
#
# Variables
#
####################################################################################################

jamfProURL="https://company.jamfcloud.com" # No trailing forward slash
apiUsername="${5}"
apiPasswordEncrypted="${6}"
computerSerialNumber=$( /usr/sbin/system_profiler SPHardwareDataType | /usr/bin/grep Serial | /usr/bin/awk '{print $NF}' )
arch=$( /usr/bin/arch )



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

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# xpath tool changes in Big Sur
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function xpath() {

    # https://scriptingosx.com/2020/10/dealing-with-xpath-changes-in-big-sur/
    # Thanks, Armin!
    if [[ $(sw_vers -buildVersion) > "20A" ]]; then
        /usr/bin/xpath -e "$@"
    else
        /usr/bin/xpath "$@"
    fi

}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Decrypt Password
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function decryptPassword() {
    /bin/echo "${1}" | /usr/bin/openssl enc -aes256 -d -a -A -S "${2}" -k "${3}"
}



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Schedule OS Update via the API
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

function scheduleOSUpdateViaAPI() {

    echo "Schedule OS Update via the API …"

    apiPassword=$( decryptPassword ${apiPasswordEncrypted} ${Salt} ${Passphrase} )
    jamfProCompID=$( /usr/bin/curl -s -u ${apiUsername}:${apiPassword} ${jamfProURL}/JSSResource/computers/serialnumber/${computerSerialNumber}/subset/general | xpath "/computer/general/id/text()" )

    # /usr/bin/curl -s -X POST -H "Content-Type: text/xml" -u ${apiUsername}:${apiPassword} ${jamfProURL}/JSSResource/computercommands/command/ScheduleOSUpdate/action/InstallForceRestart/id/${jamfProCompID}

    /usr/bin/curl -s -X POST -H "Content-Type: text/xml" -u ${apiUsername}:${apiPassword} ${jamfProURL}/JSSResource/computercommands/command/ScheduleOSUpdate/action/Default/id/${jamfProCompID}

}




####################################################################################################
#
# Program
#
####################################################################################################


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Force Software Update Snippet only
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

if [[ "$arch" == "arm64" ]]; then
    scheduleOSUpdateViaAPI
else
    /usr/sbin/softwareupdate --install --all --include-config-data --restart --force
fi
66 REPLIES 66

jonw
Contributor

I went ahead and deployed to a classroom last night and successfully updated 34 out of 36 stations from 11.5.2 to 11.6 while at the login window.  I'm still at a loss as to why it's not 100%, but hopefully this will improve with Monterey.  As best I can tell the Jamf side and API command is functioning, it just seems like something on the workstation(s) themselves related to softwareupdated, or other process is hindering the update.  I'm still digging though logs, but I'm not coming up with anything yet.

fwiw- if it helps anyone following along, here's how I'm initiating the command via check-in from a Jamf policy script (sanitized for public view & using optional script parameters for testing... I would recommend using encrypted script parameters for production).

api user permissions are only:
Jamf Pro Server Objects: Computer: Create & Read
Jamf Pro Server Actions: Send Computer Remote Command to Download and Install macOS Update

 

- see newer script in thread below using updated API

 

 

TomDay
Release Candidate Programs Tester

Love this script, thx for sharing. Working well for my tests so far with Monterey, but I have to manually click "Restart Now" in Software Update to get this to complete. Is that as intended or should this download, install and then restart automatically?

Thanks Tom!  My only thought is are you following up with a policy 'Restart Options' payload specifically set to 'MDM Restart with Kernel Cache Rebuild'?
Screen Shot 2022-05-06 at 12.24.17 PM.png
I recently updated a few hundred lab stations to 12.3.1 at the login window so I know it's still working.  However as I mentioned previously, I almost always have a few stations that despite receiving the mdm command never complete the update!?  I suspect an Apple bug with softwareupdated, and lately I've been adding this earlier in the script. I feel like it's helping but I can't honestly say for sure.  The word is that Apple patched some issues with softwareupdated in 12.3 but I've not hat time to follow up & test.

 

/bin/launchctl kickstart -k system/com.apple.softwareupdated
### give it 5 minutes to check for updates & sort itself out
sleep 300

 



TomDay
Release Candidate Programs Tester

Ah I did not have the restart payload set in my policy! Testing now, first computer did not work, will keep working on it. Using this policy with the "Enrollment complete" trigger so when when start getting new laptops ready, one of the first things that happens is updating to latest macOS.

 

Where did you apply:

/bin/launchctl kickstart -k system/com.apple.softwareupdated
### give it 5 minutes to check for updates & sort itself out
sleep 300

 

 

TomDay
Release Candidate Programs Tester

@jonw actually it did work just now! Heading out for the wkd and letting another run now, will report back Monday.

@TomDay Awesome!  Glad to hear it.  

I run the softwareupdated kickstart before everything else.  I also should have mentioned the whole update process governed by api/mdm is annoyingly 'mysterious' at times, whether triggered via script or by the native Jamf action.  I've seen it take up to an hour past the command being sent to see any obvious activity other than the Jamf log saying the command was successfully sent.

KyleEricson
Valued Contributor II

Has anyone found a Jamf API to include the new deferral option when running the update MDM command?

Read My Blog: https://www.ericsontech.com

rstasel
Valued Contributor

judging by the API commands I'm seeing, it doesn't seem like we can do this yet. =(

rstasel
Valued Contributor

Correction, just been noodling, and found it in the API docs. 

https://jamfserver.example.com/api/doc/#/macos-managed-software-updates/post_v1_macos_managed_softwa...

Wrote up an advancedsearch to find machines needing 12.2.1, and then some quick powershell to grab the computers, and build the query. It SEEMS to work, but will need to test more.

jwaltonen
New Contributor III
/bin/launchctl kickstart -k system/com.apple.softwareupdated
### give it 5 minutes to check for updates & sort itself out
sleep 300

 

This seems to have made a big difference in the effectiveness of the ScheduleOSUpdate command for me.

 

jonw
Contributor

@jwaltonen Glad to hear it!

By the way, a new feature in the recently released Jamf 10.38.0 that looks really promising!  "You can now force computers to immediately restart and install an available macOS update using the /v1/macos-managed-software-updates/send-updates endpoint via the Jamf Pro API."

I'm going to start testing asap.

jwaltonen
New Contributor III

Maybe I spoke too soon.

With Monterey:

${jss}/JSSResource/computercommands/command/ScheduleOSUpdate/action/install/id/${jamfID}

Seemed to work pretty good on its own.

Adding 

/bin/launchctl kickstart -k system/com.apple.softwareupdated

before the call seemed to make it work even better.

However it has done nothing for improving the situation on Big Sur endpoints.

 

/v1/macos-managed-software-updates/send-updates

In my limited testing this is working well for Monterey but it still very sporadic, leaning toward mostly not working for Big Sur endpoints.

I guess the solution is obvious, upgrade everything to Monterey.

BlackGloveEng1
New Contributor

Hi! With Monterrey pretty sure you want to POST to 

https://instance.jamfcloud.com/api/v1/macos-managed-software-updates/send-updates

https://developer.jamf.com/jamf-pro/reference/post_v1-macos-managed-software-updates-send-updates

 

We have tested and it works. But the thing is it's kind of just a random wait until it starts.... Wish there was a way to intercept the process start (downloading update + starting preinstall before logout) from the syslog or such other and show a notification that it's starting. Could be anywhere from 30 seconds to 15 minutes before the machine randomly reboots on the user.

@BlackGloveEng1 Funny you should mention, I've actually been working on an improved script that uses the new endpoint you mention (my version above was built prior to this) which also incorporates the use of an auth bearer token.

Here's what I'm testing now.  It seems pretty solid, but I'm happy to receive any constructive criticism. The only issue I've noticed is the same as before, periodically a computer will go through the motions, but no update actually occurs.  I can see in the logs the API call was successfully sent, so I think this is an Apple/MDM issue, not Jamf.  Running a second time almost always fixes it up. Obviously use at your own risk, you can see I only schedule this to run off hours in our unique student lab environment, ymmv.

 

#!/bin/bash

### Execute macOS update via MDM API - LAB

### latest update 2022.06.30 -JonW
### Use auth bearer token and new /v1/macos-managed-software-updates/send-updates endpoint found in Jamf Pro API 10.38.0+ 

### This is primarily intended for off hours, scheduled macOS updates while at the login window.
### Script will warn any logged-in user with a 5 minute timer, but will NOT provide an option to defer.
### Updating in this fashion eliminates the requirement for a local admin password on Silicon and also works with Intel.

### Adapting a few ideas & tips from deflounder, @talkingmoose, & @bwoods - THANK YOU!
### https://derflounder.wordpress.com/2022/01/04/basic-authentication-deprecated-for-the-jamf-pro-classic-api/
### https://community.jamf.com/t5/jamf-pro/force-a-computer-restart-to-install-macos-updates/td-p/265982

### required API permissions
### Jamf Pro Server Objects: Computer (read only - to obtain device ID)
### Jamf Pro Server Actions: Send Computer Remote Command to Download and Install macOS Update

### Ensure policy uses MDM Restart with Kernel Cache Rebuild payload

### SECURITY NOTE!!! 
### We're calling the API from the local computer, and passing the API username & password...
### which can potentially expose them to a savvy local user looking at system processes!
### In this case, risk is minimized by running only while at the login window and using a unique API user & pass with limited permissions.


#################################################################################
### Jamf policy script parameters
#################################################################################

jss="$4"
apiUser="$5"
apiPass="$6"
macOSversion="$7" ### OPTIONAL - specify a macOS version to install (if not specified, latest available will apply)
applyMajorUpdate="$8" ### OPTIONAL - allow major macOS version upgrade (specify true or false)



#################################################################################
### If a user is logged in, warn to save & logout within 5 minutes!
### As we only run late on weekends, chances are slim the user is actually active,
### more likely a stuck app has halted the automatic logout process, but just to be sure...
#################################################################################

loggedInUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )

if [[ -n "$loggedInUser" ]]; then
	
### Open JamfHelper dialog
buttonClicked=$(/Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper \
-windowType utility \
-title "Weekly MacOS Update Policy" \
-heading "Attention! " \
-description "Automatic logout to perform updates will occur in 5 minutes!  To prevent data loss, save all files and logout immediately." \
-defaultButton 1 \
-button2 "OK" \
-countdown \
-timeout 300 \
-alignCountdown center)
	
	if [ $buttonClicked == 2 ]; then
		### Button 2 clicked - user warning acknowledged
		echo "User acknowledged, waiting 5 minutes for them to save & logout"
		sleep 300
		
		### Is user still logged in?
		loggedInUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )
		if [[ -n "$loggedInUser" ]]; then
			echo "User acknowledged, but has yet to logout after 5 minutes, force logout now"
			killall loginwindow ### No need to be delicate
			sleep 30  ### wait for loginwindow to reload
		else 
			echo "proceeding, user has logged out"
		fi
		
	else
		### No click default = 1, the 5 minute countdown timed out
		echo "proceeding, no user acknowledgement after 5 minutes, force logout now"
		killall loginwindow  ### No need to be delicate
		sleep 30 ### wait for loginwindow to reload
	fi
	
fi



#################################################################################
### kick softwareupdated, not required, but does 'seem' to help with success rate
#################################################################################

/bin/launchctl kickstart -k system/com.apple.softwareupdated
sleep 120 ### give it a few minutes to sort itself out




#################################################################################
### Generate Authorization Bearer Token
### (valid 30 minutes or until we invalidate)
#################################################################################

### (thanks again Der Flounder & @talkingmoose for the slick one-liner & parsing methods!)

if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
	### Pre Monterey, grab token & parse to remove quotes & expiration details
	token=$(/usr/bin/curl -s "${jss}/api/v1/auth/token" -u "${apiUser}:${apiPass}" -X POST | python -c 'import sys, json; print json.load(sys.stdin)["token"]')
else
	### As of Monterey, grab token and use updated plutil to pull raw token directly - no parsing necessary
	token=$(/usr/bin/curl -s "${jss}/api/v1/auth/token" -s -u "${apiUser}:${apiPass}" -X POST | plutil -extract token raw -)
fi

echo "$token"




#################################################################################
### Determine Jamf Pro device id from the computer serial number (ID is required by /v1/macos-managed-software-updates endpoint)
### Note, I'm still unable to find a way to pull a device ID from the Pro API, continue using Classic for now. 
#################################################################################

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

deviceID=$(/usr/bin/curl -s "${jss}/JSSResource/computers/serialnumber/${serialNumber}" -H "Authorization: Bearer ${token}" -H "Accept: text/xml" | xmllint --xpath '/computer/general/id/text()' -)

echo "Device ID" ${deviceID}
	
	


#################################################################################
### Check current macOS, and attempt to apply updates
##################################################################################

### If optional specified macOS version is equal to the current macOS, we're already up to date, exit 0
current_macOS=$( sw_vers -productVersion )

if [[ "$7" == "$current_macOS" ]]; then
	echo "Specified macOS is $7"
	echo "current macOS is $current_macOS"
	echo "No update necessary, macOS is already at our specified version"
	exit 0
fi



### If optional script parameter $8 (allow major macOS upgrade) is not explicitely set to true, force default to be false
if [[ "$8" != "true" ]]; then
	applyMajorUpdate="false"
	echo "allow major macOS upgrade? $applyMajorUpdate"
else
	echo "allow major macOS upgrade? $applyMajorUpdate"
fi



### If the optionally specified macOS version is NOT EQUAL to the current macOS, apply it, else apply the most recently available update.
### Note! We're not validating cases where the specified version may be lower than the currently installed version. 
### The internal update process 'should' automatically ignore these attempts, but I've not yet tested the theory!
if [[ $7 != "" ]]; then
	
	echo "attempting to apply specified macOS version $7"
	
	/usr/bin/curl -s -X POST \
	--url "${jss}/api/v1/macos-managed-software-updates/send-updates" \
	--header "Authorization: Bearer ${token}" \
	--header "accept: application/json" \
	--header "Content-Type: application/json" \
	--data @<(cat <<EOF
		{
			"deviceIds": [
				"$deviceID"
			],
			"maxDeferrals": 0,
			"version": "$macOSversion",
			"skipVersionVerification": true,
			"applyMajorUpdate": $applyMajorUpdate,
			"updateAction": "DOWNLOAD_AND_INSTALL",
			"forceRestart": true
		}
EOF
)
	
	
else
	
	echo "attempting to apply the latest available macOS version"
	
	/usr/bin/curl -s -X POST \
	--url "${jss}/api/v1/macos-managed-software-updates/send-updates" \
	--header "Authorization: Bearer ${token}" \
	--header "accept: application/json" \
	--header "Content-Type: application/json" \
	--data @<(cat <<EOF
		{
			"deviceIds": [
				"$deviceID"
			],
			"maxDeferrals": 0,
			"skipVersionVerification": true,
			"applyMajorUpdate": $applyMajorUpdate,
			"updateAction": "DOWNLOAD_AND_INSTALL",
			"forceRestart": true
		}
EOF
)
	
	
fi


#################################################################################
### Invalidate our token
#################################################################################
/usr/bin/curl -s "${jss}/api/v1/auth/invalidate-token" -H "Authorization: Bearer ${token}" -X POST

 

 

kwoodard
Contributor III

Man, you are a rock star! This was next on my list to tackle. I have a great script to do erase and install, but getting user boxes updated when the user isn't an admin has been a giant pain in the butt. I am thinking I can scope this to an item in Self Service so the user can click on the item, have it display the dialog to save any work, then have it do the update...that would be awesome. For the labs, run it on a schedule.

@kwoodard Thanks!  Sorry for the late response, it's been crazy busy around here.  But yeah, my intention for this was unattended lab stations.  It would need some tweaking for standard users due to the heavy-handed logout, and despite working well late at night in labs, there's no real indication that the MDM call is going to take effect.  I still see about 10% of my computers randomly ignore the command and need a 2nd attempt.  The command is successfully sent & logged, the update just doesn't kick off.   I would probably suggest something else to notify/allow your standard users to kick off updates (upgrades are another issue). 

mjones2
New Contributor

Hey Jon. I am attempting to implement your script for our MacOS updates. I am new to JAMF and Mac administration so bear with me. First, I can get the logout prompt to work and it will logout the user, but I think I might have the parameters for $4 $5 and $6 incorrect. For $4 I have our jss url https://mycompany.jamfcloud.com/ ; for $5 and $6 I am using our admin account that we have on all managed devices. So the current flow of everything is that it will log the user out automatically after the timer expires, but the updates never download and install automatically. Any potential help would be greatly appreciated. Thanks.

jwaltonen
New Contributor III

I think this might be a good place to take this up.

I have been using the following API command with great success, to force OS updates in the middle of the night:

curl -X POST "https://ourJamfCloudAddress/api/v1/macos-managed-software-updates/send-updates" -H "accept: application/json" -H "Authorization: Bearer $api_token" -H "Content-Type: application/json" -d "{\"deviceIds\":[\"${computerID}\"],\"maxDeferrals\":0,\"skipVersionVerification\":true,\"applyMajorUpdate\":false,\"updateAction\":\"DOWNLOAD_AND_INSTALL\",\"forceRestart\":true}"

 

This has brought hundreds of computers up from 12.3 ish all the way up through 13.1 one point update at a time.

Personally I feel as if this command as written above should not have moved the machines from 12 to 13 given applyMajorUpdate is set to false.  But it did.  That's another story though.

Whats  more upsetting is that now on 13.1 almost none of the intel machines will go to 13.2.  Maybe 3 out of 300 made it.  All of the Apple silicon macs did do the updates.  I have way more intel classroom computers than Apple Silicon ones though.

Anyone else using a similar command and seeing similar results after 13.1 ?

Also of note, an MDM command with similarly optioned, pushed from the webui also does not work to move the intels past 13.1.

 

 

 

Apple released macOS 13 as a minor update.  So even if you didn't tick 'include Major updates', it would still upgrade the devices.  12.6.2 would upgrade whereas 12.6.3 would not, Apple realized what they had done and patched 12.6.3 to not auto upgrade, but for most people it was too late.  There was an Apple Support article about this issue and how to block it but I can't find it right now.

I have the opposite problem, I have 25x arm64 iMacs stuck on 13.1

For Intel devices I try more draconian tactics using software update:

softwareupdate -i -a -R

If that doesn't work you can deploy the full 13.2 app installer out to the device and run the startosinstall command to auto upgrade the device.

/Applications/Install\ macOS\ Ventura.app/Contents/Resources/startosinstall --agreetolicense --nointeraction --forcequitapps

Add a restart command if its needed.

Use the 'Download Full Installer' app to download the 12.5Gb PKG installer from Apple.

https://github.com/scriptingosx/DownloadFullInstaller 

You can also try:

softwareupdate --fetch-full-installer --full-installer-verson 13.2

and run the startosinstall command after that.  Although this method isn't always successful at downloading the full installer app.

Also, make sure your Intel devices don't have pending MDM commands stuck in them (check the management tab is clear of pending & failed commands).  If so, they will block the upgrade MDM commands as they will be added to the bottom of the list of pending commands.  You may need to cancel all MDM commands on the Intel devices via a mass action before sending the upgrade OS MDM command.

jwaltonen
New Contributor III

     "softwareupdate -i -a -R"

Hasn't worked for me since Big Sur.  A couple machines will update, most all just reboot without applying the update.

 

      "25x arm64 iMacs stuck on 13.1"

How are you trying to update them?

 

     "full installer app"

Was trying to avoid that but it looks like that may be the only option right now.

 

 

 

rstasel
Valued Contributor

yeah, that won't work for Apple silicon generally. use the API to push an MDM command is your best bet, or something like Nudge (https://github.com/macadmins/nudge) for user driven update, or Super (https://github.com/Macjutsu/super) for more IT driven side.

Big Sur has issues with software update.

In some cases it will say there are no updates available.  In other cases it will say it cannot reach the Apple servers.  Yet, if you manually upgrade the same machine to macOS 12 using the same wall port, it will suddenly reach the Apple servers and find updates. O_o?  Big Sur has bugs I guess.

 

25x arm64 - Using a mass action update command via Jamf GUI.

They all went through ADE, are all supervised, all have a bootstrap token escrowed in Jamf, none have pending or failed MDM commands and are all checking in regularly with the Jamf server.

I think for me it may be a network related issue (SCEP) as arm64 devices upgrade fine at one campus (using a mass action) but not at the other campus for those specific rooms. 

 

Full installer app

I have found this to be the most reliable option on older machines that wont play ball and that don't work properly or reliably with softwareupdate.

-ir --verbose

has been working for me on intel machines. Still working on Silicon.

mjones2
New Contributor

Hey all. Have you guys found that using a tool such as the "Download Full Installer" and then using script to run it as the most efficient way to get updates pushed? When going this route, would the user still be able to deny the update? As part of  new security requirements at my company we have to be able to push the updates at "X" time and the user will be prompted, but can't defer past a certain time. We have a mixture of Apple and Intel silicon devices. I haven't specifically tested/tried any of the scripts posted here, but I am just trying to do more research before implementing any of them. Thanks.

tdenton
Contributor

Morning All 

Just testing this scripit out on some of our M1 devices can anyone help with what this means,

Script result: eyJhbGciOiJIUzI1NiJ9.eyJhdXRoZW50aWNhdGVkLWFwcCI6IkdFTkVSSUMiLCJhdXRoZW50aWNhdGlvbi10eXBlIjoiSlNTIiwiZ3JvdXBzIjpbXSwic3ViamVjdC10eXBlIjoiSlNTX1VTRVJfSUQiLCJ0b2tlbi11dWlkIjoiMTZhZGQ3MmMtNjc3Ny00OTE2LTk5OWYtNGI5MzdhZTY3ZTEzIiwibGRhcC1zZXJ2ZXItaWQiOi0xLCJzdWIiOiI2IiwiZXhwIjoxNjkxMzk5MzMwfQ.poPO9MR8WkBRU4Eusfp0TDraRPl9eUNDBk5qcpNEq0E
Device ID 2824 allow major macOS upgrade? false attempting to apply the latest available macOS version { "processManagerUuids" : [ "d4bb3d33-8747-4912-9282-7c106bc7be29" ], "errors" : [ ]

Thanks

dan-snelson
Valued Contributor II

@tdenton: You may wish to check-out Nudge.

@dan-snelson Im looking for something that can be run independently as I was hoping to use this in lab based enviorments out of hours.

We currently use MDM software update workflows but its not very reliable.

It seems to be going through the motions but never seems to be update.


Thanks