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

--
Dan
55 REPLIES 55

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

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


Hire me as an independent contractor.

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).